v0.43.0
What this release is about
Four things an app could not do, every one of which fails silently when you get it wrong.
A UI framework can drive GTK
GTK could already describe a window instead of assembling it — that is what Blueprint is for, and it
does property bindings too. What a template cannot do is change its own shape. A .blp is
instantiated once; a list that grows, a page that swaps, a widget that appears when a checkbox is
ticked all fall back to imperative code holding a reference to every widget it might later mutate.
That is the code a UI framework exists to delete, and no UI framework could reach GTK from GJS.
@gjsify/gtk-host is the layer that lets one: fourteen operations over GTK4/Adwaita widgets —
create, insert, remove, set a property, read a parent — with no framework in them at all. Three
adapters sit on top and share it, so a fix to how a child is parented reaches all three at once
rather than being fixed once per renderer:
import { render } from '@gjsify/gtk-host/solid';
render(() => (
<gtk-box orientation="vertical">
<gtk-button label={label()} onClicked={() => setCount(count() + 1)} />
</gtk-box>
), window);Solid (/solid), Vue (/vue) and React (/react) are all real entry points, and two new packages
compile what they are written in — @gjsify/rolldown-plugin-solid for Solid's JSX and
@gjsify/rolldown-plugin-vue for single-file components.
Why the compile step is a package and not four lines of config. Point rolldown's own transformer
at a .tsx with no JSX configuration and it emits import { jsx } from "react/jsx-runtime", reports
the unresolved import as a WARNING, and exits 0. The build succeeds. The artifact throws at import,
in a bundle nobody has reason to suspect, and the message names React — a dependency the project does
not have and never asked for.
The plugins are gated on their EMITTED CODE for the same reason, and one of those gates already
caught something a running showcase could not. The Solid plugin cached its assembled Babel preset
chain rather than the compiler modules, which made every option inert after the first plugin
instance. A single build with a single instance never sees it; a process that builds two packages
emits the first one's renderer imports into the second one's bundle, and that artifact fails at
import with a module that is not there. A showcase asserts the real widget tree and is blind to it,
because a showcase is one build.
Two properties of the host are load-bearing enough to be pinned rather than described. The compiler
emits insertNode BEFORE setProp, which is why the host defers materialising a widget — a
construct-only property has to survive as a JSX attribute, and it can only do that if nothing has
been constructed yet (ADR 0027 § Decision 5). And the renderer must never emit DOM: Solid's dom
output cannot run under GJS at all, and it fails on an unresolved document at runtime, wherever the
first render happens to be.
The widget vocabulary is not hand-written. It is generated from the GIR, so a property that GTK has
is a property the types have (ADR 0028) — and ADR 0029, landed in this release, proposes moving that
vocabulary to @girs/*, where every GJS UI framework can share it instead of each generating its
own.
An app can find its own translations
gjsify ship learned to package compiled catalogues in 0.42.0, and its launcher exports
GJSIFY_LOCALE_DIR because only the launcher knows whether the payload became /usr in a .deb, a
--prefix tree, or /app in a Flatpak. Nothing read that variable. Every app was expected to write
the same four calls, and the two that tried had no translation at all.
initLocale(domain) in @gjsify/adwaita-app is the reading half:
import { initLocale } from '@gjsify/adwaita-app';
const _ = initLocale('bauplaner');
label.set_label(_('Assembly'));
status.set_label(_.plural('%d layer', '%d layers', n));Two details are the reason this is shared code and not a snippet:
textdomain()is set, not onlydgettext()used. GtkBuilder resolves every
translatable="yes"string — so everything from a.blpfile — in the DEFAULT domain, inside GTK,
where the app never gets to pass one. Binding only throughdgettexttranslates the TypeScript
strings and leaves the Blueprint ones in the source language, which reads as a half-finished
translation rather than as a missing call.- An empty
GJSIFY_LOCALE_DIRcounts as unset. The launcher exports it only when it staged
catalogues, but a wrapper that sets it unconditionally hands over''— and
bindtextdomain(domain, '')binds to the current directory, where the lookup finds nothing and
reports it exactly as "this app has no German".
Measured against a real compiled catalogue, because "the calls did not throw" is not evidence that a
lookup resolves: de_DE.utf8 returns the translation and picks the right plural form, en_US.utf8
returns the msgids (English needs no catalogue of its own), and an unset variable falls back to
/usr/share/locale.
A package can define a file type, not just claim to open one
MimeType= in a desktop entry says "I open this type". It does not say the type EXISTS. For
text/plain that never matters — the distribution defines it. For a type of your own it decides
whether the feature works, and nothing tells you when it does not: no component knows what a
.bauplan file is, so the file manager never assigns the type, MimeType= matches nothing, and a
double-click does nothing at all. No error, no log line. It is indistinguishable from the app not
being installed.
gjsify.ship.mimeTypes defines types as a shared-mime-info document staged into
share/mime/packages/<app-id>.xml, and the package refreshes the MIME cache on install — detection
reads the compiled cache under share/mime, not the packages directory, so without that refresh the
document installs and the type still does not exist.
"gjsify": {
"ship": {
"mimeTypes": [
{ "type": "application/x-bauplan", "comment": "Bauplaner project", "globs": ["*.bauplan"] }
]
}
}Declared types are folded into provides.mimetypes automatically, so the desktop entry and the
metainfo need no knowledge of the new field — they already render MimeType= (with the %f field
code) and <mediatype> from that one list. Keeping the two independent would make "defined but not
handled" reachable by omission, and that state installs cleanly and does nothing.
Four declarations are refused outright, each of which would otherwise install and never resolve: a
malformed type name (update-mime-database ignores it), a glob with no wildcard (bauplan matches
only a file called exactly that), a type with neither a glob nor a parent type (nothing can ever
match it), and a duplicate definition (which comment wins would depend on document order). See
ADR 0024 § A11.
A shared widget can have a Blueprint template
Blueprint was quietly an application-only feature. The .blp transform was installed by the
--app gjs|node|browser build factories and by nothing else, so a .blp imported from a package
built with --library reached rolldown's JavaScript parser — and a Blueprint file's first line,
using Gtk 4.0;, is valid JavaScript: a using resource declaration with no initializer. The
build died on Using declarations must have an initializer. with nothing anywhere naming Blueprint.
The consequence was not the error message, which at least stops you. It was what the error pushed
people to do instead: a widget shared between applications had to assemble itself in TypeScript, and
a caption assigned from TypeScript carries no translatable attribute, so xgettext never sees it.
LoadingStack's error page says "Something went wrong" in every consumer, in English, permanently —
and that is invisible, because an untranslatABLE string looks exactly like one nobody has translated
yet.
--library builds now run the same transform the app factories do.
LoadingStack itself stays as it was, and the reason is worth writing down because it applies to
every package in THIS repo. Converting it was tried and reverted: blueprint-compiler is not
installed on the macOS or Windows runners, and this package builds on all three — so a .blp here
makes the compiler a hard build requirement for every host rather than only for hosts that ship an
app. Worse, the repo bootstraps from the PUBLISHED CLI (ADR 0002), which is the release BEFORE this
one and therefore has no library-mode transform; during a cold bootstrap the .blp reaches the
JavaScript parser and the consumer-gate jobs fail exactly there. A capability cannot be used by the
tree that introduces it until it has shipped once. The first real consumers are applications, which
build with their own installed CLI: an app's shared widgets can carry templates today.
tests/e2e/library-blueprint/ holds the capability instead. It builds a library fixture and asserts
the emitted module carries the compiled Builder XML with translatable="yes" — and, as the
discriminator, that a MALFORMED .blp fails through blueprint-compiler rather than through the
JavaScript parser, since a JS-parser message would mean the transform never ran and the first
assertion passed for some other reason.
Two new lint rules keep the door shut, both measured against this workspace before being switched
on. gjsify/prefer-blueprint-template reports a Gtk/Adw subclass that constructs widgets and
parents them while declaring no Template — both signals are required, because constructing without
parenting is a model and parenting without constructing is a reparent. gjsify/no-literal-widget-label
reports a bare string literal in a prose property (title, label, subtitle, tooltip-text, …),
which is the half that survives even after a class has a template. Learn6502, which carries a whole
application in 24 Blueprint files, reports zero under both.
0.43.0 (2026-08-25)
Features
- adwaita-app: bind an app's translations (#1271) (205698a)
- adwaita-web: drag a carousel with the mouse (#1266) (29ef370), closes #292
- cli: ship can define file types (#1272) (1a86198)
- gtk-host: add the Solid and Vue adapters (#1267) (e6c689a)
- gtk-host: export setProp under its compiler-contract name (#1285) (9fb06ef)
- gtk-host: generate the widget table and type surfaces from the GIR (#1281) (6834302)
- gtk-host: give renderers a GTK element model (#1256) (d3889cf)
- gtk-host: render React onto the host ops (#1293) (794e190), closes #1285
- rolldown-plugin-gjsify: Blueprint in libraries (#1275) (19fbf28)
- ship: pack from a stage alone (#1268) (60deb97), closes #1057 #1263 #1269
- solid: compile Solid JSX for GJS builds (#1292) (49670de)
- vue: compile .vue SFCs for GJS builds (#1295) (7d55832)
- website: generate the documented attribute surface (#1279) (ea8b411)
Bug Fixes
- assert: make require('assert') callable (#1291) (b69a421)
- audit: print every rule's notes, and name what the stylesheet rule cannot see (#1299) (0beebc3)
- cli: apply user plugins in library builds too (#1288) (c42d7b6)
- cli: let the packument decide whether a name exists (#1307) (dbcf7c1)
- cli: refresh the stale affected bundle (#1278) (1f95a71), closes #1274 #1274
- cli: refuse a --app gjs artifact GJS cannot load (#1294) (d7ab0e8)
- fs: honour recursive in fs.watch (#1300) (f302136)
- gtk-host: close the review round on the adapters (#1274) (fd3c392), closes #15
- gtk-host: close the two findings on the generated surface (#1287) (b71e011), closes #1281
- gtk-host: drop the duplicated error constructor (#1289) (62ec143), closes #1287
- gtk-host: read the nick GIR carries, and count pairs once (#1286) (9b2a944)
- gtk-host: restore the constructed default, not the declared one (#1284) (2261691), closes #1281 #1281
- gtk-host: the generic adder is real, just unsafe (#1303) (e52a659)
- hold the claims the JSX bindings were making (#1296) (0fafc53), closes #1281 #1293
- lint: exempt type-test fixtures from the caption rule (#1282) (84a138e), closes #1275 #1281
- oxlint-plugin-gjsify: act on the review findings (#1290) (7628aa6)
- point every package's repository at itself (#1308) (343cf6d)
Documentation
- adr: 0029 — the widget vocabulary moves to @girs/* (#1306) (b830844)
- frameworks: install for every package manager (#1305) (19c0273)
- record two things that were true only by accident (#1301) (d91690d)
- release: add the UI-framework section (#1309) (42e5ec2)
- say why .blp uses the JavaScript lexer (#1273) (0df738c)
- website: document the UI framework bindings (#1298) (bee7d25)
- website: give the framework bindings their own section (#1302) (831fcc5)
Code Refactoring
Continuous Integration
- adwaita: hold every Adwaita doc sample to a compile (#1277) (04b0371)
- run build:infra on a cold tree with no node (#1259) (d823982)