v1.2.0
This release adds a future block to gtkx.config.ts, a way to take one of the next major version's behaviors at a time rather than all of them at an upgrade, and its first flag, v2ByteArrays, binds every GIR byte sequence as a Uint8Array instead of a number[], decoded as a single copy in the addon rather than one JavaScript number per byte. One half of that work lands whether or not a project opts in: a byte-sequence parameter takes Uint8Array | number[] everywhere, and a GByteArray argument accepts a typed array for the first time.
The rest of 1.2 is a round of corrections in the generated bindings and in codegen itself. Pointer-typed values are bigints, which is also what lets a GList or GPtrArray of raw pointers marshal at all; connect, on and off accept only a signal the class declares, which is what was letting useToast's handlers run with no toast; property accessors read and write through GObject carrying the property's own declared type; and the reference counting and free methods GIR declares are no longer bound, since the native layer owns those references. Codegen emits the generated stores without building a TypeScript program over them, which makes a store ECMAScript modules whatever the project's package.json declares and makes the cost of writing one track the number of modules emitted. Namespaces that could not be bound at all now bind: one whose static function narrows an inherited one, which is every gdbus-codegen proxy, and one carrying a type whose name starts with a digit. And deploy.flatpak.mode: "source" renders a manifest a pnpm project can actually build.
Changelog
New Features
Future flags, starting with v2ByteArrays
gtkx.config.ts takes a future block, which opts a project into behavior that becomes the default in the next major version, so an upgrade can be taken one change at a time instead of all at once. Every flag is off by default, codegen never warns about one that is not set, and a value that is not a boolean fails config validation.
export default defineConfig({
applicationId: "com.example.Tasks",
future: { v2ByteArrays: true },
});The first flag is v2ByteArrays, which represents GIR byte sequences as Uint8Array rather than number[]. It covers guint8 C arrays and GByteArray wherever they are read: return values, out parameters, record fields and properties. GLib.fileGetContents becomes (filename: string) => [boolean, Uint8Array], GLib.base64Decode returns a Uint8Array and so does the remainder GLib.utf8Validate reports, Gio.File.loadContents returns [boolean, Uint8Array, string | null], GdkPixbuf.Pixbuf.getPixels returns a Uint8Array, and Gio.TlsCertificate's certificate and privateKey read Uint8Array | null, in the @gtkx/gi classes, the @gtkx/jsx props and the matching onNotify handlers alike. The descriptor emitted behind such a value carries isBytes: true, or t.byteArray for a GByteArray, so the bytes cross the boundary as one copy instead of being unpacked element by element. The handwritten Cairo overrides and the OpenGL bindings are untouched, since they already use typed arrays.
Method parameters are the same either way, Uint8Array | number[], so the flag never breaks an imperative call that passes bytes in. What changes is what comes back, plus the construct-time inputs that carry a byte sequence: the @gtkx/jsx props, the @gtkx/gi constructor props, record initializers and writable record field setters all narrow from number[] to Uint8Array. Code that calls .push, .concat, Array.isArray or JSON.stringify on a byte-sequence result has to be updated as well. Flip the flag and run tsc: every site that needs attention is a type error.
The setting is hashed into the generated store's fingerprint, so changing it makes the next gtkx dev, gtkx build or gtkx codegen regenerate @gtkx/gi and @gtkx/jsx on its own, and codegen reports the enabled flags on its own line as codegen: future=v2ByteArrays. gtkx docs and the @gtkx/mcp API reference read the same setting, so the reference pages and gtkx_list_api, gtkx_search_api and gtkx_get_api_docs describe byte sequences exactly the way the installed store carries them.
Flathub source builds for pnpm projects
deploy.flatpak.mode: "source" now renders a manifest a pnpm project can build. The Node SDK extension ships no pnpm and the Flathub sandbox has no network to fetch one, so the module vendors pnpm itself: an archive source pinned to the pnpm tarball's sha512 and unpacked into flatpak-pnpm, a script source whose dest-filename is pnpm and which execs node /run/build/<binaryName>/flatpak-pnpm/bin/pnpm.cjs, and that directory prepended to build-options.append-path ahead of the Node extension's bin. The install command is pnpm install --offline --frozen-lockfile, with --trust-lockfile appended on pnpm 11, whose supply-chain check otherwise reaches the registry. flatpak-node-generator is invoked with --pnpm-store-version v10 or v11 matching the pinned major, so the vendored store layout matches the pnpm that reads it, and the npm-only npm_config_cache and npm_config_offline variables are no longer set on a pnpm build, leaving npm_config_nodedir alone. Before, the same manifest ran a pnpm command that nothing in the sandbox provided, and generated the offline sources without a store version.
The pnpm version comes from packageManager in package.json. Write it with corepack use pnpm@<version>, which records the +sha512. digest every Flathub source has to carry; a project with no packageManager field gets pnpm 11.21.0. gtkx deploy refuses to render when the field names a manager other than pnpm while the build installs with pnpm (a pnpm-lock.yaml in the project root, or deploy.flatpak.packageManager: "pnpm"), when it carries no sha512 digest, and when it pins a pnpm outside 10.x and 11.3.0 or newer, since --trust-lockfile does not exist before 11.3.0. Vendoring also needs a flatpak-node-generator that supports --pnpm-store-version, an option newer than the generator's last tagged release: preflight runs flatpak-node-generator --help, counts a copy without that option as missing, and the tool's install hint gains the pipx install --force command that replaces a copy too old to vendor pnpm. npm and yarn manifests keep the same sources, append-path and install commands as before.
Breaking Changes
These are corrections to the generated bindings and to @gtkx/runtime's descriptor surface. Most of them are type-level, and tsc names every site that has to change once the store is regenerated, which happens on the first gtkx dev, gtkx build or gtkx codegen after the upgrade.
connect, on and off only accept a signal the class declares
Each generated class declared its signal methods twice: once keyed on its own signal map and once over a plain string. The second overload accepted anything, so a misspelled signal name, a signal belonging to a different class, and a handler declaring parameters the signal never supplies all type-checked and then misbehaved at runtime. @gtkx/components hit exactly that: toast.on("button-clicked", onButtonClicked) resolved through the string overload, and the callback ran with undefined where the toast was meant to be.
Every generated class now declares connect, on, once, off, addEventListener and removeEventListener only in the K extends keyof <Class>Signals form; the plain-string overloads are gone. emit(sigName: string, ...args: unknown[]): unknown is unchanged. Code that connected through a widget typed as an ancestor has to narrow it first, so a Gtk.Widget handed back by a query has to be resolved as a Gtk.Entry before connect("delete-text", ...) compiles, and a handler that declares more parameters than the signal passes has to be wrapped.
Detailed notify names are covered by an index signature on GObject.ObjectSignals and GObject.ObjectSignalEmit, so on(`notify::${string}`) type-checks for any property, including one installed through registerClass({ properties }), where 1.1 listed only the properties GIR declares. addEventListener and removeEventListener are also marked @deprecated in favor of on and off, and are to be removed in v2.
gpointer values are bigints
Every GIR type that resolves to a raw pointer, gpointer and gconstpointer, was declared number and marshalled through t.uint64, the unsigned 64-bit descriptor that decodes to a JavaScript number, which cannot represent an address above 2^53 exactly. Both are bound as t.biguint64 and typed bigint now, everywhere they appear: method parameters and returns, record fields, properties, element props, constants, and the userData argument of every generated vfunc signature. GObject.Value.getPointer() answers 0n rather than 0 and setPointer takes bigint | null, GObject.Object.getData, setData and stealData take and return bigint | null, Gtk.TreeIter.userData, userData2 and userData3 read and write bigint, GdkPixbuf.Pixbuf's pixels prop is bigint | null, Gtk.BuildableParseContext.pop() returns bigint | null, GLib.Sequence.append takes bigint | null, and Gio.TlsClientConnection.getAcceptedCas() returns bigint[]. It is also what lets a GList or GPtrArray of pointers marshal at all.
An integer Number inside the 2^53 safe range is still accepted where a pointer is passed in, so the work is on the way out: code that stores a returned pointer in a number, compares it against a number literal or does arithmetic on it has to move to bigint, so value.getPointer() === 0 becomes value.getPointer() === 0n. The API reference pages render the new type as well, since they share the same primitive table.
Reference counting and free methods are no longer bound
Every generated class and record carried the lifetime methods GIR declares on it, so ref, unref, refSink, forceFloating and free type-checked and called straight through to g_object_ref, g_object_unref and each boxed type's own release function. The native layer owns those references: it holds one on every handle it hands out and releases it when the handle is dropped, so a call to unref or free from TypeScript dropped a reference the runtime was still counting on and left the object free to be finalized while live handles still pointed at it.
No class or record member named force_floating, free, ref, ref_sink, take_ref or unref is emitted any more, neither on GObject.Object, which put ref, unref, refSink and forceFloating on every widget and application, nor on records such as GLib.Bytes, GLib.Variant, Pango.AttrList, Gdk.RGBA and Gtk.TextIter. Namespace-level functions are untouched, so GLib.free, which is g_free, is still bound. A project that called one of the removed members has to delete the call: the handle is released when it becomes unreachable, and there is no supported way to release it by hand.
Property accessors carry the declared property type and go through GObject
A generated get/set pair used to delegate to the class's own accessor methods whenever GIR named them, so video.file = f compiled to this.setFile(f) and button.label to this.getLabel(), and each direction took its type from the method behind it, which is the per-direction typing 1.1 introduced. Both the delegation and the per-direction typing are gone. Every accessor reads through g_object_get_property and writes through g_object_set_property, and both directions carry the property's declared GIR type. A property is nullable when GIR gives it default-value="NULL" or when its type resolves to anything other than a primitive or an enum, and a property that is not readable no longer gets a getter just because a getter method exists.
Two things change for a project. Types move, and mostly widen: Gtk.Button.label, whose GIR property carries default-value="NULL", accepts string | null on write where 1.1 accepted only string, while Gtk.Label.label, which carries no default, stays string in both directions. And a write no longer passes through the set<Name> method, so a subclass that overrode setFile no longer sees a write to file, and a test that spied on Gtk.Video.prototype.setFile has to spy on the file setter instead. The get<Name> and set<Name> methods themselves are still emitted and still call the C functions directly.
The runtime's array descriptors take an options object
t.gArray, t.sizedArray, t.cursorArray and t.fixedArray from @gtkx/runtime took the inline element stride as a trailing elementSize?: number argument. That argument is now the ArrayOptions object t.array already took, so t.sizedArray(t.uint8, 1, "borrowed", 24) has to be written t.sizedArray(t.uint8, 1, "borrowed", { elementSize: 24 }), and t.list, t.slist and t.ptrArray gained the same trailing argument.
ArrayOptions carries a new isBytes key. A descriptor marked with it decodes to a Uint8Array rather than to an array of numbers, and the addon refuses to build the codec unless the item descriptor is t.uint8, so t.fn throws when a byte array is declared over a wider element. t.byteArray sets it for itself, which means a hand-written GByteArray descriptor decodes to a Uint8Array whatever the project's future block says. A descriptor that has to keep the numeric representation spells it t.array(t.uint8, "gbytearray", ownership), which is exactly what codegen emits for a project that has not set future.v2ByteArrays.
Only code that writes FFI descriptors by hand is affected: the generated store is rewritten by codegen on the first gtkx dev, gtkx build or gtkx codegen after the upgrade.
Smaller breaking changes
StaticBase<C, K>from@gtkx/runtimeno longer defaultsKto"new", so code that namedStaticBase<typeof Foo>has to name the keys it omits. The generated store applies it to every static a class redeclares incompatibly with the one it inherits, rather than to its constructors alone.
Bugfixes
- Fixed the generated
@gtkx/giand@gtkx/jsxstores being emitted as CommonJS in any project whosepackage.jsondoes not declare"type": "module". The store was compiled in a staging directory undernode_modulesbefore its own manifest was written there, so TypeScript resolved the module format from the nearest enclosing manifest, which is the project's, and emitted"use strict"modules assigning toexportsand__esModuleinto a package that then declared itself ESM, a combination Node rejects. The manifest is written before anything is compiled, codegen drops a{"type": "module"}manifest into any directory it type-checks that has none, and each module is transpiled on its own rather than through a program that consults the surrounding directory. Projects scaffolded bygtkx createwere never affected, since that template already sets"type": "module". - Fixed the setter generated for a record field that is an array of inline structs writing every element of the array it was handed, however long that array was, straight past the end of the memory the field covers, since the native write applies the byte offset without bounds-checking it against the handle. Assigning to
Pango.GlyphString.glyphs,Gio.InputMessage.vectors,GObject.EnumClass.values,GObject.FlagsClass.valuesorGObject.Value.dataan array longer than the field's own length, which isnumGlyphs,numVectors,nValuesand a fixed 2 respectively, wrote over whatever followed the struct. The generated loop breaks at that length now, so the extra elements are dropped instead of corrupting memory. A field whose inline element struct exposes no readable leaf field also no longer gets a struct-array accessor at all, where it used to emit a getter that returned an array of empty objects and a setter that wrote the array back untouched; such a field falls through to the ordinary field accessor. - Fixed a
GList,GSListorGPtrArrayof raw pointers being unusable in both directions. Decoding one read each element as though the pointer were a returned integer and threw, soGio.TlsClientConnection.getAcceptedCas,Gio.DtlsClientConnection.getAcceptedCas, theaccepted-casproperty on both of those interfaces, and thedns-namesandip-addressesproperties onGio.TlsCertificatethrew on every call that had anything to return, and answered correctly only when the container was empty. Encoding failed differently per container: aGPtrArrayargument ran its elements through the GObject handle check and rejected every one of them, and aGListargument handed the callee a flat buffer of 64-bit words where a chain of list nodes was expected. Pointer elements are read out of the container one word at a time and encoded back into container nodes, sot.list,t.slistandt.ptrArrayoft.biguint64round-trip, and an element that is not a pointer-sized integer is rejected with the index that carries it. - Fixed
useToast'sonButtonClickedandonDismissedhandlers being called with no arguments at all, althoughToastOptionstypes both as receiving theAdw.Toast, so the parameter wasundefinedfor the life of the toast. The handlers were connected straight to thebutton-clickedanddismissedsignals, and a signal handler is invoked with the signal's own arguments only, without the emitter. Both are wrapped now and receive the toast they were registered for. - Fixed a generated class whose static function narrows one it inherits producing a store TypeScript refuses. The extends clause hid only the class's own constructors behind
StaticBase, so a GIR<function>such asnewornew_for_busthat a subclass redeclares with a different parameter list, which is whatgdbus-codegenemits for every D-Bus proxy, left the subclass's static side incompatible with its parent's and failed the store's type check. DeclaringUDisks-2.0inlibrariescould not generate at all: 28 of its proxy classes redeclareGio.DBusProxy.newandnewForBuswithout theDBusInterfaceInfoparameter, andObjectManagerClientnarrows the pair it inherits too, 58 statics in all. The clause now hides every static the class declares, constructor or function alike, and only those whose signature is not assignable to the one it shadows. - Fixed a GIR type whose name starts with a digit emitting a TypeScript identifier that starts with a digit, which is a syntax error, so the whole store failed to compile and the namespace could not be bound at all. Identifier escaping covered reserved words but not a leading digit, although the enum member and method name paths already handled it, so NetworkManager's
80211Mode,80211ApFlagsand80211ApSecurityFlagscame out asexport enum 80211Mode. Such a name is prefixed with an underscore now, so those types are emitted as_80211Modeand every reference to them resolves. - Fixed every generated parameter that takes a
guint8C array or aGByteArraybeing typednumber[], so aUint8Arrayalready in hand had to be spread into a plain array before it could be passed. Those parameters areUint8Array | number[]whether or notfuture.v2ByteArraysis set, soGLib.Bytes.new(surface.getData())takes whatCairo.ImageSurface.getDatareturns directly, where it previously neededGLib.Bytes.new([...pixelData]). AGByteArrayargument was also refused at the FFI layer, where the codec declared no buffer-view support and threw for anyArrayBufferView; it encodes one now, soGLib.ByteArray.append(new Uint8Array([1, 2]), new Uint8Array([3]))works alongsideGLib.ByteArray.append([1, 2], [3]). A view whose element type is not a byte, anInt16Arrayor aDataView, still throws. - Fixed
deploy.flatpak.source.tagrendering a git source that carried only the tag, so a Flathub submission tracked a movable ref rather than the fixed tree it was generated from.gtkx deploynow resolves the tag withgit rev-parse <tag>^{commit}in the project root and writes bothtagandcommitinto the manifest, and fails with a message pointing atdeploy.flatpak.source.commitwhen the tag does not resolve in the checkout. Configuringcommitexplicitly behaves as before. - Fixed a
.girthat is not well-formed XML being reported as a parser message and a position with no sight of the offending line, whose character is by definition unprintable.GTop-2.0.girships a raw U+0004 inside<constant name="EOT_STR">, so a project that namedGTop-2.0inlibraries, readily reached throughlibraries: "*", gotIllegal control character 0x04 in attribute 'value' value. (line 48, column 30)over a file whose offending byte is invisible in an editor. The offending line is now printed with its control codes escaped, so that constant readsvalue="[U+0004]", followed by what to do about it: report the file to whoever ships it, leave the library out oflibraries, or put a corrected copy in a directory named bygirPath, which is searched ahead of/usr/share/gir-1.0.
Performance
- The generated
@gtkx/giand@gtkx/jsxstores are emitted without building a TypeScript program. Codegen used to create ats.Programover every generated module, symlink its ownnode_modulesbeside them so their imports resolved, and rungetPreEmitDiagnosticsacross the whole store before emitting anything; each module is now transpiled on its own withts.transpileDeclarationandts.transpileModule, so the cost of writing a store tracks the number of modules emitted, 52 JavaScript files for@gtkx/giand 25 for@gtkx/jsxon the defaultGtk-4.0,Adw-1,GtkSource-5andWebKit-6.0set, rather than the size of the type graph those modules form, and nothing is resolved across module boundaries or loaded from@gtkx/runtime,@gtkx/reactand the React and Node type declarations. Errors a single module raises on its own, such as a declarationisolatedDeclarationscannot emit, still fail the run, and the generated OpenGL modules still go through the full type check. A store that would not type-check as a whole is emitted anyway, so an error that used to stop codegen surfaces where a project uses the binding it affects; a failure that does stop the emit is reported asCompiling the generated @gtkx/gi store failedwhere 1.1 saidType checking. The__gtkx-env__.d.tsreference file the jsx store carried only for the sake of that check is gone. - Under
future.v2ByteArrays, decoding a byte sequence copies the whole run into a freshUint8Arrayin one pass rather than creating one JavaScript number per byte and setting each into a JS array, so reading an n-byte buffer such asGLib.fileGetContents,GdkPixbuf.Pixbuf.getPixelsor aGByteArrayreturn costs a byte copy of the buffer instead of n N-API value creations. Sized, fixed, cursor, zero-terminated,GArrayandGByteArraycontainers all take that path, and an empty or null sequence decodes to an emptyUint8Arraywithout allocating a JS array at all.