Read the announcement: GTKX 1.1: Introducing gtkx deploy.
This release adds gtkx deploy, which turns a project into the packages a Linux user actually installs: a Flatpak bundle, a .deb, an .rpm and an .AppImage, with the desktop entry, the AppStream metainfo, the Flatpak manifest and the deb and rpm control metadata generated from a deploy block in gtkx.config.ts rather than written by hand. The rest of 1.1 is a large round of defect fixes across every layer. The FFI runtime stops leaking subclasses that override a teardown slot, stops freeing the memory an async call lends, and reads the out parameters and struct fields it used to refuse or misread. The generated bindings declare abstract types abstract, type property accessors per direction and drop the return values GIR marks as skipped. gtkx dev survives a throwing component and stops reporting refreshes it did not apply, gtkx build refuses a bundle that would resolve anything at runtime, and @gtkx/testing follows GTK4's own key propagation, selection handling and window state instead of approximating them.
Changelog
New Features
gtkx deploy builds the packages a Linux user installs
gtkx deploy turns a project into installable packages. One command builds a .flatpak bundle, a .deb, an .rpm and an .AppImage, and the desktop entry, the AppStream metainfo, the Flatpak manifest and the deb and rpm control metadata are generated rather than written by hand. A 1.0 project that carried bundling scripts, a sea-config.json, a desktop entry, a metainfo file and a Flatpak manifest of its own can delete all of them.
deploy.targets picks the default set and --target deb,rpm overrides it for a single run. With neither, the command builds a Flatpak. --out changes the output directory, which defaults to build, and --skip-build packages what is already in dist/ instead of rebuilding.
Everything derivable is derived from what the project already declares
A small application configures three or four keys. summary, categories and targets are usually the whole block, because name, version, license, developer, homepage and description fall back to package.json, the icons come from the data/icons/ tree gtkx build already reads, and the metadata license, copyright and a first release entry are filled in. The deb Section and the rpm Group come from categories, the deb Depends and rpm Requires come from the libraries already declared, so ["Gtk-4.0", "Adw-1"] becomes libgtk-4-1, libadwaita-1-0 on Debian and gtk4, libadwaita on Fedora, and the glibc floor is read out of the built binaries rather than guessed.
Run the command with no deploy block and it prints a starter block with every derivable value already filled in, ready to paste into gtkx.config.ts. The application icon is the one file that has to exist and has to be named after the application ID, since the desktop entry names the ID as its icon, and the command says so when it is missing.
Metadata is validated before the application is built
The desktop entry and the metainfo depend only on configuration, so they are rendered and checked with desktop-file-validate and appstreamcli before the app is bundled. A category typo or a summary ending in a period fails in seconds, with the validator's own message, rather than after a full Flatpak build.
An AppStream error always stops the deploy. A warning stops it too when the run is a Flathub source submission, meaning deploy.flatpak.mode: "source" with the flatpak target selected, since that metainfo goes to a software center; for the other formats it is reported and the build continues. Either way, a rule that maps to configuration is printed with the key that fixes it, so url-homepage-missing says to set deploy.homepage or homepage in package.json. gtkx deploy --print-manifests renders and validates the metadata and every target's manifest, then stops without packaging, and needs none of the packaging tools.
One staged tree, and every target installs it unchanged
Every format installs the same payload: a launcher script at bin/<binaryName>, the bundled Node.js, the app bundle, gtkx.node and the compiled settings schemas under lib/<binaryName>/, and the desktop entry, metainfo, icons and schemas under share/. It goes under /usr for deb, rpm and AppImage and under /app for Flatpak, and nothing is rewritten between them, because the launcher resolves everything relative to its own location and the bundle resolves the addon and the compiled schemas from beside itself.
Node.js is bundled because GTKX needs Node.js 24 and the distributions ship older. gtkx deploy downloads the official nodejs.org build matching the Node.js running the deploy, verifies it against the published SHA-256 and caches the archive under ~/.cache/gtkx/node/, so only the first deploy needs the network. deploy.node.source: "host" copies the Node.js running the build instead, and is refused with an explanation when that binary links against something the target machine will not have, which is the case for the Node.js packages Fedora and Debian ship. deploy.node.path points at a binary of your own.
Flathub submissions build from source
gtkx deploy --target flatpak builds from the staged tree, offline and in seconds. Flathub builds every submission from source instead, so deploy.flatpak.mode: "source" emits a manifest that does the same: a git source pinned to the release, taken from the origin remote when deploy.flatpak.source.url names none, dependencies vendored for the network-isolated sandbox with flatpak-node-generator, and the generated metadata carried as inline sources, so nothing generated has to be committed to the repository. It wants a lockfile, a package-lock.json, a pnpm-lock.yaml or a yarn.lock, because the sandbox installs offline and has no network to fetch anything else; deploy.flatpak.packageManager picks which one when the project carries several.
Sandbox permissions are the one thing still written by hand, through deploy.flatpak.finishArgs. They are a security decision rather than something to infer from configuration, and the default asks for a window and hardware rendering and nothing else. deploy.flatpak.modules and deploy.flatpak.buildCommands add modules and build steps on top.
New projects are ready to deploy
create-gtkx writes a deploy block into gtkx.config.ts with the display name, summary, description and categories already filled in, an application icon at data/icons/hicolor/scalable/apps/<applicationId>.svg, and a deploy script, so gtkx deploy builds a package out of a fresh project without any further configuration. package.json gains a description, an MIT license and an author taken from git config user.name and user.email, all of which the deploy metadata falls back to.
Two smaller template changes come with it. start runs dist/bundle.mjs, matching the extension the build now emits, and tsconfig.json maps @gtkx/gi/* and @gtkx/jsx/* onto node_modules/.gtkx directly, so type-checking survives a package manager that prunes the store's links. A project scaffolded at 1.0 wants the same paths entry.
Smaller additions
nfpmandappimagetoolare downloaded, checksum-verified against pinned digests and cached under~/.cache/gtkx/, so a Fedora machine builds.debpackages and a Debian machine builds.rpmpackages.desktop-file-validate,appstreamcliand, for Flatpak,flatpak-builderare yours to install, and a run missing any of them lists all of them at once with the install command for the detected distribution.deploy.isDbusActivatablewritesshare/dbus-1/services/<id>.servicenext to the desktop entry for deb, rpm and Flatpak, pointing at the installed launcher with--gapplication-service, and dropsDBusActivatablefrom the AppImage copy, where no service file can be installed.- A
deploy.screenshotsentry can name afilein the repository instead of an absoluteurl, and the base URL is derived from theoriginremote for GitHub and GitLab, including the project's path inside the repository. Setdeploy.screenshotBaseUrlwhen the remote is somewhere else. deploy.releases,deploy.contentRating,deploy.brandinganddeploy.urlsfill the AppStream sections a software center reads, anddeploy.fileAssociations,deploy.protocolsanddeploy.desktopActionsfill the desktop entry, its MIME package and its action groups.deploy.desktopEntryadds or overrides desktop entry keys,deploy.extraFilesmaps prefix-relative destinations to files in the project,deploy.dependsanddeploy.relationsadd package relationships per format,deploy.scriptssupplies maintainer scripts, anddeploy.signingsigns the.deb, the.rpm, the Flatpak repository or the AppImage.- The whole
deployblock is validated with the rest ofgtkx.config.ts, so a misspelled key or a badly typed value is reported asgtkx.config.ts: `deploy.<key>` ...before anything is built. - Deploying documents every field, and the tutorial's packaging and Flathub appendices are rewritten around the command instead of the eleven hand-written packaging files they used to walk through.
- A method whose caller-allocated out parameter is a record with no public constructor used to be dropped from the bindings entirely. Codegen allocates the struct from the layout it already knows, so those methods are emitted and callable.
t.cursorArrayjoins the runtime's descriptor surface, for a pointer that reports a position inside a buffer the call was already given.configure({ windowActivationTimeout })sets how longrenderwaits for the window it shows the tree in to become active, defaulting to five seconds.actionabilityTimeoutkeeps its 500 ms and now covers user events only, so a slow machine can raise one without loosening the other.
Breaking Changes
None of these is a deliberate change to the public API. Every one of them falls out of fixing a defect or a behavior that was never intended, and holding those fixes back for a major release would have meant shipping 1.1 with the bugs still in it. Correcting them in a minor release was the trade we chose, so what follows is here to tell you where a fix changes something your code can observe, not to introduce anything new.
The built bundle is dist/bundle.mjs
gtkx build writes dist/bundle.mjs where 1.0 wrote dist/bundle.js, and every other chunk it emits carries the same extension: shared chunks land in dist/assets/<name>-<hash>.mjs and worker chunks in dist/workers/<name>-<hash>.mjs. The artifact is ESM whatever the project's package.json declares, so a project on "type": "commonjs" no longer gets a successful build followed by an artifact Node refuses to run. The extension is what carries that guarantee: Node decides how to parse a .js file from the nearest package.json, and a packaged application is installed into a directory that has none.
Everything naming the old path has to be updated: the start script in package.json, a packaging manifest, a systemd unit, a Dockerfile, a CI step, and any packaging glob matching dist/**/*.js. gtkx build prints the path it wrote rather than a hardcoded one, so Build complete: dist/bundle.mjs names the real artifact and follows a custom build.outDir.
gtkx build refuses a bundle that resolves anything at runtime
The build inspects every chunk it emits and fails when the code would resolve a module at runtime that is neither a node: builtin nor a file emitted beside it. That covers a require built through createRequire, a require.resolve call, a new URL(specifier, import.meta.url), and any import of a bare specifier that survived bundling. The error names the chunk and every specifier it found.
This is the check that catches an application which starts from its project directory and dies with MODULE_NOT_FOUND once it is installed anywhere else, which is exactly what packaging does to it. A build that succeeded at 1.0 can fail here. A project whose code reads a file or a manifest relative to its own source at runtime has to bundle the module, emit the file next to the bundle, or read the data at build time.
Abstract classes and opaque boxed types refuse new
Every generated class carried a public constructor. new Gtk.Widget({}) type-checked and then aborted the process on GObject's own assertion, and new GLib.KeyFile() type-checked and then threw. A class GIR marks abstract is now emitted abstract, and so is a record whose layout codegen cannot construct, which covers the opaque boxed types across every namespace: GLib.KeyFile, GLib.MainLoop, GLib.Bytes, GLib.Checksum, GLib.DateTime, GLib.VariantType, Pango.AttrList and the rest. Both now fail at the new, at compile time, rather than at runtime.
Those records also lost their ConstructorProps interface, which only ever described an empty object, so code naming GLib.KeyFileConstructorProps and its siblings no longer compiles. The error a new reached through a loosely typed path still throws names the way in, pointing at the record's own constructor function such as GLib.KeyFile.new(). Subclassing is unaffected: registerClass keeps a base's abstractness on its static side, so extending an abstract generated class registers and constructs as before.
A skipped GIR return value is dropped from the result
GIR marks a return value as skipped when it carries nothing the caller needs, which for GLib.Uri.split, GLib.Uri.splitNetwork and GLib.Uri.splitWithUser is the gboolean that a throwing function already reports by throwing. The value was packed into the result tuple anyway, one element longer than the declared type said, so every destructured URI component came back shifted by one and type-checked regardless. A skipped return is now dropped everywhere it appears: from a call's result tuple, from the value an emitted signal returns, and from the value a vtable slot's implementation is expected to supply.
GLib.Uri.split returns seven components rather than eight, splitNetwork three rather than four, and splitWithUser nine rather than ten. Code that followed the declared types needs no change; code that indexed around the extra leading element has to drop the offset.
Property accessors carry a type per direction
A generated get/set pair took both of its types from the setter method and cast the getter's result to match. Where a property's getter and setter genuinely disagree, most often a getter that can answer null against a setter that cannot, the cast was simply wrong and the store failed its type-check. Each direction now carries the type of the accessor method that implements it, resolved against every base class that declares the same property, so a subclass never narrows what its parent declared. Where a delegate does not agree with the declared type it is dropped rather than dragging the whole pair down with it.
A read that was declared as the setter's type may now be wider, typically by | null, so an assignment that leaned on the old declaration needs a check. The API reference notes the write type on a property whose two directions differ.
Asset imports are GResource-backed only under #data/
@gtkx/cli/env.d.ts used to declare a path export on every asset extension it knows, *.png and *.svg among them, and a resource:// default on *.data and *.gpa. Only an import written through the #data/ prefix is staged into the GResource bundle, so those declarations promised exports nothing produced and path came back undefined. The declarations now cover #data/*.<ext> for the GResource path and the resource:// default, while a plain *.data, *.gpa or *.css?url import is typed as what it is: an asset URL, a dev server URL under gtkx dev and an absolute filesystem path once built.
gtkx dev and gtkx build both reject an asset import that binds a name the resource module does not export, naming the source file and the specifier, where 1.0 bound it as undefined and kept going. Move the file under the data directory and import it as #data/<path>, or import the default for the asset URL. A query suffix hands the import to Vite's asset pipeline instead of the GResource bundle, so #data/logo.png?inline does not export path either.
An installed property refuses a nullish write and leaves its range to the ParamSpec
A property installed through registerClass({ properties }) let null and undefined past its type guard whatever it held, so a null written to a gint property was stored as a JavaScript null: the member served null back, C read 0, and the two halves disagreed for the life of the object. Floating-point properties had the opposite problem, refusing values their own ParamSpec admits, so a gdouble bounded by -Infinity and Infinity reported Infinity as a type mismatch.
null and undefined now both mean NULL, and mean it only where the property's type can hold NULL, which is a string, string-array, boxed, object, interface, param, variant or pointer property. Such a property holds the same null for either spelling, so the member, the type's get_property slot and g_object_get_property agree. Every other property, so every integer, floating-point, boolean, enum, flags and GType one, refuses both with the same TypeError it refuses a string with and keeps the value it already holds. Code that cleared such a property with null and read it back as null breaks.
A floating-point property takes every JavaScript number and its ParamSpec alone rules on the range, so an unbounded gdouble holds either infinity while a value a bounded one excludes, NaN included, comes back as a RangeError where 1.0 raised a TypeError. A gfloat property holds what GObject narrows the double to, so it serves 0.1 back as 0.10000000149011612. Generated properties are unchanged and still marshal through their declared descriptor rather than through these checks, so new Gtk.Label({ widthRequest: null }) lands 0 where a gint installed by hand refuses it.
@gtkx/testing follows GTK's own input handling and window state
Four gates in the testing library stopped approximating GTK4 and started following it, and a suite written against the 1.0 approximations will see different outcomes.
userEvent.type and userEvent.paste delete the widget's selection before inserting, so a test that selected text and then typed gets the replacement GTK4 performs rather than an append, and type focuses the way clicking into a widget does, leaving its text unselected and the caret at the end, so a suite that relied on the old append order sees the replacement instead.
userEvent.keyboard no longer attaches a Gtk.EventControllerKey to the target widget, and only controllers with a connected key-pressed or key-released handler take part. Keys reach ancestors and stop at the first controller that claims the press, so an ancestor handler that never fired starts firing and a target handler downstream of a claiming ancestor stops.
Every userEvent helper rejects a widget that is in no toplevel or whose window is hidden, where the gate used to pass such a widget through, so a suite that renders into a detached container (a bare Gtk.Box, or a tree it unmounted first) and then drives it must switch to fireEvent. In the other direction, the widget's window no longer has to hold activation, so a helper that used to fail with "its window never became active" now runs, and so do the MCP gtkx_click and gtkx_type tools against a second toplevel.
render and rerender throw when the window the tree is shown in is not laid out and activated within windowActivationTimeout, where they used to resolve regardless, so a suite that mounts a window which never becomes active turns from passing with false negatives into failing. That budget is separate from actionabilityTimeout, which continues to bound user events alone.
Grouped collection views render trees
A ListView or ColumnView given both sections and items carrying ListItem.children used to render a flat list. Every child was discarded, no expander was drawn, and expandedIds and onExpandedChange did nothing, although each part of the surface documents the other as supported. Each section now builds its own tree: nested rows render, an item with children gets its expander, and controlled expansion drives every section at once.
A project that passes sections alongside items that happen to carry children will see rows appear where a flat list used to be, and has to either drop the children or feed onExpandedChange back into expandedIds the way an unsectioned tree already required.
Smaller breaking changes
gtkx devexits 1 when another process already owns the application ID, where 1.0 ran on as a windowless remote instance with no window and no diagnostic.gtkx dev,gtkx buildandgtkx deploythrow when the entry file does not exist, where 1.0 resolved a default entry path whether or not the file was there. A project whose entry sits somewhere unusual has to pass it as an argument.gtkx devexits 0 for an application that callsquit()during startup, where 1.0 exited 1 and blamed the command line.gtkx devrestarts the process on events it used to ignore: deleting a watched source file, savingvite.config.tsor a file that config imports or one of the mode's.envfiles, and a save that renames or removes an exported component. State held in the running application is lost on such a save, where 1.0 kept it at the cost of showing stale code.- A missing
gtkx.config.tsis reported asgtkx.config.ts: no configuration file found in <directory>, naming the directory that was searched, where 1.0 validated the empty configuration first and blamed a key inside a file it had not found. codegen: falseremoves a store from the project's ownnode_modulesonly when it shadows a store installed above it, andgtkx codegen --forcefails in such a project rather than exiting 0 without doing anything.- A project with
codegen: falseneeds a binding store built by 1.1, since it reuses an installed one rather than generating its own and a 1.0 store does not run against this release. Every other project regenerates on its firstgtkx dev,gtkx buildorgtkx codegenwithout being asked. - The generated store is written to the
node_modulesthe installed@gtkxpackages resolve from, not the project root's, which moves it to the workspace root in a hoisting workspace. Codegen refuses outright when the@gtkxpackages are split acrossnode_modulesdirectories. gtkx docs --outmust name a directory below the project root: an empty value, the root itself and any path outside it are refused. The output directory must be empty or hold amanifest.jsonwritten bygtkx docs, and the manifest gained a generator tag 1.0 did not write, so the first 1.1 run into a directory an earliergtkx docsfilled refuses until that directory is emptied.gtkx buildsetskeepNamesandchunkFileNameson the output and merges them last, overriding a project that configured either in its ownrolldownOptions.- An interface member whose name and signature collide with one anywhere in its prerequisite chain is omitted from the interface's declared type. Reach such a member through the implementing class.
- Calling a vtable slot whose array return is sized by a length out parameter through
callVfuncorcallParentreturns just the array, and an implementation of such a slot returns only the array too. offandremoveEventListenerdisconnect one connection per call, so a function connected several times to the same signal needs one call per connection. At 1.0 only the most recent connection was reachable at all.Gio.Application.getDefault()no longer answers with an application that was merely constructed:createApplicationleaves that default alone,runApplicationclaims it andquitApplicationreleases it.- An async call that lends the callee memory and is given no completion callback is refused with an error, where 1.0 made the call and freed the borrowed memory underneath the callee.
- The receiver a
vfuncDisposeorvfuncFinalizeoverride runs against is revoked when the override returns, so code that stashedthisfrom either slot, or a handle read off it, and used it afterwards throws instead of reaching freed memory. quitreturnsGdk.EVENT_PROPAGATErather thanGdk.EVENT_STOPwhen no root holds a mounted tree, so anonCloseRequesthandler wired to it lets GTK4 close the window. Renderingnullinto a root unmounts it rather than mounting an empty tree.- The runtime installs
SIGHUP,SIGINTandSIGTERMhandlers soonExitcallbacks run before the process goes down, then re-raises the signal. It stands aside when the process already carries a handler for that signal. - A CSS rule GTK4 cannot parse as a whole rule is dropped instead of inserted. In 1.0 such a rule disabled every later rule in the sheet, so an application that unknowingly lost part of its styling will see those rules apply again, which can change how it looks.
create-gtkx's short aliases are-pand-f. The-pmand-forcespellings 1.0 printed were silently ignored by the parser, so-pm yarnwas already scaffolding with the wrong package manager and now fails loudly.@gtkx/vitestsuppliestestTimeoutandhookTimeoutas defaults rather than writing 30 s unconditionally, so a project that set either value in its own Vitest config keeps it.
Bugfixes
- Fixed a class registered with
registerClassthat overridesvfuncDisposenever being finalized, because GTKX referenced the instance to build the receiver the override runs against, so the object stayed alive for the life of the process and its dispose slot ran again on every garbage collection. OverridingvfuncFinalizereached the same code while GObject was already finalizing the instance, which emits GLib criticals and aborts underG_DEBUG=fatal-criticals. Both slots are recognized on every subclass now and their receiver is built without taking a reference. - Fixed an asynchronous call that hands the callee a borrowed buffer freeing the marshalled arguments as soon as the call returned, long before GIO touched them, so
Gio.File.replaceContentsAsync,Gio.OutputStream.writeAsyncandwriteAllAsyncwrote freed heap memory to disk instead of the bytes they were given. Every argument such a call lends is owned until its completion callback runs. - Fixed
GValueconversion resolving its getter and setter by fundamental type, which had no case for a GType-valued property and none for a custom fundamental such asGtkExpression. ReadinglistModel.itemTypethrew, as did every other GType property across Gtk, Gio and Adw, and anexpressionconstruct property threw on every element that takes one. - Fixed the FFI reader implementing no case for a
gint64orguint64out or inout parameter, so every binding carrying one threw on any call. That covered 32 parameters on 17 functions, among themGLib.asciiStringToSigned, theutf8ToUcs4anducs4ToUtf8families,GLib.TimeZone.adjustTime,Gio.File.measureDiskUsage,Gdk.FrameClock.getRefreshInfo,WebKit.ApplicationInfo.getVersionandSoup.MessageHeaders.getContentRange. - Fixed
GLib.utf8Validateandutf8ValidateLendecoding theirendpointer as a NUL-terminated array, so validating an empty input dereferenced address zero and killed the process, and validating valid text walked off the end of the buffer and returned whatever heap bytes followed it. The pointer is decoded as a cursor bounded by the buffer the call was given. - Fixed decoding an array out parameter longer than V8's maximum array length killing the process outright, which no JavaScript
trycould catch. Reading a file larger than 128 MiB withGio.File.loadContentswas enough to hit it. Lengths are checked before the array is built, so an oversized result throws a catchable error naming the element count. - Fixed a struct-valued field such as
Graphene.Rect.originorPango.GlyphInfo.geometrydecoding to a detached copy, sorect.origin.x = 99compiled clean understrictand then silently changed nothing. A field the descriptor marks as stored inline decodes to a handle aliasing the owner's memory, and reads and writes through a handle that points at nothing are refused rather than reaching address zero. - Fixed listener tracking recording one handler id per instance, signal and function, so connecting the same function twice threw the first connection's id away and it could never be disconnected: N calls to
onfollowed by N calls tooffleft N-1 connections attached and still firing. - Fixed
Gio.Application.getDefault()answering with an application that had never registered, or one already shut down, because GLib assigns the process-wide default at construction and drops it only at finalize, which a garbage-collected wrapper reaches arbitrarily late or never. - Fixed a vtable slot whose return value is an array sized by a separate out parameter, such as
Adw.Swipeable.vfuncGetSnapPoints, being impossible to implement the way its generatedImpltype declares, because the length slot was never derived from the array the implementation returned. Lengths are folded in both directions now, by the argument positions the closure actually receives. - Fixed a render root registering itself when it was created rather than when a tree was mounted, so rendering into the same root again left the new tree untracked:
quithad nothing to bring down, the window could not be closed and the process never exited. - Fixed the reconciler resolving
@gtkx/react/package.jsonat runtime to report its version, a resolution that survived into the bundle, so an application installed anywhere outside the project tree it was built in died withMODULE_NOT_FOUNDbefore it opened a window. The version is baked in at build time. - Fixed unmounting an
AdwDialog, or any subtype, force-closing it through GTK, which emitsclosed, so React ranonClosedfor a dialog the user never dismissed and the documented pattern of closing fromonClosedcanceled any dialog opened in the same state change. - Fixed codegen writing
node_modules/.gtkxunder the project root unconditionally, so in an npm or yarn workspace where the@gtkxpackages are hoisted,@gtkx/reactand the CLI could never import the storegtkx codegenhad just written. Both stores go into thenode_modulesthe installed@gtkxpackages resolve from. - Fixed
npm installdeletingnode_modules/@gtkx/giand@gtkx/jsx, which it classifies as extraneous because neither appears in yourpackage.json, after whichgtkx devblamedgtkx.config.tsfor a library that was already declared and generated. Every command that generates or builds restores the links first, and a runninggtkx devrestores them the moment the app imports a binding. - Fixed
gtkx docsclearing its output directory with a recursive delete, sogtkx docs --out .erased the project. What is deleted is now exactly what the run's ownmanifest.jsonrecords, so hand-written pages alongside survive. Freshness also covers the base path and the resolved element props, where a run that changed--base-pathused to report the pages up to date. - Fixed a truncated or empty
.giranywhere on the GIR path producing a store that would not type-check, with errors pointing into a temporary directory codegen had already deleted and naming neither the file nor the namespace at fault. Malformed sources are rejected by path, and when the store fails its type-check for any other reason the generated sources are kept atnode_modules/.gtkx-check.failed. - Fixed
gtkx buildminifying withoutkeepNames, so a subclass relying onregisterClassderivingtypeNamefrom the class name, which is the documented default, worked undergtkx devand then aborted the built app on a mangled type name. - Fixed a runtime exception raised while a component renders terminating the whole dev session, so the fix cycle for a typo was to restart
gtkx devby hand. The runner reports the error with its stack mapped back through Vite and keeps the dev server up, and an entry that throws on load parks the session instead of exiting. - Fixed saves a few milliseconds apart, which is what format-on-save or a multi-file refactor produces, leaving the running UI permanently stale while
gtkx devreportedFast Refresh complete. The watcher waits for a write to settle, changes are applied one at a time, module loads are serialized, and a refresh only counts when what the runner evaluated is still what the module graph holds. - Fixed
gtkx devwatching only thechangeevent, so adding or deleting a source file changed nothing in the running window. A save that fails because it imports a file that does not exist yet is now retried the moment a file of that name appears, so writing the import first and the file second converges. - Fixed the dev runner attaching its watcher once, to the server it started with, so after Vite restarted itself for a
vite.config.tsor.envchange every later save was ignored in silence. Those files take a full process restart now, logged asServer config changed. - Fixed renaming an exported component, or deleting one an importer still holds, producing
Fast Refresh completewhile the window kept rendering the previous implementation. The runner compares the module's exports across the save and takes a full restart when React Refresh has nothing to patch, reported asFast Refresh dropped. - Fixed
gtkx devreportingHMR enabledand then running forever with no window when the application ID was already owned by another process, because GLib registers this process as a remote instance and registration succeeds either way. The runner asks which role the process plays and stops with a message naming the owned ID. - Fixed a project without a
gtkx.config.tsbeing told thatapplicationIdinsidegtkx.config.tswas invalid, because the loader validated the empty result before checking whether a file had been found. The config and the entry are also resolved before codegen starts, so neither failure arrives after a full store regeneration. - Fixed
userEvent.keyboarddelivering keys only to the target widget, so the standard GTK4 idiom of one key controller on the window handling Escape or an application shortcut never fired. A key sequence now travels the chain GTK4 propagates through: accelerators, capture-phase controllers from the root down, the event widget's target phase, then bubble-phase controllers back up. - Fixed
userEvent.typeanduserEvent.pasteinserting at the cursor with the widget's selection left in place, so selecting all of an entry's text and typing producedabcdefZwhere GTK4 producesZ. Both delete the selected range first, wrapped in a buffer user action so undo groups it the way GTK4 does. - Fixed the actionability gate treating a widget whose root is not a visible
Gtk.Windowas actionable, so a helper aimed at a widget React had already unmounted ran with no complaint:clickfired nothing and reported success, andtypeedited a destroyed entry. - Fixed
await render(...)resolving as soon as the tree had been laid out, roughly 50 ms before the harness window became active, so every read of GTK's platform state was a false negative:toHaveFocus()straight after render reported an unfocused widget GTK had already focused, andscreen.screenshotcould not find an active toplevel. - Fixed
waitFor, everyfindBy*query and everyuserEventhelper hanging until the test timeout in a suite that had installed Vitest's fake timers, because they polled with a baresetTimeoutand bounded themselves withDate.now(). They schedule throughnode:timersand measure withperformance.now()now. - Fixed a widget whose GType has no generated class printing an empty tag, so
screen.debug()produced lines like< name="GtkModelButton">and auserEventerror named no widget at all, which covers every menu item, tooltip window and list item widget. The tag is resolved from the GType. - Fixed the collection views flattening the entire
ListItem.childrengraph on every render, with no depth limit and no visited set, so a tree deeper than about two thousand levels, or one whose children lead back to an ancestor, killed the application with a stack overflow before a window appeared. Children are read as rows are drawn. - Fixed one malformed style disabling the rest of the stylesheet, because GTK4's CSS parser recovers from a broken rule by skipping to the end of the document: a single unbalanced parenthesis or unterminated string in one
css()call silently disabled every rule inserted after it, app-wide and for the rest of the process. - Fixed
cxjoining cached class bodies with nothing between them, so merging a class whose last declaration carried no trailing semicolon fused it with the first declaration of the next class and lost both. - Fixed
create-gtkxemptying the target directory the moment the overwrite prompt was confirmed, three prompts before anything was written, so canceling at any later question destroyed the directory's contents, reported "Operation canceled" and exited 0. - Fixed the Application ID prompt pre-filling its suggestion as editable text with the cursor at the end, so typing an id appended to the suggestion and the project was scaffolded with a concatenated, invalid id. The suggestion is the prompt's default now.
- Fixed
userEvent.clickon aGtk.Notebooktab, or on the label inside it, leaving the notebook on whatever page it was showing. The switch is applied through the public API, and a double or triple click stays on the page the first press opened. - Fixed an interrupted
gtkx codegenleaving several megabytes of staging directory insidenode_modules/.gtkxthat not even--forceremoved. Every run sweeps the staging directories no live process still owns. - Fixed a codegen fingerprint that parses as JSON but carries none of the fields codegen writes aborting
gtkx codegenandgtkx buildwith an internalTypeError, although a missing or unparseable file self-healed. Every field is checked, and a file that does not match is treated as absent. - Fixed
gtkx devand@gtkx/cli/vitest-pluginleaving behind the temporary directory they stage compiled GResource data in, one per run, forever. It is released when the server or the test run closes and on process exit. - Fixed
onExitcallbacks never running when the application is stopped withSIGINT,SIGTERMorSIGHUP, although the runtime installs a process exit hook. - Fixed
gtkx_get_widget_treeserializing a widget whose GType has no generated class with an emptytype. It reports the GType name. - Fixed
create-gtkx ""being accepted without prompting and scaffolding into the working directory, which the interactive prompt refuses. The argument and the prompt go through the same validator. - Fixed an unknown package manager, a target that is a file and an unwritable directory coming out of
create-gtkxas raw Node stack traces, with the unwritable one additionally reporting "Canceled" while exiting 0. Each is one line now, with the failing step marked on its spinner and a non-zero exit.
Performance
- Tree levels and their GTK stores are built as rows are drawn. The index behind
ListViewandColumnViewused to materialize every level of the item graph, and aGio.ListModelfor each of them, before the first row was rendered. Both are created the first time a row asks for them, and collapsing a subtree walks iteratively rather than recursing through it, so the cost of a tree tracks what is expanded rather than the size of the items handed to it.