@gjsify/node-gi 0.59.0 → 0.60.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/host.node.js ADDED
@@ -0,0 +1,224 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // @gjsify/node-gi — the Node/Bun/Deno/GJS host seam (package.json `imports` → `#host`).
3
+ //
4
+ // Everything in the package that touches a Node builtin or needs the runtime's module
5
+ // system lives behind this one specifier, so a host without them (NativeScript on
6
+ // Android — see host.nativescript.js) swaps ONE file via the `nativescript` condition
7
+ // instead of every importer carrying its own guard. This variant is what index.js and
8
+ // gi.js did inline before the seam existed; behaviour on Node/Bun/Deno/GJS is unchanged.
9
+ import { createRequire } from 'node:module';
10
+ import { existsSync } from 'node:fs';
11
+ import { hostTarget, nativeCandidates, packageRoot } from './native-paths.js';
12
+ import { describeAddonLoadFailure } from './load-diagnostics.js';
13
+ import {
14
+ activateBundledGtkRuntime,
15
+ activateBundledOpenGL,
16
+ activateGiLibraryPath,
17
+ maybePrependGtkRuntimeDllPath,
18
+ maybeReexecForGtkRuntime,
19
+ maybeWireGtkWindowingEnv,
20
+ windowingEnvWrites,
21
+ } from './gtk-runtime.js';
22
+ import { activateNativePrebuilds } from './native-prebuilds.js';
23
+ // From node:timers, not the globals, for two reasons: setImmediate is a Node/Bun global
24
+ // but Deno only exposes it here; and a GJS source running through the bridge routinely
25
+ // replaces the global timers with GLib-backed ones (`@gjsify/node-globals/register`
26
+ // swaps them for `GLib.timeout_add`), which deadlocks the pump — the timer meant to
27
+ // dispatch GLib sources would itself be a GLib source nobody dispatches.
28
+ import { setImmediate, setInterval, clearInterval } from 'node:timers';
29
+ // Same reasoning for `process`: `globalThis.process` may be the `@gjsify/process`
30
+ // polyfill, whose EventEmitter never emits the runtime's 'beforeExit', so the
31
+ // loop-liveness hooks in gi.js would silently never fire.
32
+ import runtimeProcess from 'node:process';
33
+
34
+ export { setImmediate, setInterval, clearInterval, runtimeProcess, windowingEnvWrites };
35
+
36
+ const require = createRequire(import.meta.url);
37
+ const here = packageRoot;
38
+
39
+ /**
40
+ * Which JS runtime we are on. The addon loads on all four: Node/Bun/Deno implement
41
+ * Node-API natively, GJS hosts it through the `@gjsify/napi` shim. The libuv main-loop
42
+ * bridge (startMainLoop) is Node-only — Deno exports no libuv symbols and Bun panics on
43
+ * uv_backend_fd — so Bun/Deno use gi.js's portable GLib-iteration pump, and GJS needs
44
+ * neither (its host loop already IS GLib's default main context). Detection order is
45
+ * load-bearing: GJS first via `imports` + `print` (no other runtime defines both), then
46
+ * Bun/Deno, which both expose a `process` shim.
47
+ * @type {'bun' | 'deno' | 'gjs' | 'node'}
48
+ */
49
+ export const RUNTIME =
50
+ typeof globalThis.imports !== 'undefined' && typeof globalThis.print === 'function'
51
+ ? 'gjs'
52
+ : typeof globalThis.Bun !== 'undefined'
53
+ ? 'bun'
54
+ : typeof globalThis.Deno !== 'undefined'
55
+ ? 'deno'
56
+ : 'node';
57
+
58
+ // GJS host mode: `GjsifyNapi.init()` installs the loader as
59
+ // `globalThis.__gjsifyNapiLoadAddon` and re-installs it if another consumer (the
60
+ // @gjsify/napi L1) already captured + deleted it. Needs the GjsifyNapi typelib
61
+ // resolvable via GI_TYPELIB_PATH/LD_LIBRARY_PATH.
62
+ function gjsLoadAddon(path) {
63
+ let load = globalThis.__gjsifyNapiLoadAddon;
64
+ if (typeof load !== 'function') {
65
+ const GjsifyNapi = globalThis.imports?.gi?.GjsifyNapi;
66
+ if (!GjsifyNapi || !GjsifyNapi.init()) {
67
+ throw new Error(
68
+ '@gjsify/node-gi: GJS host mode needs the @gjsify/napi shim — the GjsifyNapi ' +
69
+ 'typelib is not loadable (point GI_TYPELIB_PATH and LD_LIBRARY_PATH at its prebuild).',
70
+ );
71
+ }
72
+ load = globalThis.__gjsifyNapiLoadAddon;
73
+ if (typeof load !== 'function') {
74
+ throw new Error('@gjsify/node-gi: GjsifyNapi.init() did not install the addon loader.');
75
+ }
76
+ // Capture-then-delete, the same no-side-channel discipline as the L1.
77
+ delete globalThis.__gjsifyNapiLoadAddon;
78
+ }
79
+ return load(path);
80
+ }
81
+
82
+ function loadNative() {
83
+ for (const candidate of nativeCandidates()) {
84
+ if (existsSync(candidate)) {
85
+ try {
86
+ return RUNTIME === 'gjs' ? gjsLoadAddon(candidate) : require(candidate);
87
+ } catch (err) {
88
+ // The addon FILE is here and still would not load — on win32/darwin
89
+ // that means a missing GTK dependency closure, but the raw error
90
+ // names only the .node, in the OS's language, about a file that
91
+ // plainly exists (#1063: `Das angegebene Modul wurde nicht
92
+ // gefunden.` on a present node_gi.node; #994: a missing load-time
93
+ // library surfacing as "Unsupported type void, deriving from
94
+ // fundamental void"). The cause is knowable here, so say it.
95
+ throw new Error(describeAddonLoadFailure(candidate, err), { cause: err });
96
+ }
97
+ }
98
+ }
99
+ throw new Error(
100
+ `@gjsify/node-gi: native addon not found for ${RUNTIME} on ${hostTarget()}. ` +
101
+ `Expected a prebuild at prebuilds/${hostTarget()}/node_gi.node or a local build. ` +
102
+ 'Run `node-gyp rebuild` in ' +
103
+ here +
104
+ ' (requires a C++ toolchain and the girepository-2.0 / glib-2.0 development headers), ' +
105
+ 'or install a package build that ships a prebuild for your platform. ' +
106
+ 'Under a bundled GJS app (import.meta anchors at the bundle), set NODE_GI_NATIVE to the addon path.',
107
+ );
108
+ }
109
+
110
+ /** Whether we are on Node.js (the only runtime with the libuv main-loop bridge). */
111
+ const isNodeRuntime = RUNTIME === 'node';
112
+
113
+ /**
114
+ * Load the addon and run every activation that must precede the first namespace.
115
+ * @returns {Record<string, any>} the native engine
116
+ */
117
+ export function loadNativeHost() {
118
+ // macOS bundled GTK: dyld reads DYLD_FALLBACK_LIBRARY_PATH only at launch, and GI
119
+ // needs it to g_module_open typelib backers (get_type / Pango / Gdk / Graphene) by
120
+ // leaf soname — so the re-exec must happen BEFORE the addon's GTK closure is
121
+ // dlopen'd. Never returns when it re-execs.
122
+ maybeReexecForGtkRuntime();
123
+
124
+ // Windows bundled GTK: Windows re-reads the DLL search path at every LoadLibrary
125
+ // (unlike dyld's launch-time capture), so mutating process.env.PATH in-process
126
+ // before the require() below covers both the static DLL imports and the runtime
127
+ // g_module_open of typelib backers — no re-exec needed.
128
+ maybePrependGtkRuntimeDllPath();
129
+
130
+ // Windows/macOS full-windowing GTK: point GSETTINGS_SCHEMA_DIR / GDK_PIXBUF_MODULE_FILE /
131
+ // XDG_DATA_DIRS / FONTCONFIG_* at the data a real GTK window needs, and PANGOCAIRO_BACKEND
132
+ // at the one backend that reads any of the font half. Strict no-op for the display-free
133
+ // bundle, which therefore loads byte-unchanged. On win32 these writes reach the Win32
134
+ // environment block ONLY — gi.js replays them into the C runtime's copy on the first
135
+ // requireGi(), which is the environment pango's and fontconfig's getenv() read.
136
+ maybeWireGtkWindowingEnv();
137
+ const native = loadNative();
138
+
139
+ // Activate a relocated GTK/GI runtime bundle (@gjsify/gtk-runtime-<os>-<arch> or a
140
+ // staged prebuilds/<os>-<arch>/gtk/) BEFORE any namespace is required: it prepends the
141
+ // bundle's typelib dir to the GIRepository search path env-free, so gi:// namespaces
142
+ // load with no Homebrew/system GTK. Darwin + win32 only; without a bundle the addon
143
+ // uses the host GTK as before.
144
+ try {
145
+ activateBundledGtkRuntime(native);
146
+ } catch {
147
+ // Never fatal: a missing/partial bundle just leaves the host GTK in charge.
148
+ }
149
+
150
+ // win32: decide whether the bundle's Mesa opengl32.dll or the host's OpenGL serves GTK, and
151
+ // preload the bundled one when it wins. Here because gtk-4-1.dll imports OPENGL32 statically,
152
+ // so it must happen before any namespace loads GTK. Reports rather than throws.
153
+ activateBundledOpenGL(native);
154
+
155
+ // Every OTHER staged prebuild typelib (@gjsify/webkit-native and its kin): same
156
+ // env-free treatment, so a namespace whose typelib is installed in node_modules
157
+ // resolves under a plain `node app.mjs` and not only under `gjsify run`. Deliberately
158
+ // AFTER the GTK activation — that one owns the bundle-vs-host policy (ADR 0023) and
159
+ // this one skips gtk-runtime-* for exactly that reason.
160
+ try {
161
+ activateNativePrebuilds(native);
162
+ } catch {
163
+ // Same contract as the calls around it: a failed discovery leaves the search
164
+ // path untouched, and the caller gets GI's own "typelib not found".
165
+ }
166
+
167
+ // And where the LIBRARIES those typelibs name actually live. Separate from the
168
+ // call above because it is not about a bundle: it applies to whichever GTK the
169
+ // policy chose, and it is the only loader repair bun and deno get on macOS —
170
+ // the re-exec above is Node-shaped and skips them.
171
+ try {
172
+ activateGiLibraryPath(native);
173
+ } catch {
174
+ // Same contract as above: never fatal.
175
+ }
176
+
177
+ // Cross-runtime microtask checkpoint (Bun/Deno only). Node's napi_make_callback runs
178
+ // the nextTick + microtask checkpoint when its callback scope closes, so promise
179
+ // continuations queued by a loop-dispatched GLib→JS callback drain at the callback
180
+ // boundary even while a blocking GLib.MainLoop.run()/Gio.Application.run() owns the
181
+ // thread; GJS gets the same from SpiderMonkey draining the promise-job queue when the
182
+ // last JS frame exits. Bun and Deno checkpoint NOWHERE there (Deno's
183
+ // napi_make_callback is a plain Function::Call), and with their event loop paused for
184
+ // the blocking run's lifetime the queue never drained: an async DBus method handler
185
+ // never sent its reply and the client timed out, while sync handlers worked. So
186
+ // register the runtime's own drain primitive; the loop-dispatched trampolines
187
+ // (src/signals.cc / calls.cc / class.cc) invoke it after each OUTERMOST callback
188
+ // returns (NodeGiMaybeDrainMicrotasks).
189
+ if (!isNodeRuntime && RUNTIME !== 'gjs') {
190
+ try {
191
+ let drain = null;
192
+ if (RUNTIME === 'bun') {
193
+ // JSC: drains the VM's microtask queue; callable mid-stack by design
194
+ // (Bun's own processTicksAndRejections calls it from JS).
195
+ ({ drainMicrotasks: drain } = require('bun:jsc'));
196
+ } else if (typeof globalThis.Deno?.internal === 'symbol') {
197
+ // V8: the checkpoint needs BOTH queues. core.runMicrotasks is only
198
+ // Isolate::PerformMicrotaskCheckpoint, while Deno's node-compat
199
+ // process.nextTick queue is a SEPARATE deno_core queue that only the
200
+ // runtime's own event loop drains — and that loop is paused for the whole
201
+ // lifetime of a blocking run(). node:stream delivers 'end' via nextTick
202
+ // (endReadableNT), so a microtask-only drain got body chunks but never the
203
+ // end: every asset load hung at readyState 3 forever on Deno while the SAME
204
+ // bundle settled on Bun, whose nextTick rides JSC's microtask queue.
205
+ // core.runNextTicks covers both queues in one call, like node's
206
+ // task_queues.js. Regression: test/blocking-run-checkpoint.test.mjs.
207
+ const core = globalThis.Deno[globalThis.Deno.internal]?.core;
208
+ if (typeof core?.runNextTicks === 'function') {
209
+ drain = () => core.runNextTicks();
210
+ } else if (typeof core?.runMicrotasks === 'function') {
211
+ // Older deno_core without runNextTicks: promise continuations
212
+ // settle, nextTick consumers like node:stream 'end' still starve.
213
+ drain = () => core.runMicrotasks();
214
+ }
215
+ }
216
+ if (typeof drain === 'function') native.setMicrotaskDrain(drain);
217
+ } catch {
218
+ // Unregistered ⇒ promise continuations drain only when the runtime's own loop
219
+ // runs (startMainContextPump), so async DBus replies inside a BLOCKING run stay
220
+ // unavailable. Never fatal — the sync surface is unaffected.
221
+ }
222
+ }
223
+ return native;
224
+ }
package/index.js CHANGED
@@ -5,203 +5,19 @@
5
5
  // a native package's JS entry is a loader, not a tsc artifact, and the repo
6
6
  // ignores `lib/`. The addon is Node-API (ABI-stable), so the same binary loads on
7
7
  // Node, Bun and Deno; runtime-specific behaviour is gated off RUNTIME below.
8
- import { createRequire } from 'node:module';
9
- import { existsSync } from 'node:fs';
10
- import { hostTarget, nativeCandidates, packageRoot } from './native-paths.js';
11
- import { describeAddonLoadFailure } from './load-diagnostics.js';
12
- import {
13
- activateBundledGtkRuntime,
14
- activateBundledOpenGL,
15
- activateGiLibraryPath,
16
- maybePrependGtkRuntimeDllPath,
17
- maybeReexecForGtkRuntime,
18
- maybeWireGtkWindowingEnv,
19
- } from './gtk-runtime.js';
20
- import { activateNativePrebuilds } from './native-prebuilds.js';
21
-
22
- // macOS bundled GTK: dyld reads DYLD_FALLBACK_LIBRARY_PATH only at launch, and GI
23
- // needs it to g_module_open typelib backers (get_type / Pango / Gdk / Graphene) by
24
- // leaf soname — so the re-exec must happen BEFORE the addon's GTK closure is
25
- // dlopen'd. Never returns when it re-execs.
26
- maybeReexecForGtkRuntime();
27
-
28
- // Windows bundled GTK: Windows re-reads the DLL search path at every LoadLibrary
29
- // (unlike dyld's launch-time capture), so mutating process.env.PATH in-process
30
- // before the require() below covers both the static DLL imports and the runtime
31
- // g_module_open of typelib backers — no re-exec needed.
32
- maybePrependGtkRuntimeDllPath();
33
-
34
- // Windows/macOS full-windowing GTK: point GSETTINGS_SCHEMA_DIR / GDK_PIXBUF_MODULE_FILE /
35
- // XDG_DATA_DIRS / FONTCONFIG_* at the data a real GTK window needs, and PANGOCAIRO_BACKEND
36
- // at the one backend that reads any of the font half. Strict no-op for the display-free
37
- // bundle, which therefore loads byte-unchanged. On win32 these writes reach the Win32
38
- // environment block ONLY — gi.js replays them into the C runtime's copy on the first
39
- // requireGi(), which is the environment pango's and fontconfig's getenv() read.
40
- maybeWireGtkWindowingEnv();
41
-
42
- const require = createRequire(import.meta.url);
43
- const here = packageRoot;
8
+ import { RUNTIME, loadNativeHost } from '#host';
44
9
 
45
10
  /**
46
- * Which JS runtime we are on. The addon loads on all four: Node/Bun/Deno implement
47
- * Node-API natively, GJS hosts it through the `@gjsify/napi` shim. The libuv main-loop
48
- * bridge (startMainLoop) is Node-only — Deno exports no libuv symbols and Bun panics on
49
- * uv_backend_fd — so Bun/Deno use gi.js's portable GLib-iteration pump, and GJS needs
50
- * neither (its host loop already IS GLib's default main context). Detection order is
51
- * load-bearing: GJS first via `imports` + `print` (no other runtime defines both), then
52
- * Bun/Deno, which both expose a `process` shim.
53
- * @type {'bun' | 'deno' | 'gjs' | 'node'}
11
+ * Which JS runtime we are on. Detection — and why each runtime gets the main-loop
12
+ * treatment it gets — lives at the detection site, behind `#host`.
13
+ * @type {'bun' | 'deno' | 'gjs' | 'node' | 'nativescript'}
54
14
  */
55
- export const RUNTIME =
56
- typeof globalThis.imports !== 'undefined' && typeof globalThis.print === 'function'
57
- ? 'gjs'
58
- : typeof globalThis.Bun !== 'undefined'
59
- ? 'bun'
60
- : typeof globalThis.Deno !== 'undefined'
61
- ? 'deno'
62
- : 'node';
15
+ export { RUNTIME };
63
16
 
64
17
  /** Whether we are on Node.js (the only runtime with the libuv main-loop bridge). */
65
18
  export const isNodeRuntime = RUNTIME === 'node';
66
19
 
67
- // GJS host mode: `GjsifyNapi.init()` installs the loader as
68
- // `globalThis.__gjsifyNapiLoadAddon` and re-installs it if another consumer (the
69
- // @gjsify/napi L1) already captured + deleted it. Needs the GjsifyNapi typelib
70
- // resolvable via GI_TYPELIB_PATH/LD_LIBRARY_PATH.
71
- function gjsLoadAddon(path) {
72
- let load = globalThis.__gjsifyNapiLoadAddon;
73
- if (typeof load !== 'function') {
74
- const GjsifyNapi = globalThis.imports?.gi?.GjsifyNapi;
75
- if (!GjsifyNapi || !GjsifyNapi.init()) {
76
- throw new Error(
77
- '@gjsify/node-gi: GJS host mode needs the @gjsify/napi shim — the GjsifyNapi ' +
78
- 'typelib is not loadable (point GI_TYPELIB_PATH and LD_LIBRARY_PATH at its prebuild).',
79
- );
80
- }
81
- load = globalThis.__gjsifyNapiLoadAddon;
82
- if (typeof load !== 'function') {
83
- throw new Error('@gjsify/node-gi: GjsifyNapi.init() did not install the addon loader.');
84
- }
85
- // Capture-then-delete, the same no-side-channel discipline as the L1.
86
- delete globalThis.__gjsifyNapiLoadAddon;
87
- }
88
- return load(path);
89
- }
90
-
91
- function loadNative() {
92
- for (const candidate of nativeCandidates()) {
93
- if (existsSync(candidate)) {
94
- try {
95
- return RUNTIME === 'gjs' ? gjsLoadAddon(candidate) : require(candidate);
96
- } catch (err) {
97
- // The addon FILE is here and still would not load — on win32/darwin
98
- // that means a missing GTK dependency closure, but the raw error
99
- // names only the .node, in the OS's language, about a file that
100
- // plainly exists (#1063: `Das angegebene Modul wurde nicht
101
- // gefunden.` on a present node_gi.node; #994: a missing load-time
102
- // library surfacing as "Unsupported type void, deriving from
103
- // fundamental void"). The cause is knowable here, so say it.
104
- throw new Error(describeAddonLoadFailure(candidate, err), { cause: err });
105
- }
106
- }
107
- }
108
- throw new Error(
109
- `@gjsify/node-gi: native addon not found for ${RUNTIME} on ${hostTarget()}. ` +
110
- `Expected a prebuild at prebuilds/${hostTarget()}/node_gi.node or a local build. ` +
111
- 'Run `node-gyp rebuild` in ' +
112
- here +
113
- ' (requires a C++ toolchain and the girepository-2.0 / glib-2.0 development headers), ' +
114
- 'or install a package build that ships a prebuild for your platform. ' +
115
- 'Under a bundled GJS app (import.meta anchors at the bundle), set NODE_GI_NATIVE to the addon path.',
116
- );
117
- }
118
-
119
- const native = loadNative();
120
-
121
- // Activate a relocated GTK/GI runtime bundle (@gjsify/gtk-runtime-<os>-<arch> or a
122
- // staged prebuilds/<os>-<arch>/gtk/) BEFORE any namespace is required: it prepends the
123
- // bundle's typelib dir to the GIRepository search path env-free, so gi:// namespaces
124
- // load with no Homebrew/system GTK. Darwin + win32 only; without a bundle the addon
125
- // uses the host GTK as before.
126
- try {
127
- activateBundledGtkRuntime(native);
128
- } catch {
129
- // Never fatal: a missing/partial bundle just leaves the host GTK in charge.
130
- }
131
-
132
- // win32: decide whether the bundle's Mesa opengl32.dll or the host's OpenGL serves GTK, and
133
- // preload the bundled one when it wins. Here because gtk-4-1.dll imports OPENGL32 statically,
134
- // so it must happen before any namespace loads GTK. Reports rather than throws.
135
- activateBundledOpenGL(native);
136
-
137
- // Every OTHER staged prebuild typelib (@gjsify/webkit-native and its kin): same
138
- // env-free treatment, so a namespace whose typelib is installed in node_modules
139
- // resolves under a plain `node app.mjs` and not only under `gjsify run`. Deliberately
140
- // AFTER the GTK activation — that one owns the bundle-vs-host policy (ADR 0023) and
141
- // this one skips gtk-runtime-* for exactly that reason.
142
- try {
143
- activateNativePrebuilds(native);
144
- } catch {
145
- // Same contract as the calls around it: a failed discovery leaves the search
146
- // path untouched, and the caller gets GI's own "typelib not found".
147
- }
148
-
149
- // And where the LIBRARIES those typelibs name actually live. Separate from the
150
- // call above because it is not about a bundle: it applies to whichever GTK the
151
- // policy chose, and it is the only loader repair bun and deno get on macOS —
152
- // the re-exec above is Node-shaped and skips them.
153
- try {
154
- activateGiLibraryPath(native);
155
- } catch {
156
- // Same contract as above: never fatal.
157
- }
158
-
159
- // Cross-runtime microtask checkpoint (Bun/Deno only). Node's napi_make_callback runs
160
- // the nextTick + microtask checkpoint when its callback scope closes, so promise
161
- // continuations queued by a loop-dispatched GLib→JS callback drain at the callback
162
- // boundary even while a blocking GLib.MainLoop.run()/Gio.Application.run() owns the
163
- // thread; GJS gets the same from SpiderMonkey draining the promise-job queue when the
164
- // last JS frame exits. Bun and Deno checkpoint NOWHERE there (Deno's
165
- // napi_make_callback is a plain Function::Call), and with their event loop paused for
166
- // the blocking run's lifetime the queue never drained: an async DBus method handler
167
- // never sent its reply and the client timed out, while sync handlers worked. So
168
- // register the runtime's own drain primitive; the loop-dispatched trampolines
169
- // (src/signals.cc / calls.cc / class.cc) invoke it after each OUTERMOST callback
170
- // returns (NodeGiMaybeDrainMicrotasks).
171
- if (!isNodeRuntime && RUNTIME !== 'gjs') {
172
- try {
173
- let drain = null;
174
- if (RUNTIME === 'bun') {
175
- // JSC: drains the VM's microtask queue; callable mid-stack by design
176
- // (Bun's own processTicksAndRejections calls it from JS).
177
- ({ drainMicrotasks: drain } = require('bun:jsc'));
178
- } else if (typeof globalThis.Deno?.internal === 'symbol') {
179
- // V8: the checkpoint needs BOTH queues. core.runMicrotasks is only
180
- // Isolate::PerformMicrotaskCheckpoint, while Deno's node-compat
181
- // process.nextTick queue is a SEPARATE deno_core queue that only the
182
- // runtime's own event loop drains — and that loop is paused for the whole
183
- // lifetime of a blocking run(). node:stream delivers 'end' via nextTick
184
- // (endReadableNT), so a microtask-only drain got body chunks but never the
185
- // end: every asset load hung at readyState 3 forever on Deno while the SAME
186
- // bundle settled on Bun, whose nextTick rides JSC's microtask queue.
187
- // core.runNextTicks covers both queues in one call, like node's
188
- // task_queues.js. Regression: test/blocking-run-checkpoint.test.mjs.
189
- const core = globalThis.Deno[globalThis.Deno.internal]?.core;
190
- if (typeof core?.runNextTicks === 'function') {
191
- drain = () => core.runNextTicks();
192
- } else if (typeof core?.runMicrotasks === 'function') {
193
- // Older deno_core without runNextTicks: promise continuations
194
- // settle, nextTick consumers like node:stream 'end' still starve.
195
- drain = () => core.runMicrotasks();
196
- }
197
- }
198
- if (typeof drain === 'function') native.setMicrotaskDrain(drain);
199
- } catch {
200
- // Unregistered ⇒ promise continuations drain only when the runtime's own loop
201
- // runs (startMainContextPump), so async DBus replies inside a BLOCKING run stay
202
- // unavailable. Never fatal — the sync surface is unaffected.
203
- }
204
- }
20
+ const native = loadNativeHost();
205
21
 
206
22
  // Straight re-exports of the native engine surface. Each contract — arguments,
207
23
  // ownership, the OUT/INOUT return-tuple convention, the keep-alive rules of the
@@ -0,0 +1,16 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // @nativescript/webpack plugin hook (it discovers this exact file name in every dependency
3
+ // and `require()`s it). Adds the `nativescript` export condition so package.json `imports`
4
+ // (`#host`) resolves to host.nativescript.js; webpack's `target('node')` would otherwise
5
+ // pick the Node host.
6
+ //
7
+ // This package is `"type": "module"`, so the file is ESM: the `module.exports` export name
8
+ // is what `require()` of an ES module hands back as the CommonJS export (Node >= 22.12).
9
+ function hook(webpack) {
10
+ webpack.chainWebpack((config) => {
11
+ // `...` keeps webpack's own defaults (import/require/node/default, per request kind).
12
+ config.resolve.set('conditionNames', ['nativescript', '...']);
13
+ });
14
+ }
15
+
16
+ export { hook as 'module.exports' };
@@ -42,6 +42,23 @@ const ENCODE_LABELS = new Map([
42
42
  ['utf16le', 'utf16le'],
43
43
  ]);
44
44
 
45
+ // Buffer.from(string, 'latin1' | 'ascii' | 'utf16le') for hosts without Buffer: latin1 and
46
+ // ascii keep the low byte of each UTF-16 code unit, utf16le writes each unit little-endian.
47
+ function encodeWithoutBuffer(string, bufferEncoding) {
48
+ if (bufferEncoding === 'utf16le') {
49
+ const out = new Uint8Array(string.length * 2);
50
+ for (let i = 0; i < string.length; i++) {
51
+ const c = string.charCodeAt(i);
52
+ out[2 * i] = c & 0xff;
53
+ out[2 * i + 1] = c >> 8;
54
+ }
55
+ return out;
56
+ }
57
+ const out = new Uint8Array(string.length);
58
+ for (let i = 0; i < string.length; i++) out[i] = string.charCodeAt(i) & 0xff;
59
+ return out;
60
+ }
61
+
45
62
  function isUtf8Label(encoding) {
46
63
  const label = String(encoding).toLowerCase();
47
64
  return label === 'utf-8' || label === 'utf8';
@@ -95,8 +112,13 @@ export function createByteArray(requireGi) {
95
112
  } else {
96
113
  const bufferEncoding = ENCODE_LABELS.get(String(encoding).toLowerCase());
97
114
  if (!bufferEncoding) throw new Error(`Unsupported encoding for fromString(): ${encoding}`);
98
- const buf = Buffer.from(string, bufferEncoding);
99
- encoded = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
115
+ // NativeScript has no Buffer; encodeWithoutBuffer yields the same bytes.
116
+ if (typeof Buffer === 'function') {
117
+ const buf = Buffer.from(string, bufferEncoding);
118
+ encoded = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
119
+ } else {
120
+ encoded = encodeWithoutBuffer(string, bufferEncoding);
121
+ }
100
122
  }
101
123
  const data = zeroTerminated(encoded);
102
124
  // Copy into a fresh plain Uint8Array (gjs returns a standalone buffer, and
package/package.json CHANGED
@@ -1,17 +1,24 @@
1
1
  {
2
2
  "name": "@gjsify/node-gi",
3
- "version": "0.59.0",
3
+ "version": "0.60.0",
4
4
  "description": "GObject-Introspection runtime for Node.js — load gi:// namespaces (GLib/GObject/Gio/…) with GJS-compatible semantics. Vendored + rewritten from node-gtk under MIT.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "gypfile": true,
8
8
  "main": "./index.js",
9
9
  "types": "./index.d.ts",
10
+ "imports": {
11
+ "#host": {
12
+ "nativescript": "./host.nativescript.js",
13
+ "default": "./host.node.js"
14
+ }
15
+ },
10
16
  "exports": {
11
17
  ".": {
12
18
  "types": "./index.d.ts",
13
19
  "default": "./index.js"
14
20
  },
21
+ "./package.json": "./package.json",
15
22
  "./gi": {
16
23
  "types": "./gi.d.ts",
17
24
  "default": "./gi.js"
@@ -39,6 +46,9 @@
39
46
  },
40
47
  "files": [
41
48
  "index.js",
49
+ "host.node.js",
50
+ "host.nativescript.js",
51
+ "nativescript.webpack.js",
42
52
  "index.d.ts",
43
53
  "gi.js",
44
54
  "gi.d.ts",
@@ -86,6 +96,7 @@
86
96
  "install": "node scripts/install.mjs",
87
97
  "build": "node-gyp rebuild",
88
98
  "build:native": "node-gyp rebuild",
99
+ "build:android": "node scripts/build-android.mjs",
89
100
  "build:prebuild": "node-gyp rebuild && node scripts/stage-prebuild.mjs",
90
101
  "rebuild": "node-gyp rebuild",
91
102
  "clean": "node-gyp clean",
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/src/addon.cc CHANGED
@@ -64,6 +64,13 @@ static Napi::Object Init(Napi::Env env, Napi::Object exports) {
64
64
  // Without it the process stays in the C locale, where GNU gettext refuses to
65
65
  // translate; the block in private.cc records what that cost.
66
66
  NodeGiInitProcessLocale();
67
+ // Also before any GLib call: on Android, point GLib's messages at logcat. Without
68
+ // it every g_warning explaining a failed GI/GTK call is written to a stderr that
69
+ // goes to /dev/null (android-log.cc).
70
+ InitAndroidLog(env, exports);
71
+ // Only the export; the bring-up itself runs when the host asks for it, since it
72
+ // dlopens the whole GTK stack (android-gdk.cc).
73
+ InitAndroidGdk(env, exports);
67
74
  // The owner env + its JS/main thread are captured lazily at the first wrap
68
75
  // (EnsureDrainAsync) — NOT here, since Init runs once PER env and a per-env
69
76
  // overwrite would mis-identify the main thread under worker_threads. The cleanup