@gjsify/node-gi 0.43.0 → 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -1092
- package/gi.js +101 -9
- package/gtk-runtime.d.ts +4 -1
- package/gtk-runtime.js +67 -8
- package/index.d.ts +39 -0
- package/index.js +3 -0
- package/package.json +1 -1
- package/prebuilds/darwin-arm64/node_gi.node +0 -0
- package/prebuilds/darwin-x64/node_gi.node +0 -0
- package/prebuilds/linux-arm64/node_gi.node +0 -0
- package/prebuilds/linux-x64/node_gi.node +0 -0
- package/prebuilds/win32-x64/node_gi.node +0 -0
- package/src/addon.cc +3 -0
- package/src/calls.cc +202 -66
- package/src/class.cc +240 -64
- package/src/common.h +11 -1
- package/src/marshal.cc +68 -1
package/README.md
CHANGED
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
**GObject-Introspection runtime for Node.js** — the native engine that lets
|
|
4
4
|
unchanged GJS / GObject-Introspection code run under Node.js, the inverse of
|
|
5
|
-
gjsify's Node/Web/DOM → GJS direction
|
|
6
|
-
`### Axis 5 active track`).
|
|
5
|
+
gjsify's Node/Web/DOM → GJS direction.
|
|
7
6
|
|
|
8
|
-
It loads `gi://` namespaces (GLib, GObject, Gio, …) via
|
|
9
|
-
exposes them with GJS-compatible semantics, so the same
|
|
10
|
-
on both GJS and Node via `gjsify build --app {gjs,node}`.
|
|
7
|
+
It loads `gi://` namespaces (GLib, GObject, Gio, Gtk, Adw, …) via
|
|
8
|
+
`libgirepository` and exposes them with GJS-compatible semantics, so the same
|
|
9
|
+
source builds and runs on both GJS and Node via `gjsify build --app {gjs,node}`.
|
|
11
10
|
|
|
12
11
|
> **Status: product (Tier 2 —
|
|
13
12
|
> [ADR 0005](../../../docs/adr/0005-node-gi-scope.md)).** node-gi graduated
|
|
@@ -22,57 +21,6 @@ on both GJS and Node via `gjsify build --app {gjs,node}`.
|
|
|
22
21
|
> (`--runtime node` dev flows, the `@gjsify/sqlite` consumer) and the conditional
|
|
23
22
|
> `--app node` build injection — enforced by `scripts/audit-runtimes.mjs` in CI.
|
|
24
23
|
|
|
25
|
-
> **Status: milestone 1 (headless core) — in progress.** The native engine over
|
|
26
|
-
> the modern `girepository-2.0` API now does: resolve the default repository,
|
|
27
|
-
> `require` a namespace, enumerate its infos; call namespace-level functions and
|
|
28
|
-
> **instance methods** (own + implemented-interface methods, up the parent chain)
|
|
29
|
-
> with value marshalling (numbers, booleans, strings, GObjects, enums/flags),
|
|
30
|
-
> including **OUT and INOUT parameters** surfaced per the GJS return-tuple
|
|
31
|
-
> convention (`[returnValue?, ...outArgs]` — one value bare, several as an Array),
|
|
32
|
-
> containers, struct OUT params and **caller-allocates OUT structs** — boxed
|
|
33
|
-
> (incl. the GValue auto-unbox) AND plain non-boxed C structs (the engine
|
|
34
|
-
> g_malloc0's the struct, the callee fills it in place, JS gets a field-readable
|
|
35
|
-
> handle that owns the storage — e.g. the `PangoRectangle`s of
|
|
36
|
-
> `PangoLayout.get_pixel_extents()`, the canvas2d `measureText` path). A JS
|
|
37
|
-
> `Uint8Array` (or `Buffer`/`DataView`/`ArrayBuffer`) passed where a **`GLib.Bytes`
|
|
38
|
-
> IN-arg** is expected is copied into a fresh GBytes and released per transfer
|
|
39
|
-
> after the call, exactly as GJS (`GdkPixbuf.Pixbuf.new_from_bytes(pixels, …)`);
|
|
40
|
-
> construct GObjects and read/write properties (GValue round-trip); connect /
|
|
41
|
-
> emit / disconnect signals (incl. detailed names like `notify::prop`); and
|
|
42
|
-
> **register GObject subclasses** (subtype + construct-by-type, inheriting the
|
|
43
|
-
> parent's properties/methods, plus **custom properties + signals** declared on
|
|
44
|
-
> the subclass). Ownership rides
|
|
45
|
-
> N-API finalizers (no V8-GC reentrancy). On top of the engine, an **L1
|
|
46
|
-
> GJS-compatibility layer** (`@gjsify/node-gi/gi`, `requireGi`) surfaces a
|
|
47
|
-
> GJS-shaped namespace: `new Gio.SimpleAction({ name })`, `action.name` property
|
|
48
|
-
> access, `action.get_name()` methods, `.connect()/.emit()/.disconnect()`, and
|
|
49
|
-
> enums / flags / constants (`Gio.BusType.SESSION`, `GLib.PRIORITY_DEFAULT`);
|
|
50
|
-
> constructor/static methods (`Gio.File.new_for_path(...)`); and both snake_case
|
|
51
|
-
> and camelCase accessors. The L1 layer also surfaces a **GJS-shaped
|
|
52
|
-
> `GObject.registerClass(meta, class)` decorator** (with `GObject.ParamSpec` /
|
|
53
|
-
> `ParamFlags` / `SignalFlags`): a JS `class extends GObject.Object { … }` with
|
|
54
|
-
> `Properties` / `Signals` / `vfunc_*` methods becomes a constructor whose
|
|
55
|
-
> instances carry both the user methods and the GObject property/signal surface.
|
|
56
|
-
> A **libuv↔GLib mainloop bridge** (`startMainLoop`,
|
|
57
|
-
> auto-attached by `requireGi`) nests Node's libuv loop inside the GLib loop, so a
|
|
58
|
-
> blocking `GLib.MainLoop.run()` keeps Node's timers/I/O alive — including the
|
|
59
|
-
> **boxed/struct slice** that needs (`GLib.MainLoop.new(...)` → a boxed handle →
|
|
60
|
-
> `.run()`/`.quit()`). The same call arms the **uv-driven auto-pump** for the
|
|
61
|
-
> NON-blocking case: pending GLib sources (Gio async completions, GLib
|
|
62
|
-
> timeouts/idles, DBus) dispatch from Node's own event loop, so a plain
|
|
63
|
-
> `node bundle.mjs` that `await`s a Gio async op needs no explicit mainloop —
|
|
64
|
-
> matching GJS, where the GLib loop is the process loop. **JS functions marshal as GI callbacks** via an ffi
|
|
65
|
-
> closure (`GLib.timeout_add`/`idle_add` fire from the loop, the boolean return
|
|
66
|
-
> drives source continuation; the hidden user_data/destroy slots are auto-filled).
|
|
67
|
-
> register GObject subclasses (subtype + construct-by-type, inheriting the
|
|
68
|
-
> parent's properties/methods, plus **custom properties + signals**, plus
|
|
69
|
-
> **vfunc overrides** — a JS function overriding a parent GObject vfunc, hooked
|
|
70
|
-
> into the new type's class vtable). The gjsify `--app node` bundler integration
|
|
71
|
-
> already rewrites `gi://` onto the L1 layer. vfunc **chain-up** to the parent
|
|
72
|
-
> implementation (with the toggle-ref GC bridge) and general struct field access
|
|
73
|
-
> land in subsequent drops — for now a vfunc override fully replaces the inherited
|
|
74
|
-
> implementation.
|
|
75
|
-
|
|
76
24
|
## Provenance
|
|
77
25
|
|
|
78
26
|
Derived from [node-gtk](https://github.com/romgrk/node-gtk) (romgrk and
|
|
@@ -85,7 +33,8 @@ vendored as-is — gjsify ships its own dual (GJS + Node) example/test infra.
|
|
|
85
33
|
|
|
86
34
|
## Requirements
|
|
87
35
|
|
|
88
|
-
- Node.js ≥ 20
|
|
36
|
+
- **Node.js ≥ 20, Bun ≥ 1.3, or Deno ≥ 2** — the engine is a Node-API addon, so
|
|
37
|
+
ONE binary serves all three (see [Runtimes](#runtimes-node--bun--deno)).
|
|
89
38
|
- **No C++ toolchain on the four prebuilt targets.** The published tarball ships
|
|
90
39
|
`prebuilds/<platform>-<arch>/node_gi.node` for `linux-x64`, `linux-arm64`,
|
|
91
40
|
`darwin-arm64` and `win32-x64` (`package.json#gjsify.platforms`), and the
|
|
@@ -98,313 +47,55 @@ vendored as-is — gjsify ships its own dual (GJS + Node) example/test infra.
|
|
|
98
47
|
(Fedora: `glib2-devel gobject-introspection-devel cairo-devel gcc-c++`;
|
|
99
48
|
Debian/Ubuntu: `libglib2.0-dev libgirepository-2.0-dev libcairo2-dev g++`)
|
|
100
49
|
- At runtime, the target libraries' typelibs must be installed (same as `gi://`
|
|
101
|
-
under GJS)
|
|
102
|
-
|
|
103
|
-
works with no Homebrew/gvsbuild GTK
|
|
104
|
-
load and prepends its typelib dir to the
|
|
105
|
-
(`gtk-runtime.js`) — nothing in that loader keys on a
|
|
106
|
-
new bundle is found by the mechanism that found the
|
|
107
|
-
|
|
108
|
-
[
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
All three declare `os`/`cpu`, so npm/yarn/pnpm skip them off-platform. Making them
|
|
122
|
-
`optionalDependencies` of this package (which would remove the manual step
|
|
123
|
-
without any consumer-side platform branching) is pending two decisions outside
|
|
124
|
-
this package — the ADR-0003 dependency-direction rule between node-gi's tier
|
|
125
|
-
and the bundles', and `os`/`cpu` filtering in `gjsify install`'s native
|
|
126
|
-
backend, which currently places foreign-platform optional deps.
|
|
127
|
-
- **Node.js ≥ 20, Bun ≥ 1.3, or Deno ≥ 2** — the addon is Node-API, so one binary
|
|
128
|
-
runs on all three (see [Runtimes](#runtimes-node--bun--deno)).
|
|
129
|
-
|
|
130
|
-
## Runtimes (Node / Bun / Deno)
|
|
131
|
-
|
|
132
|
-
The engine is a **Node-API** addon, so the same binary loads and runs on **Node,
|
|
133
|
-
Bun and Deno** — Node-API is their common native ABI (no separate bindings, no Rust
|
|
134
|
-
rewrite). `index.js` detects the runtime and prefers a shipped
|
|
135
|
-
`prebuilds/<platform>-<arch>/node_gi.node` over a local `build/` (Deno runs no
|
|
136
|
-
postinstall build, so a prebuild is its only install path — stage one with
|
|
137
|
-
`npm run build:prebuild`).
|
|
138
|
-
|
|
139
|
-
Two libuv-coupled subsystems are portable across all three:
|
|
140
|
-
|
|
141
|
-
- **GC bridge** — the toggle-ref teardown drain uses a `napi_threadsafe_function`
|
|
142
|
-
(core Node-API), not node-gtk's raw `uv_async_t` (Deno exports no libuv symbols;
|
|
143
|
-
Bun panics on `uv_async_init`).
|
|
144
|
-
- **Main loop** — Node runs BOTH directions: the uv-nesting bridge co-pumps
|
|
145
|
-
Node's own event loop during a blocking GLib loop, and the **uv-driven
|
|
146
|
-
auto-pump** dispatches pending GLib sources from Node's loop when NO blocking
|
|
147
|
-
GLib loop runs (async Gio + `await` just works, no explicit loop). Bun/Deno
|
|
148
|
-
co-pump the non-blocking case by hand, iterating the default GLib context from
|
|
149
|
-
a runtime timer (`startMainContextPump` from `@gjsify/node-gi/gi`), so GIO
|
|
150
|
-
async callbacks / GLib timeouts / DBus fire while the runtime's own loop stays
|
|
151
|
-
live.
|
|
152
|
-
|
|
153
|
-
| Capability | Node | Bun | Deno |
|
|
154
|
-
|---|:--:|:--:|:--:|
|
|
155
|
-
| introspection, marshalling, enums, variants | ✅ | ✅ | ✅ |
|
|
156
|
-
| GObject create / properties / signals | ✅ | ✅ | ✅ |
|
|
157
|
-
| `registerClass` / subclass / vfunc chain-up | ✅ | ✅ | ✅ |
|
|
158
|
-
| toggle-ref GC + cross-thread teardown | ✅ | ✅ | ✅ |
|
|
159
|
-
| GLib async with NO mainloop (timeouts, GIO async, DBus) | ✅ auto | via `startMainContextPump` | via `startMainContextPump` |
|
|
160
|
-
| blocking `GLib.MainLoop.run()` / `Gio.Application.runAsync()` | ✅ | ✅ | ✅ |
|
|
161
|
-
| Promise continuations drain **during** a blocking GLib loop (async DBus replies, `await` chains) | ✅ | ✅ | ✅ |
|
|
162
|
-
| Node timers/I/O alive **during** a blocking GLib loop (uv co-pump) | ✅ | — | — |
|
|
163
|
-
|
|
164
|
-
Bun reaches full parity with Node on the core surface; Deno passes the same
|
|
165
|
-
conformance subset since 2.9 (the marshalling/async N-API quirks that excluded
|
|
166
|
-
`arrays`/`async-error` on Deno 2.1 are fixed upstream) — the Node-only co-pump
|
|
167
|
-
cases are the remaining, by-design gap. The authoritative full suite runs on Node.
|
|
168
|
-
|
|
169
|
-
**Architectures.** CI runs the Bun/Deno legs on x86-64 only, so `linux-arm64` was
|
|
170
|
-
measured by hand on a OnePlus 6T / postmarketOS (2026-08-03) against the PUBLISHED
|
|
171
|
-
0.27.0 `prebuilds/linux-arm64/node_gi.node`: **Bun 1.3.14 → 38/38** cross-runtime
|
|
172
|
-
files, **Deno 2.9.4 → 35/38 plus the three known non-gating Deno teardown exits,
|
|
173
|
-
0 failed**. Two libc notes from that run:
|
|
174
|
-
|
|
175
|
-
- the prebuild declares glibc, yet it **loads and runs on a pure-musl host** (Alpine
|
|
176
|
-
/ postmarketOS, no glibc, no `gcompat`) under Bun's musl build: musl's dynamic
|
|
177
|
-
loader maps the `libc.so.6` / `libm.so.6` `DT_NEEDED` entries onto ITSELF via its
|
|
178
|
-
reserved-name list, and every other dependency (`libglib-2.0`,
|
|
179
|
-
`libgirepository-2.0`, `libcairo`, `libstdc++`, `libgcc_s`) exists musl-built.
|
|
180
|
-
**Do not read a successful load as "every symbol resolved".** Measured with a real
|
|
181
|
-
artifact (`@gjsify/sab-native`'s pre-#955 prebuild) on aarch64 musl and in
|
|
182
|
-
`alpine:3.24` x86-64: `dlopen` behaves oppositely by mode — `RTLD_NOW` fails at
|
|
183
|
-
load, `RTLD_LAZY` **loads** and the process dies at the first call through the
|
|
184
|
-
deferred PLT slot. GI opens typelib backers with `G_MODULE_BIND_LAZY`, so on musl a
|
|
185
|
-
glibc-only symbol really does hide from a load. Both arches behave identically.
|
|
186
|
-
A weak undefined symbol hides the same way. So this binary is clear by audit, not
|
|
187
|
-
by its load: 5 of its 579 undefined symbols are weak, and the 3 unresolvable on
|
|
188
|
-
musl (`_ITM_{,de}registerTMCloneTable`, `__gmon_start__`) are the NULL-guarded gcc
|
|
189
|
-
boilerplate every DSO carries. What defeats the hiding is `dlopen(RTLD_NOW)`, or —
|
|
190
|
-
with no musl machine at all — a glibc-floor `SHT_GNU_verneed` audit.
|
|
191
|
-
- **Deno publishes no musl build** (`aarch64-unknown-linux-gnu` only), so on such a
|
|
192
|
-
host Deno needs a glibc environment (here the `org.gnome.Platform//49` flatpak
|
|
193
|
-
runtime: glibc 2.42, gjs 1.86.0). With `node_modules/` already materialised, both
|
|
194
|
-
`--node-modules-dir=manual` and Deno's default worked.
|
|
195
|
-
|
|
196
|
-
On **Linux** the typelib's bare library leaf needs no help: `Gtk`/`Gdk`/`Adw`/`Pango`/
|
|
197
|
-
`Graphene` all resolve and `Gtk.DrawingArea` subclasses correctly with no
|
|
198
|
-
`GI_TYPELIB_PATH` and no `LD_LIBRARY_PATH`, both on the musl host and inside the
|
|
199
|
-
flatpak runtime, because the system loader's default search path already covers the
|
|
200
|
-
distro's own libraries. That is worth stating explicitly because the same step FAILS
|
|
201
|
-
on macOS (`Failed to load shared library 'libgtk-4.1.dylib' referenced by the
|
|
202
|
-
typelib` → `Gtk.DrawingArea is not a subclassable GObject type`): dyld has no
|
|
203
|
-
`ld.so.conf` equivalent, and a plain `node`/`bun`/`deno` binary carries no rpath into
|
|
204
|
-
Homebrew's prefix, where `gjs` itself does. A loader-path failure of that shape is
|
|
205
|
-
never a node-gi or runtime defect — see `gtk-runtime.js` and the CLI's
|
|
206
|
-
`detectNativePackages()` / `buildNativeEnv()` for how the env normally gets wired.
|
|
207
|
-
|
|
208
|
-
Promise draining during a blocking loop is cross-runtime because the engine runs
|
|
209
|
-
a **microtask checkpoint at every outermost loop-dispatched GLib→JS callback
|
|
210
|
-
boundary**: Node's `napi_make_callback` performs it natively; Bun's and Deno's
|
|
211
|
-
do not, so on those runtimes the engine invokes the runtime's own drain
|
|
212
|
-
primitive (`bun:jsc` `drainMicrotasks` / Deno `core.runMicrotasks`), registered
|
|
213
|
-
at addon load (`src/loop.cc` `NodeGiMaybeDrainMicrotasks`). Without it, an
|
|
214
|
-
**async** (Promise-returning) DBus method handler exported via
|
|
215
|
-
`Gio.DBusExportedObject.wrapJSObject` never sent its reply on Bun/Deno while a
|
|
216
|
-
blocking `run()` owned the thread — the client timed out — while sync handlers
|
|
217
|
-
worked (regression: `test/dbus-async.test.mjs`). Runtime *timers/I/O* during a
|
|
218
|
-
blocking loop remain Node-only (the uv co-pump).
|
|
219
|
-
|
|
220
|
-
## Build & test
|
|
221
|
-
|
|
222
|
-
```bash
|
|
223
|
-
npm install # prebuild if one is staged, else node-gyp (scripts/install.mjs)
|
|
224
|
-
npm test # node --test (full suite, Node — authoritative)
|
|
225
|
-
npm run test:gc # node --test --expose-gc (toggle-ref GC-stress leg)
|
|
226
|
-
npm run test:bun # conformance subset on Bun (needs `bun`)
|
|
227
|
-
npm run test:deno # conformance subset on Deno (needs `deno`)
|
|
228
|
-
npm run build:prebuild # node-gyp rebuild + stage prebuilds/<platform>-<arch>/
|
|
229
|
-
# or rebuild explicitly:
|
|
230
|
-
npm run rebuild
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
The load order prefers a staged `prebuilds/<platform>-<arch>/node_gi.node` over
|
|
234
|
-
`build/Release` (the consumer/Deno install path). Local verification always runs
|
|
235
|
-
the **just-built** addon instead: the Node test scripts pin `NODE_GI_NATIVE=build`
|
|
236
|
-
and the bun/deno runner defaults to it — without that, a stale staged prebuild
|
|
237
|
-
silently shadows your build. CI's cross-runtime job sets `NODE_GI_NATIVE=prebuild`
|
|
238
|
-
to keep validating the prebuild load path with a freshly staged binary.
|
|
239
|
-
`NODE_GI_NATIVE` accepts `build`, `prebuild`, or an explicit path to a
|
|
240
|
-
`node_gi.node`. That candidate order lives in `native-paths.js` and is shared
|
|
241
|
-
with the `install` script, so the binary the guard decides not to rebuild is
|
|
242
|
-
by construction the binary the loader looks for.
|
|
243
|
-
|
|
244
|
-
In a checkout `prebuilds/` is gitignored, so `npm install` here always runs the
|
|
245
|
-
source build — as do the `npm install --foreground-scripts` steps in
|
|
246
|
-
`node-gi.yml` and the `node-gi-prebuild-*` legs in `release.yml`, which need
|
|
247
|
-
`build/Release/node_gi.node` for `scripts/stage-prebuild.mjs`. Set
|
|
248
|
-
`NODE_GI_BUILD_FROM_SOURCE=1` to force it regardless of a staged prebuild.
|
|
249
|
-
|
|
250
|
-
Two debug-only env vars instrument the toggle-ref / teardown machinery (both
|
|
251
|
-
parsed once at first use, zero cost when unset — never set them in production):
|
|
252
|
-
|
|
253
|
-
- `NODE_GI_TOGGLE_DEBUG=1` — stderr tracing of the GC bridge: owner-env claim,
|
|
254
|
-
drain-TSFN create/release, shutdown-flag flips, teardown enqueue/drop, drain
|
|
255
|
-
runs/skips (with JS-availability), and the C→JS trampoline skips at env
|
|
256
|
-
teardown; each line carries the emitting thread.
|
|
257
|
-
- `NODE_GI_TOGGLE_TEARDOWN_DELAY_MS=<n>` — test-only latency seam (clamped to
|
|
258
|
-
10s): the drain defers queued idle teardowns younger than `n` ms (re-waking
|
|
259
|
-
itself), which deterministically parks teardowns — with a pending drain wake —
|
|
260
|
-
across the event loop's exit. That is the regression vehicle for the
|
|
261
|
-
env-cleanup drain race (`test/gc-cross-thread.test.mjs`, "teardown drain
|
|
262
|
-
during env cleanup never aborts").
|
|
263
|
-
|
|
264
|
-
## Conformance (golden-diff)
|
|
265
|
-
|
|
266
|
-
The exactness oracle for GJS parity: small self-contained `gi://` programs
|
|
267
|
-
under `conformance/programs/*.conf.mjs` run UNCHANGED on all four runtimes —
|
|
268
|
-
gjs natively (`gjs -m`, ambient `print`), node/bun/deno via a lightweight
|
|
269
|
-
generated runtime twin (the `globals.js` shim + `requireGi`, no bundler) — and
|
|
270
|
-
every runtime's **stdout must be byte-identical to the committed golden**
|
|
271
|
-
(`conformance/golden/<name>.txt`). **gjs is the reference**: the goldens ARE
|
|
272
|
-
the gjs output, and a gjs↔golden drift fails loudly (either GJS changed or the
|
|
273
|
-
golden is stale — never paper over it).
|
|
50
|
+
under GJS). A distro GTK on Linux needs no env wiring at all; on **macOS (both
|
|
51
|
+
arm64 and Intel x64) and Windows x64** a batteries-included, relocated GTK/GI
|
|
52
|
+
runtime bundle ships instead, so `gi://` works with no Homebrew/gvsbuild GTK.
|
|
53
|
+
node-gi auto-detects a bundle at load and prepends its typelib dir to the
|
|
54
|
+
GIRepository search path (`gtk-runtime.js`) — nothing in that loader keys on a
|
|
55
|
+
specific target, so each new bundle is found by the mechanism that found the
|
|
56
|
+
first. The loader contract and the per-platform measurements are
|
|
57
|
+
[docs/node-gi-platform-notes.md](../../../docs/node-gi-platform-notes.md).
|
|
58
|
+
- An application that carries **its own** GI library — a typelib plus the shared
|
|
59
|
+
library behind it, staged beside the app rather than installed — names those
|
|
60
|
+
directories in `GJSIFY_GI_LIBRARY_PATH` (`:`-separated; `;` on Windows;
|
|
61
|
+
absolute paths only). node-gi reads it in JS and hands the directories to
|
|
62
|
+
`gi_repository_prepend_library_path()`, so GI can open the bare leaf its own
|
|
63
|
+
typelib records. This is the only route that survives on macOS: dyld strips
|
|
64
|
+
every `DYLD_*` from a restricted process, so a signed `.app` cannot use one.
|
|
65
|
+
Entries take precedence over the GTK bundle / system GI libdirs.
|
|
66
|
+
|
|
67
|
+
## Install
|
|
274
68
|
|
|
275
69
|
```bash
|
|
276
|
-
npm
|
|
277
|
-
node scripts/conformance.mjs --runtimes=gjs,node # runtime subset (gjs/node never auto-skip)
|
|
278
|
-
node scripts/conformance.mjs --filter=variant # program subset
|
|
279
|
-
node scripts/conformance.mjs --update-golden # regenerate goldens from gjs
|
|
70
|
+
npm install @gjsify/node-gi
|
|
280
71
|
```
|
|
281
72
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
The ledger contract (`conformance/ledger.json`) is strict: every known-failing
|
|
290
|
-
program×runtime combo is a **committed entry**
|
|
291
|
-
`{ "program", "runtime", "reason", "issue"? }` — a failing combo *not* in the
|
|
292
|
-
ledger fails the run, and a passing combo still *in* the ledger fails as a
|
|
293
|
-
stale entry (remove it). Exit 0 means zero unexpected results; there are no
|
|
294
|
-
silent exclusions (bun/deno merely report `skipped` when not installed — gjs
|
|
295
|
-
and node never skip).
|
|
296
|
-
|
|
297
|
-
### Tier B — GJS installed-tests port
|
|
298
|
-
|
|
299
|
-
The breadth oracle: GJS's own installed-tests
|
|
300
|
-
(`refs/gjs/installed-tests/js/testGIMarshalling.js`) encode GJS's marshalling
|
|
301
|
-
behavior against the purpose-built `GIMarshallingTests-1.0` typelib.
|
|
302
|
-
`gimarshalling/testGIMarshalling.port.mjs` is a near-verbatim port of that
|
|
303
|
-
file to `node:test` via a minimal jasmine shim
|
|
304
|
-
(`gimarshalling/jasmine-shim.mjs`), mapping the WHOLE upstream surface:
|
|
305
|
-
already-green sections run live, everything else is a `describeSkip` stub
|
|
306
|
-
naming the upstream section. Assertions are never weakened.
|
|
73
|
+
**On macOS and Windows the GTK runtime bundle is a MANUAL install today.**
|
|
74
|
+
[`@gjsify/gtk-runtime-darwin-arm64`](../gtk-runtime-darwin-arm64),
|
|
75
|
+
[`@gjsify/gtk-runtime-darwin-x64`](../gtk-runtime-darwin-x64) and
|
|
76
|
+
[`@gjsify/gtk-runtime-win32-x64`](../gtk-runtime-win32-x64) are published, and
|
|
77
|
+
node-gi finds whichever one is present in the tree — but **no package declares
|
|
78
|
+
them as a dependency**, so nothing installs them for you. Install the one for
|
|
79
|
+
your platform alongside node-gi:
|
|
307
80
|
|
|
308
81
|
```bash
|
|
309
|
-
npm
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
`scripts/build-gi-test-typelibs.mjs` builds the test typelibs reproducibly
|
|
313
|
-
from GNOME's gobject-introspection-tests project at the **pinned revision**
|
|
314
|
-
GJS itself tests against (`PINNED_REV`, copied from
|
|
315
|
-
`refs/gjs/subprojects/gobject-introspection-tests.wrap`) into the gitignored
|
|
316
|
-
`.gi-tests/` (meson + ninja required; cairo disabled; Regress builds too).
|
|
317
|
-
The launcher (`scripts/gimarshalling.mjs`) sets `GI_TYPELIB_PATH` /
|
|
318
|
-
`LD_LIBRARY_PATH` before spawning `node --test` — dlopen cannot pick up late
|
|
319
|
-
env changes — and pins `NODE_GI_NATIVE=build`. The port files are named
|
|
320
|
-
`*.port.mjs` so the default `npm test` glob never picks them up.
|
|
321
|
-
|
|
322
|
-
**Skip contract (strict, mirrors the tier-A ledger):** every skipped
|
|
323
|
-
spec/suite carries a reason — a phase-2.x roadmap item from the taxonomy at
|
|
324
|
-
the top of the port file (e.g. `phase 2.1 BigInt-64-bit`), an upstream issue
|
|
325
|
-
URL, or a `FIDELITY-BUG: …` note — and the reason is reported in the
|
|
326
|
-
`node:test` output (`# SKIP <reason>`). A bare skip throws (`pending()`,
|
|
327
|
-
`itSkip`, `describeSkip` all require the reason; `xit` must chain
|
|
328
|
-
`.pend(reason)`). Later marshalling PRs un-skip their sections — this port is
|
|
329
|
-
phase 2's acceptance gate.
|
|
330
|
-
|
|
331
|
-
## Usage (milestone 1)
|
|
332
|
-
|
|
333
|
-
```js
|
|
334
|
-
import {
|
|
335
|
-
requireNamespace, listInfoNames, callFunction,
|
|
336
|
-
newObject, getProperty, setProperty, callMethod,
|
|
337
|
-
connectSignal, emitSignal,
|
|
338
|
-
} from '@gjsify/node-gi';
|
|
339
|
-
|
|
340
|
-
requireNamespace('GLib', '2.0');
|
|
341
|
-
console.log(listInfoNames('GLib').includes('MainLoop')); // true
|
|
342
|
-
console.log(callFunction('GLib', 'get_host_name')); // namespace function
|
|
343
|
-
|
|
344
|
-
requireNamespace('Gio', '2.0');
|
|
345
|
-
const action = newObject('Gio', 'SimpleAction', { name: 'greet', enabled: true });
|
|
346
|
-
console.log(getProperty(action, 'name')); // 'greet' (GValue round-trip)
|
|
347
|
-
console.log(callMethod(action, 'get_name')); // 'greet' (interface method)
|
|
348
|
-
callMethod(action, 'set_enabled', [false]); // method with an IN argument
|
|
349
|
-
|
|
350
|
-
const c = newObject('Gio', 'Cancellable', {});
|
|
351
|
-
connectSignal(c, 'cancelled', () => console.log('cancelled'));
|
|
352
|
-
emitSignal(c, 'cancelled');
|
|
353
|
-
```
|
|
354
|
-
|
|
355
|
-
Register a GObject subclass and construct it (inherited properties + methods):
|
|
356
|
-
|
|
357
|
-
```js
|
|
358
|
-
import { registerClass, constructType, callMethod } from '@gjsify/node-gi';
|
|
359
|
-
|
|
360
|
-
const MyAction = registerClass('MyAction', 'Gio', 'SimpleAction');
|
|
361
|
-
const a = constructType(MyAction, { name: 'greet', enabled: true });
|
|
362
|
-
console.log(callMethod(a, 'get_name')); // 'greet' (inherited GAction method)
|
|
82
|
+
npm install @gjsify/gtk-runtime-win32-x64 # Windows x64
|
|
83
|
+
npm install @gjsify/gtk-runtime-darwin-arm64 # macOS Apple silicon
|
|
84
|
+
npm install @gjsify/gtk-runtime-darwin-x64 # macOS Intel
|
|
363
85
|
```
|
|
364
86
|
|
|
365
|
-
|
|
87
|
+
All three declare `os`/`cpu`, so npm/yarn/pnpm skip them off-platform. Making them
|
|
88
|
+
`optionalDependencies` of this package (which would remove the manual step
|
|
89
|
+
without any consumer-side platform branching) is pending two decisions outside
|
|
90
|
+
this package — the ADR-0003 dependency-direction rule between node-gi's tier
|
|
91
|
+
and the bundles', and `os`/`cpu` filtering in `gjsify install`'s native
|
|
92
|
+
backend, which currently places foreign-platform optional deps.
|
|
366
93
|
|
|
367
|
-
|
|
368
|
-
import { registerClass, constructType, getProperty, setProperty,
|
|
369
|
-
connectSignal, emitSignal } from '@gjsify/node-gi';
|
|
94
|
+
## Usage
|
|
370
95
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
const c = constructType(Counter, { count: 1 });
|
|
377
|
-
connectSignal(c, 'notify::count', (pspec) => console.log('changed:', pspec.name));
|
|
378
|
-
connectSignal(c, 'changed', (n) => console.log('count is now', n));
|
|
379
|
-
setProperty(c, 'count', 5); // fires notify::count
|
|
380
|
-
emitSignal(c, 'changed', [5]);
|
|
381
|
-
console.log(getProperty(c, 'count')); // 5
|
|
382
|
-
```
|
|
383
|
-
|
|
384
|
-
Override a parent GObject vfunc with a JS function (the override runs as a method
|
|
385
|
-
on the instance — `this` is the GObject handle). Chain-up to the parent vfunc
|
|
386
|
-
lands in a later drop, so an override fully replaces the inherited implementation:
|
|
387
|
-
|
|
388
|
-
```js
|
|
389
|
-
import { registerClass, constructType, getProperty } from '@gjsify/node-gi';
|
|
390
|
-
|
|
391
|
-
const Greeter = registerClass('Greeter', 'Gio', 'SimpleAction', {
|
|
392
|
-
vfuncs: {
|
|
393
|
-
// GObject's `constructed` vfunc — runs once, after construct properties are
|
|
394
|
-
// set. `name` is a CONSTRUCT_ONLY property already available on `this`.
|
|
395
|
-
constructed() {
|
|
396
|
-
console.log('constructed:', getProperty(this, 'name'));
|
|
397
|
-
},
|
|
398
|
-
},
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
constructType(Greeter, { name: 'greet' }); // logs "constructed: greet"
|
|
402
|
-
```
|
|
403
|
-
|
|
404
|
-
### L1 — GJS-shaped surface (`@gjsify/node-gi/gi`)
|
|
405
|
-
|
|
406
|
-
The ergonomic layer the gjsify `--app node` build rewrites `gi://` imports onto.
|
|
407
|
-
This is the same code you would write under GJS:
|
|
96
|
+
`requireGi` hands you a GJS-shaped namespace: the same code you would write
|
|
97
|
+
under GJS, property accessors, methods, signals, enums and constructors
|
|
98
|
+
included.
|
|
408
99
|
|
|
409
100
|
```js
|
|
410
101
|
import { requireGi } from '@gjsify/node-gi/gi';
|
|
@@ -414,757 +105,95 @@ console.log(GLib.get_host_name());
|
|
|
414
105
|
|
|
415
106
|
const Gio = requireGi('Gio', '2.0');
|
|
416
107
|
const action = new Gio.SimpleAction({ name: 'greet', enabled: true });
|
|
417
|
-
console.log(action.name);
|
|
418
|
-
console.log(action.get_name());
|
|
419
|
-
action.enabled = false;
|
|
420
|
-
|
|
421
|
-
const c = new Gio.Cancellable();
|
|
422
|
-
c.connect('cancelled', () => console.log('cancelled'));
|
|
423
|
-
c.cancel(); // fires the signal
|
|
108
|
+
console.log(action.name); // 'greet' — property accessor
|
|
109
|
+
console.log(action.get_name()); // 'greet' — introspected method
|
|
110
|
+
action.enabled = false; // property set → set_property
|
|
424
111
|
|
|
425
|
-
|
|
426
|
-
console.log(
|
|
427
|
-
|
|
428
|
-
console.log(Gio.ApplicationFlags.HANDLES_OPEN); // 4
|
|
112
|
+
const cancellable = new Gio.Cancellable();
|
|
113
|
+
cancellable.connect('cancelled', () => console.log('cancelled'));
|
|
114
|
+
cancellable.cancel(); // fires the signal
|
|
429
115
|
|
|
430
|
-
//
|
|
116
|
+
console.log(Gio.BusType.SESSION); // 2 — enums, flags and constants
|
|
431
117
|
const file = Gio.File.new_for_path('/usr/bin/gjs');
|
|
432
|
-
console.log(file.get_path());
|
|
433
|
-
console.log(file.getBasename()); // 'gjs' (camelCase alias)
|
|
434
|
-
|
|
435
|
-
// mainloop: a blocking GLib loop, with Node's libuv kept alive underneath
|
|
436
|
-
const loop = GLib.MainLoop.new(null, false);
|
|
437
|
-
setTimeout(() => loop.quit(), 100); // a libuv timer that fires during run()
|
|
438
|
-
loop.run(); // blocks like under GJS; returns on quit()
|
|
439
|
-
|
|
440
|
-
// GI callbacks: a JS function passed where a GI callback is expected. The
|
|
441
|
-
// GLib source fires from the loop; returning false (G_SOURCE_REMOVE) stops it.
|
|
442
|
-
const ticker = GLib.MainLoop.new(null, false);
|
|
443
|
-
let n = 0;
|
|
444
|
-
GLib.timeout_add(GLib.PRIORITY_DEFAULT, 50, () => {
|
|
445
|
-
if (++n >= 3) { ticker.quit(); return false; }
|
|
446
|
-
return true;
|
|
447
|
-
});
|
|
448
|
-
ticker.run();
|
|
118
|
+
console.log(file.get_path()); // '/usr/bin/gjs'
|
|
449
119
|
```
|
|
450
120
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
A blocking loop is **optional**: the same call arms the uv-driven auto-pump, so
|
|
456
|
-
pending GLib sources dispatch from Node's own event loop too — async Gio work
|
|
457
|
-
awaited at top level behaves exactly as under `gjs -m`:
|
|
458
|
-
|
|
459
|
-
```js
|
|
460
|
-
import { requireGi } from '@gjsify/node-gi/gi';
|
|
461
|
-
|
|
462
|
-
const Gio = requireGi('Gio', '2.0');
|
|
463
|
-
const file = Gio.File.new_for_path('/etc/hostname');
|
|
464
|
-
// No GLib.MainLoop.run() anywhere: the GTask completion dispatches from Node's
|
|
465
|
-
// loop, the in-flight op keeps the process alive, then Node exits normally.
|
|
466
|
-
const [ok, contents] = await new Promise((resolve, reject) => {
|
|
467
|
-
file.load_contents_async(null, (_source, res) => {
|
|
468
|
-
try { resolve(file.load_contents_finish(res)); } catch (e) { reject(e); }
|
|
469
|
-
});
|
|
470
|
-
});
|
|
471
|
-
```
|
|
472
|
-
|
|
473
|
-
Process-lifetime semantics follow Node conventions: an in-flight async op
|
|
474
|
-
(a pending `GAsyncReadyCallback`) and an armed GLib timeout keep the process
|
|
475
|
-
alive — like Node's own pending I/O and timers — so a REPEATING GLib timeout
|
|
476
|
-
keeps the process running like `setInterval` (under `gjs -m` the process would
|
|
477
|
-
instead exit once the module settles; remove the source to release the process).
|
|
478
|
-
A *passive* fd source with no pending op (e.g. only a listening
|
|
479
|
-
`Gio.SocketService`) does not keep the process alive on its own, and a
|
|
480
|
-
purely-sync program still exits immediately.
|
|
481
|
-
|
|
482
|
-
#### Class prototypes and the lookup order
|
|
483
|
-
|
|
484
|
-
`Ns.Class.prototype` is a real prototype: reading a name on it MATERIALIZES the
|
|
485
|
-
introspected method behind that name (the JS twin of the `resolve` hook GJS
|
|
486
|
-
installs on every GObject prototype), and every wrapper resolves its members
|
|
487
|
-
through the prototype of its runtime GType — a `registerClass` subclass's own
|
|
488
|
-
prototype, otherwise the introspected class's — before any native fallback. So
|
|
489
|
-
instrumenting a method works the way it does on GJS:
|
|
490
|
-
|
|
491
|
-
```js
|
|
492
|
-
const proto = Gio.ZlibDecompressor.prototype;
|
|
493
|
-
const original = proto.convert; // the introspected method
|
|
494
|
-
proto.convert = function (...args) { // instrument it
|
|
495
|
-
calls++;
|
|
496
|
-
return original.apply(this, args);
|
|
497
|
-
};
|
|
498
|
-
const d = new Gio.ZlibDecompressor({ format: Gio.ZlibCompressorFormat.GZIP });
|
|
499
|
-
d.convert === proto.convert; // true — and the wrapper IS called
|
|
500
|
-
proto.convert = original; // put it back
|
|
501
|
-
```
|
|
502
|
-
|
|
503
|
-
Until 0.39 an instance ignored its prototype: the assignment stuck, `d.convert`
|
|
504
|
-
still reached the native method, and a spy-based test reported itself installed
|
|
505
|
-
while measuring nothing. Byte-compared against GJS by the `prototype-chain`
|
|
506
|
-
conformance program.
|
|
507
|
-
|
|
508
|
-
Lookup order on a wrapper: own JS field → the class prototype chain →
|
|
509
|
-
`runAsync` / the GObject.js shims → GObject property → inherited `Object.prototype`
|
|
510
|
-
member → a `Gio._promisify` registration → introspected method → `undefined`.
|
|
511
|
-
|
|
512
|
-
Deliberate divergences from GJS, all measured:
|
|
513
|
-
|
|
514
|
-
- class prototypes are NOT chained to their base classes, so `instanceof` goes
|
|
515
|
-
through `g_type_is_a` (see below) and an interface method materializes once per
|
|
516
|
-
implementing class instead of being one shared function;
|
|
517
|
-
- GObject **properties** are not surfaced as prototype accessors — they resolve on
|
|
518
|
-
the instance only;
|
|
519
|
-
- a private concrete GType (`GLocalFile`) resolves to its nearest INTROSPECTABLE
|
|
520
|
-
ancestor rather than to the interface GJS would use, which is why
|
|
521
|
-
`Gio._promisify` also keeps a per-class registry.
|
|
522
|
-
|
|
523
|
-
#### `GLib.Variant` (build + unpack, GJS semantics)
|
|
524
|
-
|
|
525
|
-
`new GLib.Variant(signature, value)` recursively builds a GVariant from a type
|
|
526
|
-
signature, and the wrapper exposes the GJS unpack flavours — the contract
|
|
527
|
-
GAction / GSettings / DBus payloads expect:
|
|
528
|
-
|
|
529
|
-
```js
|
|
530
|
-
const GLib = requireGi('GLib', '2.0');
|
|
531
|
-
|
|
532
|
-
new GLib.Variant('s', 'hi').deepUnpack(); // 'hi'
|
|
533
|
-
new GLib.Variant('as', ['a', 'b']).deepUnpack(); // ['a', 'b']
|
|
534
|
-
new GLib.Variant('(si)', ['x', 1]).deepUnpack(); // ['x', 1]
|
|
535
|
-
|
|
536
|
-
const v = new GLib.Variant('a{sv}', {
|
|
537
|
-
name: new GLib.Variant('s', 'Ada'),
|
|
538
|
-
age: new GLib.Variant('i', 36),
|
|
539
|
-
});
|
|
540
|
-
v.get_type_string(); // 'a{sv}'
|
|
541
|
-
v.deepUnpack(); // { name: Variant, age: Variant } (one level; values stay Variants)
|
|
542
|
-
v.recursiveUnpack(); // { name: 'Ada', age: 36 } (fully plain JS)
|
|
543
|
-
v.unpack(); // single level; children stay Variants
|
|
544
|
-
```
|
|
545
|
-
|
|
546
|
-
Supported type strings: the basics `b y n q i u x t h d s o g`, `v` (variant),
|
|
547
|
-
`m*` (maybe), `a*` arrays (incl. the `as` strv + `ay` bytestring fast-paths and
|
|
548
|
-
`a{..}` dictionaries), `(...)` tuples and `{kv}` dict-entries. Built Variants
|
|
549
|
-
round-trip as GObject arguments/properties/signal values — e.g. a
|
|
550
|
-
`Gio.SimpleAction` state:
|
|
551
|
-
|
|
552
|
-
```js
|
|
553
|
-
const Gio = requireGi('Gio', '2.0');
|
|
554
|
-
const action = Gio.SimpleAction.new_stateful('counter', null, new GLib.Variant('i', 0));
|
|
555
|
-
action.get_state().deepUnpack(); // 0
|
|
556
|
-
action.change_state(new GLib.Variant('i', 5));
|
|
557
|
-
action.get_state().deepUnpack(); // 5
|
|
558
|
-
```
|
|
121
|
+
An existing GJS program needs no edit: built with `gjsify build --app node`, an
|
|
122
|
+
`import Gio from 'gi://Gio?version=2.0'` is rewritten onto `requireGi` for you,
|
|
123
|
+
and the ambient GJS globals (`print`, `imports`, `ARGV`) are injected when the
|
|
124
|
+
bundled output still references them.
|
|
559
125
|
|
|
560
|
-
|
|
126
|
+
No explicit main loop is required for async work — `requireGi` arms the
|
|
127
|
+
GLib↔event-loop bridge on the first namespace load, so an `await`ed Gio async
|
|
128
|
+
op settles from the runtime's own loop, while a blocking `GLib.MainLoop.run()`
|
|
129
|
+
keeps Node's timers and I/O alive the way it does under `gjs -m`.
|
|
561
130
|
|
|
562
|
-
`
|
|
563
|
-
|
|
564
|
-
|
|
131
|
+
The whole surface — `GObject.registerClass`, `GLib.Variant`, the DBus client and
|
|
132
|
+
export halves, the legacy `imports.*` modules, cairo, and the measured
|
|
133
|
+
divergences from GJS — is
|
|
134
|
+
[docs/node-gi-gjs-surface.md](../../../docs/node-gi-gjs-surface.md).
|
|
565
135
|
|
|
566
|
-
|
|
567
|
-
const GObject = requireGi('GObject', '2.0');
|
|
568
|
-
|
|
569
|
-
const Counter = GObject.registerClass(
|
|
570
|
-
{
|
|
571
|
-
GTypeName: 'Counter',
|
|
572
|
-
Properties: {
|
|
573
|
-
// CONSTRUCT so the value is set before vfunc_constructed runs.
|
|
574
|
-
count: GObject.ParamSpec.int(
|
|
575
|
-
'count', 'Count', 'A counter',
|
|
576
|
-
GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT,
|
|
577
|
-
0, 100, 0,
|
|
578
|
-
),
|
|
579
|
-
},
|
|
580
|
-
Signals: { 'changed': { param_types: ['int'] } },
|
|
581
|
-
},
|
|
582
|
-
class Counter extends GObject.Object {
|
|
583
|
-
increment() { this.count += 1; this.emit('changed', this.count); }
|
|
584
|
-
vfunc_constructed() { /* runs during construction; `this` is the instance */ }
|
|
585
|
-
},
|
|
586
|
-
);
|
|
587
|
-
|
|
588
|
-
const c = new Counter({ count: 5 });
|
|
589
|
-
c.connect('changed', (n) => console.log('now', n));
|
|
590
|
-
c.increment(); // logs "now 6"
|
|
591
|
-
console.log(c.count); // 6 (custom property)
|
|
592
|
-
```
|
|
593
|
-
|
|
594
|
-
`new Counter(props)` constructs the GObject (`constructType`) and wraps it so the
|
|
595
|
-
user class's own prototype methods resolve FIRST, then the GObject property/signal
|
|
596
|
-
surface. `registerClass(class)` (no meta) is also accepted; the GTypeName then
|
|
597
|
-
defaults to the class name. The parent namespace/type is read from the class's
|
|
598
|
-
`extends` (its `$gtypeName`), so it works for both `GObject.Object` and real GI
|
|
599
|
-
classes (`class extends Gio.SimpleAction { … }`).
|
|
600
|
-
|
|
601
|
-
Caveats (this is the no-toggle-ref object model): the user class's JS constructor
|
|
602
|
-
body is not run — GObject-idiomatic init belongs in `vfunc_constructed`;
|
|
603
|
-
instances are Proxies over a native handle (but `instanceof` still works — it is
|
|
604
|
-
resolved through the GObject type system, see the `instanceof` note below);
|
|
605
|
-
**plain (non-GObject-property) JS instance fields do NOT cross the
|
|
606
|
-
vfunc↔instance boundary** — inside a vfunc, `this` is a distinct wrapper over the
|
|
607
|
-
same GObject (the native engine mints a fresh handle per call, so there is no
|
|
608
|
-
shared per-instance JS object yet), so use GObject **properties** for any state
|
|
609
|
-
that must be visible both inside a vfunc and on the instance (those live in C and
|
|
610
|
-
are consistent; the unified instance identity arrives with the toggle-ref work);
|
|
611
|
-
a JS↔GObject reference cycle on a custom instance leaks (the same cycle-leak
|
|
612
|
-
caveat the signal/vfunc layer carries).
|
|
613
|
-
|
|
614
|
-
#### `GObject` conveniences (signals, `GObject.Value`, `Object.new`)
|
|
615
|
-
|
|
616
|
-
`requireGi('GObject')` carries the GJS `GObject.js` convenience surface on top of
|
|
617
|
-
introspection:
|
|
618
|
-
|
|
619
|
-
```js
|
|
620
|
-
const GObject = requireGi('GObject', '2.0');
|
|
621
|
-
const Gio = requireGi('Gio', '2.0');
|
|
622
|
-
|
|
623
|
-
// By-function signal ops — node-gi connects through private closures, so the
|
|
624
|
-
// (function → handler id) mapping is recorded at connect() time.
|
|
625
|
-
const action = new Gio.SimpleAction({ name: 'a', enabled: true });
|
|
626
|
-
const onChange = () => { /* … */ };
|
|
627
|
-
action.connect('notify::enabled', onChange);
|
|
628
|
-
GObject.signal_handlers_block_by_func(action, onChange); // → count blocked
|
|
629
|
-
GObject.signal_handlers_unblock_by_func(action, onChange); // → count unblocked
|
|
630
|
-
GObject.signal_handlers_disconnect_by_func(action, onChange); // → count disconnected
|
|
631
|
-
action.block_signal_handler(id); action.unblock_signal_handler(id); // by id
|
|
632
|
-
action.stop_emission_by_name('notify::enabled'); // from within a handler
|
|
633
|
-
|
|
634
|
-
// GObject.Value — an explicit GValue you can build, set, read, copy and pass IN.
|
|
635
|
-
const v = new GObject.Value();
|
|
636
|
-
v.init(GObject.TYPE_INT); v.set_int(42); v.get_int(); // → 42
|
|
637
|
-
const s = new GObject.Value(GObject.TYPE_STRING, 'hi'); // 2-arg convenience
|
|
638
|
-
v instanceof GObject.Value; // → true
|
|
639
|
-
|
|
640
|
-
// Construct a GObject from a runtime GType.
|
|
641
|
-
const made = GObject.Object.new(Gio.SimpleAction.$gtype, { name: 'made' });
|
|
642
|
-
```
|
|
643
|
-
|
|
644
|
-
Also present: `GObject.ParamFlags` / `SignalFlags` (full introspected bitfields),
|
|
645
|
-
the fundamental `GObject.TYPE_*` GTypes, `GObject.AccumulatorType`, and
|
|
646
|
-
`GObject.signal_connect` / `signal_connect_after` / `signal_emit_by_name`.
|
|
647
|
-
|
|
648
|
-
`bind_property_full` / `BindingGroup.bind_full` work with **real JS transform
|
|
649
|
-
functions** (the engine marshals them as C `GBindingTransformFunc` trampolines,
|
|
650
|
-
the same architecture gjs uses via GjsPrivate — see `src/private.cc`): a
|
|
651
|
-
transform-to converts the bound value, returning `[false, …]` leaves the target
|
|
652
|
-
unchanged, a bidirectional transform-from converts back, and `null` transforms
|
|
653
|
-
give a plain copy binding. Verified byte-identical to gjs by the
|
|
654
|
-
`gclosure-in-args` conformance program. The related raw primitives
|
|
655
|
-
`GObject.signal_connect_closure` / `GObject.source_set_closure` accept a plain
|
|
656
|
-
JS function wherever a `GObject.Closure` IN-argument is expected (the engine
|
|
657
|
-
marshals it as a real GClosure).
|
|
658
|
-
|
|
659
|
-
**Kept-throw** (a clear, actionable error, not a crash): `ParamSpec.enum`
|
|
660
|
-
/ `flags` / `char` / `uchar` / `long` / `ulong` / `param` are not yet buildable (the
|
|
661
|
-
native param-spec builder covers int/uint/int64/uint64/double/float/string/boolean/
|
|
662
|
-
object/boxed).
|
|
663
|
-
|
|
664
|
-
#### `GLib` conveniences (`log_structured`, one-shot idle/timeout)
|
|
665
|
-
|
|
666
|
-
`requireGi('GLib')` carries `GLib.log_structured(domain, level, fields)` (packs
|
|
667
|
-
string / `Uint8Array` / `GLib.Variant` fields into an `a{sv}` and hands it to
|
|
668
|
-
`g_log_variant`) and the one-shot source helpers `idle_add_once` /
|
|
669
|
-
`timeout_add_once` / `timeout_add_seconds_once` (the callback runs once, then the
|
|
670
|
-
source is auto-removed).
|
|
671
|
-
|
|
672
|
-
`GLib.log_set_writer_func(fn)` installs a **JS `GLogWriterFunc`** as the
|
|
673
|
-
process structured-log writer, with gjs semantics (node-gi ships the same
|
|
674
|
-
thread-guarded C wrapper gjs routes through GjsPrivate — `src/private.cc`): the
|
|
675
|
-
writer receives `(logLevel, fields)` where `fields` is a plain object whose
|
|
676
|
-
values are `Uint8Array`s of the field bytes (`null` for empty fields) —
|
|
677
|
-
byte-for-byte the shape gjs's `{...stringFields.recursiveUnpack()}` produces —
|
|
678
|
-
and its returned `GLib.LogWriterOutput` drives the handled/unhandled fallback.
|
|
679
|
-
`GLib.log_set_writer_default()` detaches the JS writer (later logs fall back to
|
|
680
|
-
`g_log_writer_default`). Verified byte-identical to gjs by the `log-writer`
|
|
681
|
-
conformance program. Two contracts, identical under gjs: the underlying
|
|
682
|
-
`g_log_set_writer_func` may only ever be called ONCE per process — a second
|
|
683
|
-
`GLib.log_set_writer_func(fn)` call aborts inside GLib itself (install one
|
|
684
|
-
writer per process; `log_set_writer_default()` detaches the JS side but cannot
|
|
685
|
-
re-arm a new install) — and an off-thread log falls back to the default writer
|
|
686
|
-
in C (JS is never entered from a foreign thread).
|
|
687
|
-
|
|
688
|
-
#### `Gio.DBus` (client proxy, name owning + object export)
|
|
689
|
-
|
|
690
|
-
`requireGi('Gio')` carries the GJS DBus surface — both halves. The **client**
|
|
691
|
-
half: `Gio.DBusProxy.makeProxyWrapper(interfaceXml)` parses the interface XML and
|
|
692
|
-
returns a proxy constructor whose instances expose each method as `NameSync` (sync),
|
|
693
|
-
`NameRemote` (raw async callback) and `NameAsync` (Promise), each property as a
|
|
694
|
-
getter/setter, and each signal via `connectSignal` / `disconnectSignal` (the same
|
|
695
|
-
pure-JS `_signals` mixin GJS uses). `Gio.DBus.session` / `Gio.DBus.system` are the
|
|
696
|
-
bus getters; `Gio.DBus.own_name` / `unown_name` / `watch_name` / `unwatch_name`
|
|
697
|
-
own and watch bus names. The **export** half
|
|
698
|
-
(`Gio.DBusExportedObject.wrapJSObject` — exporting a JS object AS a DBus
|
|
699
|
-
service) is described below the example.
|
|
700
|
-
|
|
701
|
-
```js
|
|
702
|
-
const Gio = requireGi('Gio', '2.0');
|
|
703
|
-
|
|
704
|
-
const Proxy = Gio.DBusProxy.makeProxyWrapper(`<node>
|
|
705
|
-
<interface name="org.freedesktop.DBus">
|
|
706
|
-
<method name="GetId"><arg type="s" direction="out"/></method>
|
|
707
|
-
<signal name="NameOwnerChanged"><arg type="s"/><arg type="s"/><arg type="s"/></signal>
|
|
708
|
-
</interface>
|
|
709
|
-
</node>`);
|
|
710
|
-
|
|
711
|
-
const proxy = new Proxy(Gio.DBus.session, 'org.freedesktop.DBus', '/org/freedesktop/DBus');
|
|
712
|
-
const [busId] = proxy.GetIdSync(); // synchronous method
|
|
713
|
-
proxy.GetIdRemote((result, error) => { /* … */ }); // async, raw callback
|
|
714
|
-
const [id2] = await proxy.GetIdAsync(); // async, Promise (drains after run())
|
|
715
|
-
proxy.connectSignal('NameOwnerChanged', (p, sender, [name, oldOwner, newOwner]) => { /* … */ });
|
|
716
|
-
|
|
717
|
-
const id = Gio.DBus.own_name(Gio.BusType.SESSION, 'org.example.App',
|
|
718
|
-
Gio.BusNameOwnerFlags.NONE, null, (conn, name) => { /* acquired */ }, null);
|
|
719
|
-
```
|
|
720
|
-
|
|
721
|
-
Async replies / signals / name callbacks dispatch from the default main context —
|
|
722
|
-
either a blocking `GLib.MainLoop.run()` or, with no loop anywhere, the uv-driven
|
|
723
|
-
auto-pump (an `await proxy.GetIdAsync()` at top level settles like any other
|
|
724
|
-
async Gio op). A `NameAsync` **Promise** `.then` drains *while* a node-gi loop
|
|
725
|
-
blocks on all three runtimes (the reply's GI callback settles the Promise and
|
|
726
|
-
the microtask checkpoint at that loop-dispatched boundary runs the
|
|
727
|
-
continuation). One Node-only divergence remains: when the blocking `run()` is
|
|
728
|
-
itself entered inside a live async scope (module top-level evaluation, an
|
|
729
|
-
`await`, `node:test`), V8 refuses the nested checkpoint (node-gtk #442/#121) —
|
|
730
|
-
the reply still fires and settles the Promise, so it resolves once `run()`
|
|
731
|
-
returns; defer the blocking run to a macrotask (what `runAsync` does) or drive
|
|
732
|
-
the method through the raw `NameRemote` callback. Bun/Deno do not share that
|
|
733
|
-
nesting restriction: their registered drain primitives run even under a
|
|
734
|
-
top-level blocking `run()`.
|
|
735
|
-
|
|
736
|
-
**Object export** (`Gio.DBusExportedObject.wrapJSObject` — exporting a JS
|
|
737
|
-
object AS a DBus service) works with GJS semantics. GJS builds it on
|
|
738
|
-
`GjsPrivate.DBusImplementation` (a GJS-internal C type, absent on a plain
|
|
739
|
-
Node/GI host); node-gi instead drives the **introspectable**
|
|
740
|
-
`g_dbus_connection_register_object_with_closures2` (GLib ≥ 2.84) — the
|
|
741
|
-
method-call / get-property / set-property vtable slots are plain JS functions
|
|
742
|
-
the engine marshals as real **GClosure IN-arguments**:
|
|
743
|
-
|
|
744
|
-
```js
|
|
745
|
-
const service = {
|
|
746
|
-
Level: 7, // property (read via the interface XML signature)
|
|
747
|
-
Echo(s) { return `echo:${s}`; }, // method (an Async variant + Promise return also work)
|
|
748
|
-
Boom() { throw new Error('kaboom'); }, // a throw becomes a DBus error (org.gnome.gjs.JSError.*)
|
|
749
|
-
};
|
|
750
|
-
const impl = Gio.DBusExportedObject.wrapJSObject(interfaceXml, service);
|
|
751
|
-
impl.export(Gio.DBus.session, '/org/example/App');
|
|
752
|
-
impl.emit_signal('Pinged', new GLib.Variant('(s)', ['ping!']));
|
|
753
|
-
impl.emit_property_changed('Level', new GLib.Variant('i', 8)); // updates proxy caches
|
|
754
|
-
impl.unexport(); // releases the registration + its closures
|
|
755
|
-
```
|
|
756
|
-
|
|
757
|
-
The impl surface matches GJS (`export` / `unexport` / `unexport_from_connection`
|
|
758
|
-
/ `emit_signal` / `emit_property_changed` / `flush` / `get_object_path`, plus
|
|
759
|
-
node-gi's usual camelCase aliases). The full round-trip — exported method,
|
|
760
|
-
property get/set through `org.freedesktop.DBus.Properties`, a throwing method
|
|
761
|
-
returning a DBus error, `emit_signal`, `emit_property_changed` updating a
|
|
762
|
-
proxy's cached property, and unexport — runs byte-identical to gjs
|
|
763
|
-
(`dbus/export-scenario.mjs`, cross-checked against `gjs -m` under
|
|
764
|
-
`dbus-run-session` by `npm run test:dbus`, which passes
|
|
765
|
-
`--config-file=test/session.conf` — a plain `unix:tmpdir=/tmp` bus, because
|
|
766
|
-
dbus-run-session's DEFAULT config cannot start on macOS, where Homebrew's dbus
|
|
767
|
-
listens on launchd). Lifetime: the registration ref+sinks
|
|
768
|
-
its closures, so the service object lives exactly as long as the registration
|
|
769
|
-
(surviving GC with no JS references) and becomes collectable after
|
|
770
|
-
`unexport()` — guarded by the `--expose-gc` leg of the dbus suite. A method
|
|
771
|
-
handler receives the GJS-appended trailing `Gio.UnixFDList` argument (`null`
|
|
772
|
-
when the call carries no fds — verified against gjs; an actual fd-carrying
|
|
773
|
-
call arrives as a wrapped `UnixFDList` but deeper fd extraction is untested).
|
|
774
|
-
As under GJS, a **sync** self-call from the exporting process deadlocks the
|
|
775
|
-
shared main loop that must also service the incoming request — use the async
|
|
776
|
-
`NameRemote` forms.
|
|
777
|
-
|
|
778
|
-
### GJS ambient globals (`@gjsify/node-gi/globals`)
|
|
779
|
-
|
|
780
|
-
GJS source relies on globals that exist implicitly under `gjs` — `print`,
|
|
781
|
-
`printerr`, `log`, `logError`, `ARGV`, and the legacy `imports` object. Importing
|
|
782
|
-
`@gjsify/node-gi/globals` (a side effect) installs Node-backed equivalents that
|
|
783
|
-
route through the same backend:
|
|
784
|
-
|
|
785
|
-
```js
|
|
786
|
-
import '@gjsify/node-gi/globals';
|
|
787
|
-
|
|
788
|
-
print('hello', 1, true); // → stdout, GJS String()-join
|
|
789
|
-
const GLib = imports.gi.GLib; // legacy imports.gi (honours .versions)
|
|
790
|
-
imports.gi.versions.Gtk = '4.0';
|
|
791
|
-
console.log(imports.gettext.gettext('x')); // no-translation passthrough
|
|
792
|
-
|
|
793
|
-
// Legacy script modules many older GJS sources use:
|
|
794
|
-
const emitter = {};
|
|
795
|
-
imports.signals.addSignalMethods(emitter); // the pure-JS Signals mixin
|
|
796
|
-
emitter.connect('ready', () => imports.mainloop.quit());
|
|
797
|
-
imports.mainloop.timeout_add(50, () => { emitter.emit('ready'); return false; });
|
|
798
|
-
imports.mainloop.run(); // thin GLib.MainLoop wrapper
|
|
799
|
-
|
|
800
|
-
// imports.byteArray — the legacy byte-array module, with GJS semantics
|
|
801
|
-
// (fromString/toString are zero-terminated + fatal-decode; fromGBytes/toGBytes
|
|
802
|
-
// round-trip GLib.Bytes; fromArray wraps in the legacy ByteArray class). This
|
|
803
|
-
// is the seam @gjsify/utils' cli()/gbytesToUint8Array — and through them
|
|
804
|
-
// @gjsify/os + @gjsify/child_process — read GLib subprocess output with:
|
|
805
|
-
const [ok, out] = imports.gi.GLib.spawn_command_line_sync('echo hi');
|
|
806
|
-
console.log(imports.byteArray.toString(out)); // 'hi\n'
|
|
807
|
-
const bytes = imports.byteArray.toGBytes(Uint8Array.of(1, 2, 3));
|
|
808
|
-
imports.byteArray.fromGBytes(bytes); // Uint8Array [1, 2, 3] (a copy)
|
|
809
|
-
```
|
|
810
|
-
|
|
811
|
-
A follow-up `--app node` build step will inject this automatically for any
|
|
812
|
-
bundle that references those globals (so `const Gtk = imports.gi.Gtk` /
|
|
813
|
-
`print(...)` GJS source runs unmodified on Node); today it is an explicit import.
|
|
814
|
-
|
|
815
|
-
The remaining GJS-compatible surface (`import GLib from 'gi://GLib?version=2.0'`,
|
|
816
|
-
`const GLib = imports.gi.GLib`, the core overrides, `_promisify`, the legacy
|
|
817
|
-
`imports.*` modules) is layered on top of this engine in the gjsify bundler
|
|
818
|
-
integration and subsequent drops.
|
|
819
|
-
|
|
820
|
-
### cairo (`@gjsify/node-gi/cairo`)
|
|
821
|
-
|
|
822
|
-
cairo is a **foreign struct** in GObject-Introspection: GI does not know the
|
|
823
|
-
layout of `cairo_t` / `cairo_surface_t` / `cairo_pattern_t`, so it delegates their
|
|
824
|
-
marshalling to a module. GJS ships a native cairo binding + a foreign-struct
|
|
825
|
-
registration so that a GI function taking/returning a cairo pointer (e.g. a
|
|
826
|
-
`Gtk.DrawingArea` draw-func's `cairo_t`) round-trips to/from the JS cairo objects.
|
|
827
|
-
node-gi ports that seam: the same drawing code runs on GJS (native cairo) and Node
|
|
828
|
-
(this binding). An npm `cairo` package cannot stand in — a foreign cairo argument
|
|
829
|
-
must marshal through the SAME module the engine's foreign-struct seam knows about.
|
|
830
|
-
|
|
831
|
-
```js
|
|
832
|
-
import cairo from '@gjsify/node-gi/cairo'; // bare `cairo` on the --app node build
|
|
833
|
-
import { requireGi } from '@gjsify/node-gi/gi';
|
|
834
|
-
|
|
835
|
-
// Headless drawing — read the pixels back with getData().
|
|
836
|
-
const surface = new cairo.ImageSurface(cairo.Format.ARGB32, 64, 48);
|
|
837
|
-
const cr = new cairo.Context(surface);
|
|
838
|
-
cr.setSourceRGB(0.8, 0.1, 0.1);
|
|
839
|
-
cr.rectangle(8, 8, 20, 16);
|
|
840
|
-
cr.fill();
|
|
841
|
-
cr.$dispose();
|
|
842
|
-
surface.flush();
|
|
843
|
-
const pixels = surface.getData(); // Uint8Array (stride * height), ARGB32
|
|
844
|
-
|
|
845
|
-
// The foreign seam: a GI function taking a cairo_t marshals the Context through.
|
|
846
|
-
const PangoCairo = requireGi('PangoCairo', '1.0');
|
|
847
|
-
const layout = PangoCairo.create_layout(new cairo.Context(surface));
|
|
848
|
-
|
|
849
|
-
// A Gtk.DrawingArea draw-func receives a cairo_t → a live cairo.Context:
|
|
850
|
-
// area.set_draw_func((_area, ctx, w, h) => { ctx.setSourceRGB(1, 0, 0); … });
|
|
851
|
-
```
|
|
852
|
-
|
|
853
|
-
Ported this slice: `cairo.Context` (drawing + transform ops incl.
|
|
854
|
-
`identityMatrix` and the `userToDevice[Distance]` / `deviceToUser[Distance]`
|
|
855
|
-
point transforms, state getters, `setDash`/`getDashCount` (+ a net-new
|
|
856
|
-
`getDash`), `inFill`/`inStroke`, `newSubPath`, `copyPath`/`copyPathFlat`/
|
|
857
|
-
`appendPath` (owned `cairo.Path` handles), `getSource` with concrete-subclass
|
|
858
|
-
fan-out, `$dispose`), `cairo.Surface` + `cairo.ImageSurface` (`getData`/
|
|
859
|
-
`getWidth`/`getHeight`/`getStride`/`getFormat`/`flush`/`writeToPNG`/
|
|
860
|
-
`createFromPNG`), the patterns — `cairo.SolidPattern`,
|
|
861
|
-
`cairo.LinearGradient`/`cairo.RadialGradient` (`addColorStopRGB[A]` via the
|
|
862
|
-
shared `cairo.Gradient` base), `cairo.SurfacePattern`
|
|
863
|
-
(`setExtend`/`getExtend`/`setFilter`/`getFilter`) — the opaque `cairo.Path`,
|
|
864
|
-
and the enums (`Format`, `Operator`, `Content`, `Extend`, `Filter`, …). This is
|
|
865
|
-
the full surface `@gjsify/canvas2d-core` draws through (headless Canvas 2D).
|
|
866
|
-
The native binding paints **byte-for-byte identically to GJS** (verified
|
|
867
|
-
pixel-for-pixel against `gjs -m`, incl. gradients / repeating surface patterns /
|
|
868
|
-
dashed strokes / path round-trips — `test/cairo-canvas2d.test.mjs`). Deferred:
|
|
869
|
-
region objects, the PDF/SVG/PS surfaces, and the text/font ops
|
|
870
|
-
(`showText`/`selectFontFace` — canvas2d text rides PangoCairo instead).
|
|
871
|
-
|
|
872
|
-
Building on that seam, the **LIVE `@gjsify/canvas2d` `Canvas2DBridge`** — a
|
|
873
|
-
`Gtk.DrawingArea` that wraps an `HTMLCanvasElement` 2D context and blits its Cairo
|
|
874
|
-
surface onto the widget each frame — realizes, draws and blits UNCHANGED on
|
|
875
|
-
node-gi under a display: an app draws via the standard `canvas.getContext('2d')`
|
|
876
|
-
DOM API in `bridge.onReady`, the GTK draw_func fires, the bridge blits
|
|
877
|
-
(`cr.setSourceSurface` + `cr.paint`) and the rAF (`add_tick_callback`) path ticks.
|
|
878
|
-
The same source builds `--app gjs` and `--app node` and prints byte-identical
|
|
879
|
-
output, pixels read back off the canvas included — `test/canvas2d-bridge.test.mjs`
|
|
880
|
-
+ `fixtures/canvas2d-bridge-app.ts`. It is a **local/dev verification** (see the
|
|
881
|
-
run recipe in the test header), NOT wired into CI: the LIVE bridge pulls the whole
|
|
882
|
-
`@gjsify/canvas2d` gi:// graph, so it needs the full gjsify workspace built with a
|
|
883
|
-
current-source `@gjsify/cli` (the bare-`cairo`→`@gjsify/node-gi/cairo` and
|
|
884
|
-
register-inline fixes the published CLI predates) plus a display and the addon — a
|
|
885
|
-
heavyweight from-scratch rebuild not worth gating a minimal CI container on. The
|
|
886
|
-
test self-skips in the default `npm test` (no display). One node-only note: a
|
|
887
|
-
mapped `Gtk.DrawingArea`'s live `GdkFrameClock` stays an active GLib source after
|
|
888
|
-
`app.quit()`, so — matching the documented lifetime divergence — a node-gi GTK
|
|
889
|
-
program that must terminate exits explicitly (`process.exit(0)`), whereas `gjs -m`
|
|
890
|
-
exits on module completion.
|
|
891
|
-
|
|
892
|
-
### The live `@gjsify/event-bridge` dispatches DOM events on node-gi
|
|
893
|
-
|
|
894
|
-
The GTK→DOM event bridge (`@gjsify/event-bridge`'s `attachEventControllers`) —
|
|
895
|
-
which attaches GTK4 `EventControllerMotion`/`GestureClick`/`EventControllerScroll`/
|
|
896
|
-
`EventControllerKey`/`EventControllerFocus` to a widget and dispatches W3C DOM
|
|
897
|
-
events (Mouse/Pointer/Keyboard/Wheel/FocusEvent) — runs UNCHANGED on node-gi. The
|
|
898
|
-
shared fixture presents a `Gtk.DrawingArea`, attaches the controllers, and drives a
|
|
899
|
-
SYNTHESIZED event through each live `Gtk.EventController*` via `emit(signal, …)`
|
|
900
|
-
(the same path the GJS `event-bridge.spec.ts` drives), then asserts the dispatched
|
|
901
|
-
DOM event's type / coords / `getModifierState` / key / code. The `Gdk.ModifierType`
|
|
902
|
-
flags and `Gdk.keyval_name`/`Gdk.keyval_to_unicode` marshalling produce
|
|
903
|
-
byte-identical DOM events under node-gi and `gjs -m` — the same source builds
|
|
904
|
-
`--app gjs` and `--app node` and prints the committed golden
|
|
905
|
-
(`test/event-bridge.test.mjs` + `fixtures/event-bridge-app.ts`). Every golden line
|
|
906
|
-
is deterministic + display-independent (coords clamp to a fixed 400x300 allocation;
|
|
907
|
-
key/code/modifiers derive from the Gdk marshalling), so byte-parity is stable.
|
|
136
|
+
## Runtimes (Node / Bun / Deno)
|
|
908
137
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
`
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
and a `registerClass` subclass against its leaf / base / interface — while a sibling
|
|
916
|
-
type, an unrelated class, a boxed/`Variant` handle, `null` or a plain object stay
|
|
917
|
-
`false`. `test/instanceof.test.mjs` + the cross-runtime golden
|
|
918
|
-
`conformance/programs/instanceof-hierarchy.conf.mjs` (gjs/node/bun/deno byte-identical)
|
|
919
|
-
guard it. The event-bridge fixture retrieves controllers by ADD ORDER off
|
|
920
|
-
`widget.observe_controllers()` as a stylistic choice (identity is preserved and
|
|
921
|
-
`emit()` resolves the signal by the live GType) — no longer forced by a gap.
|
|
922
|
-
(A second gap the fixture originally routed around — `new
|
|
923
|
-
Gdk.Rectangle()` threw `no static method 'new'` — is FIXED: `new <BoxedStruct>()`
|
|
924
|
-
now zero-allocates with GJS `gi/boxed.cpp` semantics when the struct has no `new`
|
|
925
|
-
constructor (`Graphene.Rect`/`Point`, `Gdk.Rectangle`, `Gdk.RGBA` — the
|
|
926
|
-
`@gjsify/devtools` screenshot chain), routes to `new` when it exists, and throws
|
|
927
|
-
a clear error for args without one; `test/struct-construct.test.mjs` guards it,
|
|
928
|
-
gjs-parity included. The fixture still reads the presented window's real
|
|
929
|
-
allocation — simpler and display-truthful.)
|
|
138
|
+
The engine is a **Node-API** addon, so the same binary loads and runs on **Node,
|
|
139
|
+
Bun and Deno** — Node-API is their common native ABI (no separate bindings, no Rust
|
|
140
|
+
rewrite). `index.js` detects the runtime and prefers a shipped
|
|
141
|
+
`prebuilds/<platform>-<arch>/node_gi.node` over a local `build/` (Deno runs no
|
|
142
|
+
postinstall build, so a prebuild is its only install path — stage one with
|
|
143
|
+
`npm run build:prebuild`).
|
|
930
144
|
|
|
931
|
-
|
|
932
|
-
|
|
145
|
+
| Capability | Node | Bun | Deno |
|
|
146
|
+
|---|:--:|:--:|:--:|
|
|
147
|
+
| introspection, marshalling, enums, variants | ✅ | ✅ | ✅ |
|
|
148
|
+
| GObject create / properties / signals | ✅ | ✅ | ✅ |
|
|
149
|
+
| `registerClass` / subclass / vfunc chain-up | ✅ | ✅ | ✅ |
|
|
150
|
+
| toggle-ref GC + cross-thread teardown | ✅ | ✅ | ✅ |
|
|
151
|
+
| GLib async with NO mainloop (timeouts, GIO async, DBus) | ✅ auto | via `startMainContextPump` | via `startMainContextPump` |
|
|
152
|
+
| blocking `GLib.MainLoop.run()` / `Gio.Application.runAsync()` | ✅ | ✅ | ✅ |
|
|
153
|
+
| Promise continuations drain **during** a blocking GLib loop (async DBus replies, `await` chains) | ✅ | ✅ | ✅ |
|
|
154
|
+
| Node timers/I/O alive **during** a blocking GLib loop (uv co-pump) | ✅ | — | — |
|
|
933
155
|
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
node --test test/event-bridge.test.mjs
|
|
941
|
-
# Drop NODE_GI_EB_SKIP_GJS to additionally re-prove the golden IS gjs's own output
|
|
942
|
-
# (builds + runs --app gjs; needs the workspace CLI, not the published one).
|
|
943
|
-
```
|
|
156
|
+
Two libuv-coupled subsystems are what make that portable: the toggle-ref
|
|
157
|
+
teardown drain uses a `napi_threadsafe_function` (core Node-API) rather than a raw
|
|
158
|
+
`uv_async_t`, and the main loop runs both directions — Node co-pumps libuv during
|
|
159
|
+
a blocking GLib loop and dispatches pending GLib sources from its own loop when
|
|
160
|
+
none runs, while Bun/Deno iterate the default GLib context from a runtime timer
|
|
161
|
+
(`startMainContextPump` from `@gjsify/node-gi/gi`, auto-armed by `requireGi`).
|
|
944
162
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
163
|
+
Bun reaches full parity with Node on the core surface; Deno passes the same
|
|
164
|
+
conformance subset since 2.9 (the marshalling/async N-API quirks that excluded
|
|
165
|
+
`arrays`/`async-error` on Deno 2.1 are fixed upstream) — the Node-only co-pump
|
|
166
|
+
cases are the remaining, by-design gap. The authoritative full suite runs on Node.
|
|
949
167
|
|
|
950
|
-
**
|
|
951
|
-
|
|
952
|
-
`
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
(`set_use_es(true)`, `set_required_version(3, 2)`, depth + stencil) realizes
|
|
956
|
-
with `get_error() === null`, an **OpenGL ES 3.2** context
|
|
957
|
-
(`Gdk.GLContext.get_current()` non-null in both `realize` and `render`), and
|
|
958
|
-
the `gwebgl` Vala bridge (`new Gwebgl.WebGLRenderingContextBase()` — the native
|
|
959
|
-
class `@gjsify/webgl` wraps) works through it: `getString(GL_VERSION/…)`, a
|
|
960
|
-
`getParameterx` **GVariant** round-trip, and a real WebGL draw —
|
|
961
|
-
`clearColor(1,0,0,1)` + `clear` + `readPixels` reading back the exact
|
|
962
|
-
`255,0,0,255` pixel. The committed golden is byte-identical between `gjs -m`
|
|
963
|
-
and `node` (the gjs gold-standard leg re-proves it wherever `gjs` is present;
|
|
964
|
-
`NODE_GI_WEBGL_SKIP_GJS=1` skips that leg).
|
|
168
|
+
**The measured detail lives one hop away:** the hand-measured `linux-arm64` /
|
|
169
|
+
musl / Bun / Deno numbers, the darwin dyld loader gap (why
|
|
170
|
+
`Failed to load shared library 'libgtk-4.1.dylib' referenced by the typelib` is
|
|
171
|
+
never a node-gi defect), and the GTK runtime bundles are
|
|
172
|
+
[docs/node-gi-platform-notes.md](../../../docs/node-gi-platform-notes.md).
|
|
965
173
|
|
|
966
|
-
|
|
174
|
+
## Build & test
|
|
967
175
|
|
|
968
176
|
```bash
|
|
969
|
-
#
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
```
|
|
975
|
-
|
|
976
|
-
**
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
`gjsify workspace @gjsify/cli build --with-dependencies`), like
|
|
993
|
-
`canvas2d-bridge`; the gjs gold-standard leg additionally needs the workspace
|
|
994
|
-
register libs built (`--app gjs` force-inlines `<pkg>/register`). Two engine
|
|
995
|
-
gaps this spike fixed on the way (headless regression coverage in
|
|
996
|
-
`test/gerror-return.test.mjs`): GError-typed RETURNS
|
|
997
|
-
(`Gtk.GLArea.get_error()` — `GI_TYPE_TAG_ERROR` → a field-readable GLib.Error
|
|
998
|
-
boxed) and literal-first method-name resolution (Vala GIRs carry camelCase
|
|
999
|
-
names — Gwebgl's `getString` — which the unconditional camelCase→snake_case
|
|
1000
|
-
alias destroyed; the engine now resolves the literal name first, alias second).
|
|
1001
|
-
|
|
1002
|
-
### Excalibur.js renders through WebGL on node-gi (the GTK-bridge capstone)
|
|
1003
|
-
|
|
1004
|
-
**Definitive: a REAL WebGL game engine — Excalibur 0.32, the engine behind the
|
|
1005
|
-
`excalibur-jelly-jumper` showcase and the PixelRPG map-editor — boots, runs its
|
|
1006
|
-
clock, and renders frames through `@gjsify/webgl`'s `WebGLBridge` UNCHANGED
|
|
1007
|
-
under node-gi** (`test/excalibur-webgl.test.mjs` + the ONE dual-runtime source
|
|
1008
|
-
`fixtures/excalibur-webgl-app.ts`). `new ex.Engine({ canvasElement })` builds
|
|
1009
|
-
against the bridge's `HTMLCanvasElement` (WebGL2 context), `engine.start()`
|
|
1010
|
-
resolves, the engine's real render pipeline runs (shader compile/link,
|
|
1011
|
-
`bufferData`, VAOs, `vertexAttribPointer`, `drawArrays`/`drawElements`,
|
|
1012
|
-
`clearBufferfv` at `RenderTarget.blitToScreen`) for 5 frames, and the committed
|
|
1013
|
-
golden asserts the pixels read back off the GL framebuffer — the screen-centered
|
|
1014
|
-
blue Actor and the red engine clear color — **byte-identical between `gjs -m`
|
|
1015
|
-
and `node`**. The DOM surface (document/HTMLCanvasElement/ResizeObserver/
|
|
1016
|
-
matchMedia/XHR) comes from the SAME `@gjsify/*` registers the gjs build injects,
|
|
1017
|
-
via the `--app node` explicit-`--globals` reverse-bridge injection.
|
|
1018
|
-
|
|
1019
|
-
Excalibur's real GL + engine usage exposed four core gaps, all fixed at the
|
|
1020
|
-
engine (each with regression coverage):
|
|
1021
|
-
|
|
1022
|
-
- **`GVariant 'ay'` rejects `null`** (`src/variant.cc`) — GJS packs
|
|
1023
|
-
`new GLib.Variant('ay', null)` as the EMPTY byte array (`GLib.Bytes(null)`);
|
|
1024
|
-
node-gi threw. Exposing call: Excalibur's `texImage2D(..., null)`
|
|
1025
|
-
blank-texture allocation at renderer init (`Uint8ArrayToVariant(null)`).
|
|
1026
|
-
- **Unknown members must be `undefined`, not a throw-on-call thunk**
|
|
1027
|
-
(`src/calls.cc` `hasMethod` + the L1 wrapper `get`) — real consumers
|
|
1028
|
-
feature-detect optional native methods (`typeof gl.clearBufferfv ===
|
|
1029
|
-
'function'` gates `@gjsify/webgl`'s clearBuffer emulation, hit at
|
|
1030
|
-
`blitToScreen`); the old always-a-function proxy made that detection lie,
|
|
1031
|
-
then threw mid-frame. `hasMethod(handle, name)` resolves through the SAME
|
|
1032
|
-
literal-first/snake-alias walk `callMethod` uses.
|
|
1033
|
-
- **Signal dispatch now runs the microtask checkpoint at its boundary**
|
|
1034
|
-
(`src/signals.cc`, `napi_make_callback`) — GJS drains the promise-job queue
|
|
1035
|
-
when the outermost JS frame exits; promise chains resolved inside a
|
|
1036
|
-
loop-dispatched signal handler (Excalibur's whole `engine.start()` boot,
|
|
1037
|
-
queued from the GLArea `render`/`onReady` dispatch) previously lingered
|
|
1038
|
-
until the libuv↔GLib bridge's prepare-phase drain.
|
|
1039
|
-
- **The uv co-pump source must not outrank GTK painting** (`src/loop.cc`) —
|
|
1040
|
-
at the default `G_PRIORITY_DEFAULT` a busy Node loop STARVED
|
|
1041
|
-
`GDK_PRIORITY_REDRAW`: Excalibur's `requestIdleCallback` polyfill (a
|
|
1042
|
-
self-re-arming 1 ms `setTimeout`, run perpetually by its GarbageCollector)
|
|
1043
|
-
kept the UvLoopSource ready on every GLib iteration, so ticks/renders/rAF
|
|
1044
|
-
froze while plain GLib timeouts kept firing. The source now sits below
|
|
1045
|
-
redraw (`G_PRIORITY_HIGH_IDLE + 30`): rendering outranks Node timers,
|
|
1046
|
-
browser-like, and Node I/O still runs in every frame gap.
|
|
1047
|
-
|
|
1048
|
-
Run it (a LOCAL/dev verification like the other display suites — self-skips
|
|
1049
|
-
without a display / CLI / prebuild / built workspace):
|
|
1050
|
-
|
|
1051
|
-
```sh
|
|
1052
|
-
export GJSIFY_BIN="$(git rev-parse --show-toplevel)/packages/infra/cli/lib/index.js"
|
|
1053
|
-
xvfb-run -a dbus-run-session -- \
|
|
1054
|
-
env -u FORCE_COLOR GSK_RENDERER=cairo GDK_BACKEND=x11 LIBGL_ALWAYS_SOFTWARE=1 \
|
|
1055
|
-
GTK_A11Y=none NODE_GI_NATIVE=build GJSIFY_BIN="$GJSIFY_BIN" \
|
|
1056
|
-
node --test test/excalibur-webgl.test.mjs
|
|
1057
|
-
# Drop NODE_GI_EXCALIBUR_SKIP_GJS to additionally re-prove the golden IS gjs's
|
|
1058
|
-
# own byte-output (builds + runs --app gjs).
|
|
1059
|
-
```
|
|
1060
|
-
|
|
1061
|
-
The FULL `excalibur-jelly-jumper` showcase builds `--app node`
|
|
1062
|
-
(`gjsify run build:node`; `gjsify.example.runtimes` includes `node`) and gets
|
|
1063
|
-
remarkably far on node-gi: the GTK window presents, the devtools control plane
|
|
1064
|
-
exports over DBus, `Gst.init` runs and every `ex.Sound` constructs its
|
|
1065
|
-
Gst-backed `AudioContext` (this exposed + fixed the nullable-array `null`
|
|
1066
|
-
marshalling — `Gst.init(null)`), and Excalibur boots into resource loading.
|
|
1067
|
-
The remaining blocker is POLYFILL ROUTING, not marshalling: on Node the
|
|
1068
|
-
GLOBAL `fetch` is the native undici one (the register convention never
|
|
1069
|
-
overrides an existing native), and `@excaliburjs/plugin-tiled`'s fileLoader
|
|
1070
|
-
feeds it the root-relative `/res/…` paths that only OUR GJS fetch/XHR resolve
|
|
1071
|
-
against the program dir — undici rejects them (`Failed to parse URL`), the
|
|
1072
|
-
Tiled map never loads, and scene init fails. Making the reverse bridge route
|
|
1073
|
-
`fetch` (and friends) to the `@gjsify/*` polyfills over the runtime natives is
|
|
1074
|
-
the follow-up that unlocks the full game. (`jsdom` — plugin-tiled's node-side
|
|
1075
|
-
DOMParser fallback — is aliased to `@gjsify/empty` in `build:node`, mirroring
|
|
1076
|
-
the plugin's own `"browser": { "jsdom": false }`.)
|
|
1077
|
-
|
|
1078
|
-
### A real Adwaita WINDOW realizes + renders (the GTK-GUI capstone)
|
|
1079
|
-
|
|
1080
|
-
Beyond the display-free conformance: an UNCHANGED `Adw.Application` +
|
|
1081
|
-
`Adw.ApplicationWindow` (HeaderBar / WindowTitle / StatusPage) not only
|
|
1082
|
-
constructs + presents but **realizes and RENDERS a surface** through the GSK
|
|
1083
|
-
renderer on node-gi — the same in-process capture path `@gjsify/devtools`'
|
|
1084
|
-
`Screenshot` uses: `Gtk.WidgetPaintable` → `Gtk.Snapshot.to_node()` →
|
|
1085
|
-
`Gsk.Renderer.render_texture` → `Gdk.Texture.save_to_png_bytes`. A non-empty PNG
|
|
1086
|
-
is the unambiguous proof that a `GdkSurface` was allocated + a GSK render tree
|
|
1087
|
-
rasterised — not reachable by any headless program. Guarded by
|
|
1088
|
-
`test/windowing.test.mjs` + `test/windowing-interactive.test.mjs` (an
|
|
1089
|
-
`Adw.ApplicationWindow` that RESPONDS to a `Gio.SimpleAction` + a
|
|
1090
|
-
`Gtk.Button::clicked` through the node-gi signal chain) + `test/widgets.test.mjs`
|
|
1091
|
-
(the Adwaita widget breadth below) — all self-skip without a display on Linux (the
|
|
1092
|
-
win32/darwin GDK backend supplies its own display, so they run there), wired into
|
|
1093
|
-
the Linux `gtk-smoke` + the Windows windowing CI jobs. The
|
|
1094
|
-
`showcases/gtk/node-gi-window` showcase runs the SAME single source on both GJS
|
|
1095
|
-
and Node and screenshots the live window over the `org.gjsify.Devtools` DBus
|
|
1096
|
-
surface.
|
|
1097
|
-
|
|
1098
|
-
This exposed one core gap, fixed at the engine:
|
|
1099
|
-
|
|
1100
|
-
- **Non-GObject GObject-fundamentals wrap through their introspected ref/unref,
|
|
1101
|
-
not `WrapGObject`** (`src/object.cc` `MakeFundamentalHandle` + the
|
|
1102
|
-
`src/marshal.cc` return branch). `Gtk.Snapshot.to_node()` returns a
|
|
1103
|
-
`GskRenderNode` — introspected as OBJECT_INFO but a GObject FUNDAMENTAL
|
|
1104
|
-
(`gi_object_info_get_fundamental`), ref-counted via `gsk_render_node_ref/unref`,
|
|
1105
|
-
NOT `g_object_ref`, with `G_IS_OBJECT` FALSE. Routing it through `WrapGObject`
|
|
1106
|
-
ran the toggle-ref/qdata dance on a non-GObject → a cascade of
|
|
1107
|
-
`g_object_*: assertion 'G_IS_OBJECT (object)' failed` criticals AND a leaked ref.
|
|
1108
|
-
It now gets a type-tagged External carrying the raw pointer + the introspected
|
|
1109
|
-
unref func as the finalizer hint (`isFundamentalHandle` / L1 `wrapFundamental`,
|
|
1110
|
-
an opaque round-trippable pass-through) — GParamSpec + GValue keep their
|
|
1111
|
-
dedicated branches, this catches the rest.
|
|
1112
|
-
|
|
1113
|
-
### Windows: the FULL-windowing GTK runtime bundle
|
|
1114
|
-
|
|
1115
|
-
The batteries-included [`@gjsify/gtk-runtime-win32-x64`](../gtk-runtime-win32-x64)
|
|
1116
|
-
bundle has two closures: the DEFAULT display-free set (loadable DLLs + typelibs)
|
|
1117
|
-
and the `--windowing` SUPERSET that adds the runtime DATA a real GTK window needs
|
|
1118
|
-
on Windows — the gdk-pixbuf loaders + `loaders.cache`, compiled GSettings schemas
|
|
1119
|
-
(`gschemas.compiled`), the Adwaita/hicolor icon themes + `icon-theme.cache`, and
|
|
1120
|
-
Fontconfig config/cache. node-gi's loader (`gtk-runtime.js`
|
|
1121
|
-
`maybeWireGtkWindowingEnv`) detects the windowing data via the `gschemas.compiled`
|
|
1122
|
-
marker and wires the env (`GSETTINGS_SCHEMA_DIR` / `GDK_PIXBUF_MODULE_FILE` /
|
|
1123
|
-
`XDG_DATA_DIRS` / `FONTCONFIG_*`); a display-free bundle carries no marker, so the
|
|
1124
|
-
wiring is a strict no-op and that load is byte-unchanged.
|
|
1125
|
-
|
|
1126
|
-
The bundle is not pulled in automatically — `npm install
|
|
1127
|
-
@gjsify/gtk-runtime-win32-x64` alongside node-gi (see
|
|
1128
|
-
[Requirements](#requirements)). `resolveGtkRuntimeBundle()` finds it wherever the
|
|
1129
|
-
package manager placed it: an explicit `GJSIFY_GTK_RUNTIME` dir, a staged
|
|
1130
|
-
`prebuilds/<target>/gtk/`, the sibling monorepo checkout, or
|
|
1131
|
-
`require.resolve('@gjsify/gtk-runtime-<target>')` — the last of which covers a
|
|
1132
|
-
hoisted `node_modules/@gjsify/gtk-runtime-win32-x64`, so no loader change is
|
|
1133
|
-
needed if the dependency edge is ever declared.
|
|
1134
|
-
|
|
1135
|
-
### Adwaita widget breadth realizes + reacts + renders
|
|
1136
|
-
|
|
1137
|
-
Beyond one window's chrome: a representative slice of the REAL Libadwaita widget
|
|
1138
|
-
set constructs, RENDERS and REACTS on node-gi. `test/widgets.test.mjs` builds an
|
|
1139
|
-
`Adw.PreferencesPage` / `Adw.PreferencesGroup` of `Adw.ActionRow`, `Adw.SwitchRow`,
|
|
1140
|
-
`Adw.EntryRow`, `Adw.ComboRow` (a `Gtk.StringList` model), `Adw.SpinRow` (a
|
|
1141
|
-
`Gtk.Adjustment`) and `Adw.ExpanderRow`, plus a `Gtk.ListBox` and a dismissible
|
|
1142
|
-
`Adw.Toast` via an `Adw.ToastOverlay`. Two tiers, robust on a runner whose surface
|
|
1143
|
-
may or may not realize:
|
|
1144
|
-
|
|
1145
|
-
- **DumpTree** — the widget tree contains every expected type (`AdwSwitchRow`,
|
|
1146
|
-
`AdwComboRow`, `AdwEntryRow`, `AdwSpinRow`, `AdwExpanderRow`, `AdwPreferencesPage`,
|
|
1147
|
-
`GtkListBox`, …), read via the runtime GType (`$typeName`) the `@gjsify/devtools`
|
|
1148
|
-
DumpTree uses — so each class constructs + parents correctly through node-gi.
|
|
1149
|
-
- **Interaction** — toggling the switch, changing the combo selection, moving the
|
|
1150
|
-
spin value, expanding the expander and setting the entry text all drive
|
|
1151
|
-
OBSERVABLE property changes AND fire their paired `notify::<prop>` handlers, and a
|
|
1152
|
-
toast add → `dismiss()` fires `::dismissed`. This surfaces the `Gtk.StringList` /
|
|
1153
|
-
`Gtk.Adjustment` model marshalling (GListModel + object construct props),
|
|
1154
|
-
`notify::` property signals and the boxed-model paths — all display-independent, so
|
|
1155
|
-
the interaction + DumpTree tier holds headless on Windows CI.
|
|
1156
|
-
- **Render** — when the surface realizes, the whole preferences surface rasterises
|
|
1157
|
-
through the GSK renderer (a non-empty PNG), the strong proof the broad widget set
|
|
1158
|
-
renders, not just constructs.
|
|
1159
|
-
|
|
1160
|
-
The same widgets back the `showcases/gtk/node-gi-window` "Settings" view (an
|
|
1161
|
-
`Adw.ViewStack` page reachable from the bottom `Adw.ViewSwitcherBar`, beside the
|
|
1162
|
-
counter). No engine change and no windowing-bundle change were needed: the rows are
|
|
1163
|
-
core GTK4/Adwaita widgets backed by the already-bundled `gtk-4-*.dll` / `libadwaita-1`
|
|
1164
|
-
and the full Adwaita icon theme + compiled schemas the `--windowing` bundle already
|
|
1165
|
-
ships.
|
|
1166
|
-
|
|
1167
|
-
Scoped out of the capstone fixture itself (deliberately): the Gst audio
|
|
1168
|
-
DECODE/playback path (`decodeAudioData`, `autoaudiosink`) — construction is
|
|
1169
|
-
proven by the showcase run above, the streaming pipeline is its own follow-up
|
|
1170
|
-
surface.
|
|
177
|
+
npm install # prebuild if one is staged, else node-gyp (scripts/install.mjs)
|
|
178
|
+
npm test # node --test — the full suite on Node, the authoritative one
|
|
179
|
+
npm run test:bun # conformance subset on Bun (needs `bun`)
|
|
180
|
+
npm run test:deno # conformance subset on Deno (needs `deno`)
|
|
181
|
+
npm run test:conformance # the gjs × node × bun × deno golden-diff matrix
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**In a checkout a staged prebuild shadows your local build**, so every `test*`
|
|
185
|
+
script pins `NODE_GI_NATIVE=build`; an ad-hoc probe must too. The full
|
|
186
|
+
contributor picture — the load order and its env vars, the toggle-ref debug
|
|
187
|
+
seams, the golden-diff conformance contract, the GJS installed-tests port, and
|
|
188
|
+
the display capstones with the exact environment each one needs — is
|
|
189
|
+
[docs/node-gi-verification.md](../../../docs/node-gi-verification.md).
|
|
190
|
+
|
|
191
|
+
## Further reading
|
|
192
|
+
|
|
193
|
+
| | |
|
|
194
|
+
|---|---|
|
|
195
|
+
| the GJS-compatible surface, in full | [docs/node-gi-gjs-surface.md](../../../docs/node-gi-gjs-surface.md) |
|
|
196
|
+
| build, test, conformance, display capstones | [docs/node-gi-verification.md](../../../docs/node-gi-verification.md) |
|
|
197
|
+
| cross-runtime, darwin/win32 loading, GTK bundles | [docs/node-gi-platform-notes.md](../../../docs/node-gi-platform-notes.md) |
|
|
198
|
+
| the engineering rules for this tree | [packages/node-gi/AGENTS.md](../AGENTS.md) |
|
|
199
|
+
| scope + tier decision | [ADR 0005](../../../docs/adr/0005-node-gi-scope.md) |
|