Skip to content

Runtimes

gjsify targets four JavaScript runtimes, and one flag decides which build you get:

Terminal
gjsify build src/index.ts --app gjs # GJS
gjsify build src/index.ts --app node # Node.js, Bun and Deno
gjsify build src/index.ts --app browser # a web build of the same source

Your source stays the same. What changes is which implementation each import resolves to.

Every code sample on this site follows that rule, so read them as runtime-neutral even where they name one. The TypeScript in the widget gallery is written for GJS, and it runs unchanged on Node.js, Bun and Deno. gi://Adw is the same import on all four.

Two further targets answer those same Adw.* and Gtk.* names from a port rather than from libadwaita. Add --gi-renderer to the build, and --app browser resolves gi://Adw out of @gjsify/adwaita-web, --app nativescript out of @gjsify/adwaita-nativescript, on a phone. So the widget you learn once keeps its name in five places. What changes is the host around it.

Start from the runtime you already have:

You runBuild targetgi:// is resolved byOperating systems
GJS--app gjsGJS itself, no bridgeLinux, macOS in part
Node.js--app node@gjsify/node-giLinux, macOS, Windows
Bun--app node, the same bundle@gjsify/node-giLinux, macOS, Windows
Deno--app node, the same bundle@gjsify/node-giLinux, macOS, Windows

Node, Bun and Deno share one bundle because Node-API is their common native-addon ABI, so moving --runtime between those three changes the launcher and nothing else. Crossing between gjs and any of the three is a different build: gjsify storybook rebuilds with the other --app, and gjsify showcase resolves the other artifact.

Every template gjsify create ships covers all four. Each one builds both bundles and carries a start script per runtime: start runs the --app gjs build, start:node, start:bun and start:deno run the --app node build on the runtime they name.

GJS is GNOME’s own JavaScript runtime, SpiderMonkey plus GObject introspection. It resolves gi://Gtk?version=4.0 itself, so a GTK call from your code enters libgtk with nothing bridging in between. GNOME ships GJS, so a GNOME desktop already has the runtime installed.

The build rewrites node:* imports and Web globals to gjsify’s implementations, each backed by a GNOME library: node:fs by Gio, fetch and WebSocket by libsoup, <canvas> by Cairo, node:sqlite by libgda.

Terminal
gjsify build src/index.ts --app gjs --outfile dist/index.gjs.js
gjsify run dist/index.gjs.js

You need gjs 1.86 or newer. Linux distributions ship it. On macOS it comes from Homebrew, where only part of it is verified, and there is no GJS build for Windows. Packages lists what is implemented.

Two things are specific to this target. A .deb or .rpm built from a GJS app depends on the distribution’s own gjs (>= 1.86) rather than carrying an interpreter, which is what keeps those packages small; see Ship your app. And a showcase’s published artifact is its --app gjs bundle (gjsify.main), which is why gjsify showcase runs on GJS when a gjs binary is on PATH and follows the host runtime when there is none.

The same GObject code runs on all three through @gjsify/node-gi, a native addon that resolves gi://Gtk?version=4.0 imports and supports GObject.registerClass, signals, virtual functions with chain-up, boxed structs, Cairo drawing and the GLib main loop. One prebuilt binary and one --app node bundle serve all three.

Add the bridge to the project that needs it, then build and run:

Terminal
gjsify install @gjsify/node-gi
gjsify build src/index.ts --app node --outfile dist/index.node.mjs
gjsify run dist/index.node.mjs --runtime node # or bun, or deno

These three are the runtimes that reach macOS and Windows. @gjsify/gtk-runtime-win32-x64, @gjsify/gtk-runtime-darwin-arm64 and @gjsify/gtk-runtime-darwin-x64 carry the GTK 4 and Adwaita closure that @gjsify/node-gi loads its gi:// namespaces from, so there is no gvsbuild or Homebrew GTK to install first.

What differs between the three:

  • Node.js is the one with a declared engine floor: @gjsify/node-gi asks for Node 20 or newer. It is also the widest tested of the three. On Linux it carries the full GTK, Adwaita, windowing, widget, GtkSourceView and template checks.
  • Bun installs with bun add and writes its own module layout. Tracked at Bun’s latest release, with the GTK and Adwaita checks run on every release.
  • Deno loads the addon through the prebuild path, because it runs no postinstall build, and gjsify run --runtime deno launches with --node-modules-dir=manual so the already linked dependency is used as it stands. Tracked at Deno v2.x, with the same checks as Bun.

On macOS, all three are checked without a display, and the windowed proof runs under Node. On Windows, Node is the runtime we exercise; Bun and Deno are not verified there today.

Devtools works over this path as well: you can inspect, drive and screenshot a GTK app that is running on Node, Bun or Deno, and gjsify storybook --runtime node hosts the whole GTK storybook there.

A --app node bundle that never touches a gi:// import stays free of node-gi and runs on stock Node.js, so you are not paying for the bridge unless you use it.

The same source can be built as a web app. @gjsify/adwaita-web carries the Adwaita design system over as Web Components, and the bridge widgets have DOM-native counterparts, so a <canvas> that was a Gtk.DrawingArea on GJS is an ordinary canvas here. The showcases embedded on this site are exactly these builds.

Terminal
gjsify build src/index.ts --app browser --outfile dist/browser.js

Browser builds carry no native code, so they run wherever a browser does, and there is no bridge and no engine floor to check. Every package declares what it provides here in its gjsify.runtimes.browser slot, the same way it declares its gjs, node and nativescript slots, and that declaration is held against what the source actually imports rather than taken on trust. The bundles themselves are driven on Firefox, which shares the SpiderMonkey engine with GJS. What that checks is our implementation claims against the real browser platform, not our GJS packages inside a browser.

gjsify build --app nativescript produces bundles for the NativeScript toolchain, and @gjsify/adwaita-nativescript implements the Adwaita widget set, the storybook renderer and the devtools agent as real native Android and iOS views, not a WebView. The widget packages ship with every gjsify release. The runtime target itself is still experimental, so treat it as something to try rather than something to ship.

The same small gi:// programs are run unchanged on gjs, node, bun and deno, and every runtime’s output has to match GJS’s byte for byte. GJS is the reference because it is the one implementation we did not write: a drift on either side fails the release, whether it came from the bridge or from a GJS change. Nothing is quietly excluded. The combinations known not to match are written down, by name, with the reason.

Above that, each package’s own test bundle is built once, engine-agnostic, and run as that same file on Node, Bun and Deno. The three cannot drift apart without the release stopping.

The web and NativeScript ports are held a second way, because they are ports rather than the same code: they share one behaviour layer with each other, and both assert against the same recorded vectors, so a fix to a toast queue or a spin-button clamp lands in both at once.

gjsify build defaults --app to the target of the runtime hosting the CLI: gjs under GJS, node under Node, Bun or Deno. gjsify storybook follows the host the same way. gjsify run follows the host too, except that an --app gjs bundle is always launched on GJS, since its gi:// imports are externalised and there is no bridge to stand in. gjsify showcase uses GJS when a gjs binary is on PATH and the host runtime otherwise, because the --app gjs bundle is the artifact every showcase publishes.

Set it explicitly whenever you want something else:

Terminal
gjsify run dist/index.node.mjs --runtime bun
gjsify showcase canvas2d-fireworks --runtime deno

Or fix it per project with gjsify.app in package.json. The full flag list is in the CLI Reference; which runtime hosts the CLI itself is covered on Install & Update.

The four are not interchangeable in every direction. These are the differences that usually decide it:

  • Operating system. There is no GJS for Windows, and on macOS the GJS side is partial. Node, Bun and Deno reach all three, taking GTK and Adwaita from the system on Linux and from the platform runtime packages on macOS and Windows. Platform Support has the per-target matrix.
  • What the target machine already has. GNOME ships GJS, so a GNOME desktop needs no extra runtime. A machine already set up for npm-based tooling has one of the other three.
  • The bridge. On GJS, gi:// is the runtime’s own import path. On Node, Bun and Deno it goes through @gjsify/node-gi, which means a native addon in your dependency tree and a prebuilt binary per platform.
  • Distribution. gjsify ship turns a built app into an installable artifact with no packaging files in your repo. Linux packages come from a GJS build, and the macOS and Windows artifacts from a --app node one, since those two carry their own interpreter and GTK. Ship your app has the formats.
  • The web. --app browser drops native code entirely, which is what makes it portable and also what rules out the GNOME libraries: anything backed by GTK, Gio or libsoup needs a DOM counterpart, and @gjsify/adwaita-web is where the widget set comes from.
  • Stability. node-gi is younger than the GJS side. It is tested and released with every gjsify release, but a breaking change can still land in a minor version. No gjsify package depends on it at runtime, so it cannot destabilise a build that does not use it. See Versioning for the stability model.
  • Platform Support: Linux, macOS and Windows, per target
  • Packages: what is implemented on each runtime
  • Coverage: live dashboards of what is implemented
  • node-gi: the bridge that puts GObject on Node.js
  • napi: the other direction, native .node addons inside GJS
  • How It Works: the build pipeline behind the --app flag