@gjsify/node-gi 0.22.0 → 0.23.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 CHANGED
@@ -678,6 +678,16 @@ imports.signals.addSignalMethods(emitter); // the pure-JS Signals mixin
678
678
  emitter.connect('ready', () => imports.mainloop.quit());
679
679
  imports.mainloop.timeout_add(50, () => { emitter.emit('ready'); return false; });
680
680
  imports.mainloop.run(); // thin GLib.MainLoop wrapper
681
+
682
+ // imports.byteArray — the legacy byte-array module, with GJS semantics
683
+ // (fromString/toString are zero-terminated + fatal-decode; fromGBytes/toGBytes
684
+ // round-trip GLib.Bytes; fromArray wraps in the legacy ByteArray class). This
685
+ // is the seam @gjsify/utils' cli()/gbytesToUint8Array — and through them
686
+ // @gjsify/os + @gjsify/child_process — read GLib subprocess output with:
687
+ const [ok, out] = imports.gi.GLib.spawn_command_line_sync('echo hi');
688
+ console.log(imports.byteArray.toString(out)); // 'hi\n'
689
+ const bytes = imports.byteArray.toGBytes(Uint8Array.of(1, 2, 3));
690
+ imports.byteArray.fromGBytes(bytes); // Uint8Array [1, 2, 3] (a copy)
681
691
  ```
682
692
 
683
693
  A follow-up `--app node` build step will inject this automatically for any
package/gi.js CHANGED
@@ -985,7 +985,10 @@ let pumpRefCount = 0;
985
985
  * @returns {() => void}
986
986
  */
987
987
  export function startMainContextPump() {
988
- if (native.isNodeRuntime) return () => {};
988
+ // Node: the libuv↔GLib bridge already co-pumps. GJS: the host loop IS
989
+ // GLib's default main context — a JS-timer pump would only iterate the
990
+ // context recursively from within its own dispatch.
991
+ if (native.isNodeRuntime || native.RUNTIME === 'gjs') return () => {};
989
992
  pumpRefCount++;
990
993
  if (pumpTimer === null) {
991
994
  // ~4 ms cadence: low latency without busy-spinning. Each tick drains every
@@ -1011,7 +1014,7 @@ export function startMainContextPump() {
1011
1014
 
1012
1015
  /** Decrement the main-context pump reference count; clears the timer at zero. */
1013
1016
  export function stopMainContextPump() {
1014
- if (native.isNodeRuntime || pumpRefCount === 0) return;
1017
+ if (native.isNodeRuntime || native.RUNTIME === 'gjs' || pumpRefCount === 0) return;
1015
1018
  pumpRefCount--;
1016
1019
  if (pumpRefCount === 0 && pumpTimer !== null) {
1017
1020
  clearInterval(pumpTimer);
package/globals.d.ts CHANGED
@@ -15,6 +15,19 @@ export interface GjsImports {
15
15
  system: SystemModule;
16
16
  /** `imports.gettext` — the `@gjsify/node-gi/gettext` module (no-translation passthrough). */
17
17
  gettext: GettextModule;
18
+ /** `imports.byteArray` — the legacy GJS byte-array module (GJS semantics: zero-terminated, fatal decode). */
19
+ byteArray: {
20
+ ByteArray: new (arg?: number | Uint8Array) => {
21
+ length: number;
22
+ toString(encoding?: string): string;
23
+ toGBytes(): unknown;
24
+ };
25
+ fromArray(array: Iterable<number>): unknown;
26
+ fromGBytes(bytes: unknown): Uint8Array;
27
+ fromString(string: string, encoding?: string): Uint8Array;
28
+ toGBytes(array: Uint8Array): unknown;
29
+ toString(byteArray: Uint8Array, encoding?: string): string;
30
+ };
18
31
  versions: Record<string, unknown>;
19
32
  }
20
33
 
package/globals.js CHANGED
@@ -22,6 +22,7 @@ import cairo from './cairo.js';
22
22
  // Legacy `imports.signals` (the pure-JS Signals mixin) + `imports.mainloop` (the
23
23
  // GLib.MainLoop convenience layer) — the GJS script modules many older sources use.
24
24
  import * as signalsModule from './overrides/_signals.js';
25
+ import { createByteArray } from './overrides/byte-array.js';
25
26
  import { createMainloop } from './overrides/mainloop.js';
26
27
  import { createPackage } from './overrides/package.js';
27
28
 
@@ -109,6 +110,11 @@ function makeImports() {
109
110
  system,
110
111
  gettext,
111
112
  cairo,
113
+ // `imports.byteArray` — the legacy GJS byte-array module
114
+ // (fromGBytes/toGBytes/fromString/toString/fromArray/ByteArray), the seam
115
+ // `@gjsify/utils`' gbytesToUint8Array/cli() read subprocess output through.
116
+ // GLib is only pulled in lazily (toGBytes), so seeding it here is free.
117
+ byteArray: createByteArray(requireGi),
112
118
  signals: { addSignalMethods, _connect, _connectAfter, _disconnect, _emit, _signalHandlerIsConnected, _disconnectAll },
113
119
  // GJS's `imports.searchPath` — `imports.package.init` unshifts the app's
114
120
  // moduledir onto it. Inert on node-gi (ESM + gi://), but present + safe so
package/index.js CHANGED
@@ -49,19 +49,25 @@ const require = createRequire(import.meta.url);
49
49
  const here = dirname(fileURLToPath(import.meta.url)); // package root
50
50
 
51
51
  /**
52
- * Which JS runtime we are on. The Node-API addon loads on all three, but the
52
+ * Which JS runtime we are on. The Node-API addon loads on all four: Node, Bun
53
+ * and Deno implement Node-API natively; under GJS the `@gjsify/napi` shim is
54
+ * the N-API host and the addon is loaded through it (`loadNative` below). The
53
55
  * libuv-backed main-loop bridge (startMainLoop) is Node-only — Deno exports no
54
56
  * libuv symbols and Bun panics on uv_backend_fd — so Bun/Deno use the portable
55
- * GLib-iteration pump instead (see gi.js). Detection order matters: Bun and Deno
56
- * both expose a `process` shim, so probe their own globals first.
57
- * @type {'bun' | 'deno' | 'node'}
57
+ * GLib-iteration pump instead (see gi.js), and GJS needs NEITHER: the host
58
+ * loop already IS GLib's default main context. Detection order matters: GJS is
59
+ * probed first via its own globals (`imports` + `print` — no other runtime
60
+ * defines both), then Bun/Deno (both expose a `process` shim), then Node.
61
+ * @type {'bun' | 'deno' | 'gjs' | 'node'}
58
62
  */
59
63
  export const RUNTIME =
60
- typeof globalThis.Bun !== 'undefined'
61
- ? 'bun'
62
- : typeof globalThis.Deno !== 'undefined'
63
- ? 'deno'
64
- : 'node';
64
+ typeof globalThis.imports !== 'undefined' && typeof globalThis.print === 'function'
65
+ ? 'gjs'
66
+ : typeof globalThis.Bun !== 'undefined'
67
+ ? 'bun'
68
+ : typeof globalThis.Deno !== 'undefined'
69
+ ? 'deno'
70
+ : 'node';
65
71
 
66
72
  /** Whether we are on Node.js (the only runtime with the libuv main-loop bridge). */
67
73
  export const isNodeRuntime = RUNTIME === 'node';
@@ -84,9 +90,37 @@ function nativeCandidates() {
84
90
  return [prebuild, release, debug];
85
91
  }
86
92
 
93
+ // GJS host mode: the addon is loaded through the @gjsify/napi shim, not
94
+ // `require()`. The loader native is `globalThis.__gjsifyNapiLoadAddon`,
95
+ // installed by `GjsifyNapi.init()`; if another consumer (the @gjsify/napi L1)
96
+ // already captured + deleted it, init() re-defines it (idempotent). The
97
+ // GjsifyNapi typelib must be resolvable (GI_TYPELIB_PATH/LD_LIBRARY_PATH →
98
+ // the shim prebuild).
99
+ function gjsLoadAddon(path) {
100
+ let load = globalThis.__gjsifyNapiLoadAddon;
101
+ if (typeof load !== 'function') {
102
+ const GjsifyNapi = globalThis.imports?.gi?.GjsifyNapi;
103
+ if (!GjsifyNapi || !GjsifyNapi.init()) {
104
+ throw new Error(
105
+ '@gjsify/node-gi: GJS host mode needs the @gjsify/napi shim — the GjsifyNapi ' +
106
+ 'typelib is not loadable (point GI_TYPELIB_PATH and LD_LIBRARY_PATH at its prebuild).',
107
+ );
108
+ }
109
+ load = globalThis.__gjsifyNapiLoadAddon;
110
+ if (typeof load !== 'function') {
111
+ throw new Error('@gjsify/node-gi: GjsifyNapi.init() did not install the addon loader.');
112
+ }
113
+ // Capture-then-delete, the same no-side-channel discipline as the L1.
114
+ delete globalThis.__gjsifyNapiLoadAddon;
115
+ }
116
+ return load(path);
117
+ }
118
+
87
119
  function loadNative() {
88
120
  for (const candidate of nativeCandidates()) {
89
- if (existsSync(candidate)) return require(candidate);
121
+ if (existsSync(candidate)) {
122
+ return RUNTIME === 'gjs' ? gjsLoadAddon(candidate) : require(candidate);
123
+ }
90
124
  }
91
125
  throw new Error(
92
126
  `@gjsify/node-gi: native addon not found for ${RUNTIME} on ${process.platform}-${process.arch}. ` +
@@ -94,7 +128,8 @@ function loadNative() {
94
128
  'Run `node-gyp rebuild` in ' +
95
129
  here +
96
130
  ' (requires a C++ toolchain and the girepository-2.0 / glib-2.0 development headers), ' +
97
- 'or install a package build that ships a prebuild for your platform.',
131
+ 'or install a package build that ships a prebuild for your platform. ' +
132
+ 'Under a bundled GJS app (import.meta anchors at the bundle), set NODE_GI_NATIVE to the addon path.',
98
133
  );
99
134
  }
100
135
 
@@ -133,7 +168,10 @@ try {
133
168
  // matching Node's checkpoint and GJS (SpiderMonkey drains the promise-job queue
134
169
  // when the last JS frame exits). Node never registers — its checkpoint already
135
170
  // runs natively, so the Node path is byte-identical to before.
136
- if (!isNodeRuntime) {
171
+ // GJS never registers: SpiderMonkey/GJS drains the promise-job queue from its
172
+ // own main-loop source whenever the last JS frame exits — the exact semantic
173
+ // this checkpoint recreates for Bun/Deno.
174
+ if (!isNodeRuntime && RUNTIME !== 'gjs') {
137
175
  try {
138
176
  let drain = null;
139
177
  if (RUNTIME === 'bun') {
@@ -0,0 +1,197 @@
1
+ // SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
2
+ // SPDX-FileCopyrightText: 2017 Philip Chimento <philip.chimento@gmail.com>
3
+ //
4
+ // Adapted from GJS (refs/gjs/modules/script/byteArray.js + the native
5
+ // refs/gjs/gjs/byteArray.cpp `imports._byteArrayNative` functions). Copyright
6
+ // (c) 2017 Philip Chimento. MIT OR LGPL-2.0-or-later.
7
+ // Modifications: the legacy `imports.byteArray` module ported to
8
+ // @gjsify/node-gi. The native `fromGBytes`/`fromString`/`toString` are
9
+ // reimplemented in JS over the engine's boxed-GBytes surface and
10
+ // TextEncoder/TextDecoder, preserving GJS semantics:
11
+ // - toString/fromString are ZERO-TERMINATED (data truncates at the first NUL
12
+ // byte — gjs/text-encoding.cpp `zero_terminated_length`), and decoding is
13
+ // FATAL (invalid sequences throw a TypeError, like gjs's fatal=true).
14
+ // - fromGBytes/fromString return a COPY as a plain Uint8Array carrying the
15
+ // legacy own `toString(encoding)` method gjs tacks on
16
+ // (`define_legacy_tostring`), minus gjs's once-per-callsite deprecation
17
+ // warning (stderr-only noise a byte-golden never sees).
18
+ // - toGBytes routes through `new GLib.Bytes(array)` exactly as the gjs
19
+ // script module does; GLib is loaded lazily so building `imports` never
20
+ // pulls the namespace in.
21
+
22
+ // Truncate a byte array at the first NUL, per gjs's ZERO_TERMINATED handling
23
+ // (gjs/text-encoding.cpp zero_terminated_length).
24
+ function zeroTerminated(u8) {
25
+ const nul = u8.indexOf(0);
26
+ return nul === -1 ? u8 : u8.subarray(0, nul);
27
+ }
28
+
29
+ // gjs routes non-UTF-8 encodings through iconv; Node's TextDecoder (decode
30
+ // side) accepts WHATWG labels which cover the practical iconv set, so decoding
31
+ // just passes the label through. The ENCODE side has no general converter on
32
+ // Node — map the labels gjs consumers actually use onto Buffer encodings and
33
+ // throw a clear error for the rest.
34
+ const ENCODE_LABELS = new Map([
35
+ ['latin1', 'latin1'],
36
+ ['iso-8859-1', 'latin1'],
37
+ ['iso8859-1', 'latin1'],
38
+ ['ascii', 'ascii'],
39
+ ['us-ascii', 'ascii'],
40
+ ['utf-16', 'utf16le'],
41
+ ['utf-16le', 'utf16le'],
42
+ ['utf16le', 'utf16le'],
43
+ ]);
44
+
45
+ function isUtf8Label(encoding) {
46
+ const label = String(encoding).toLowerCase();
47
+ return label === 'utf-8' || label === 'utf8';
48
+ }
49
+
50
+ // The legacy own `toString(encoding)` gjs defines on arrays returned by
51
+ // fromString/fromGBytes/fromArray (byteArray.cpp define_legacy_tostring).
52
+ function defineLegacyToString(u8, toString) {
53
+ Object.defineProperty(u8, 'toString', {
54
+ value(encoding = 'utf-8') {
55
+ return toString(u8, encoding);
56
+ },
57
+ writable: true,
58
+ enumerable: false,
59
+ configurable: true,
60
+ });
61
+ return u8;
62
+ }
63
+
64
+ /**
65
+ * Build the legacy `imports.byteArray` module bound to the L1 backend.
66
+ * @param {(ns: string, version?: string) => Record<string, any>} requireGi
67
+ * @returns {{ ByteArray: any, fromArray: Function, fromGBytes: Function, fromString: Function, toGBytes: Function, toString: Function }}
68
+ */
69
+ export function createByteArray(requireGi) {
70
+ /**
71
+ * @param {Uint8Array} byteArray bytes to decode
72
+ * @param {string} [encoding] a WHATWG/iconv label, default UTF-8
73
+ * @returns {string} the decoded string, stopping at the first NUL byte
74
+ */
75
+ function toString(byteArray, encoding = 'utf-8') {
76
+ // Plain Error, not TypeError — gjs raises these argument failures via
77
+ // gjs_throw (Error); only the GBytes typecheck below is a TypeError.
78
+ if (!(byteArray instanceof Uint8Array)) throw new Error('Argument to decode() must be a Uint8Array');
79
+ const data = zeroTerminated(byteArray);
80
+ if (data.length === 0) return '';
81
+ // fatal:true mirrors gjs's fatal decode (invalid sequences throw).
82
+ return new TextDecoder(encoding, { fatal: true }).decode(data);
83
+ }
84
+
85
+ /**
86
+ * @param {string} string the string to encode
87
+ * @param {string} [encoding] a WHATWG/iconv label, default UTF-8
88
+ * @returns {Uint8Array} the encoded bytes, truncated at the first NUL byte
89
+ */
90
+ function fromString(string, encoding = 'utf-8') {
91
+ if (typeof string !== 'string') throw new Error('Argument to fromString() must be a string');
92
+ let encoded;
93
+ if (isUtf8Label(encoding)) {
94
+ encoded = new TextEncoder().encode(string);
95
+ } else {
96
+ const bufferEncoding = ENCODE_LABELS.get(String(encoding).toLowerCase());
97
+ 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);
100
+ }
101
+ const data = zeroTerminated(encoded);
102
+ // Copy into a fresh plain Uint8Array (gjs returns a standalone buffer, and
103
+ // Buffer.from may hand out a view into Node's shared slab).
104
+ const out = new Uint8Array(data.length);
105
+ out.set(data);
106
+ return defineLegacyToString(out, toString);
107
+ }
108
+
109
+ /**
110
+ * @param {any} bytes a GLib.Bytes boxed handle
111
+ * @returns {Uint8Array} a copy of the bytes as a plain Uint8Array
112
+ */
113
+ function fromGBytes(bytes) {
114
+ // The engine's boxed GBytes proxy carries `toArray()` (gi.js wrapBoxed) —
115
+ // the same convenience gjs adds to GLib.Bytes.prototype. Its absence means
116
+ // the value is not a GBytes, matching gjs's G_TYPE_BYTES typecheck throw.
117
+ if (!bytes || typeof bytes.toArray !== 'function') {
118
+ throw new TypeError('Argument to fromGBytes() must be a GLib.Bytes');
119
+ }
120
+ const data = bytes.toArray();
121
+ // toArray() already returns a copy of the GBytes data; re-view it as a
122
+ // PLAIN Uint8Array (gjs returns Uint8Array, not Node's Buffer subclass).
123
+ const out = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
124
+ return defineLegacyToString(out, toString);
125
+ }
126
+
127
+ /**
128
+ * @param {Uint8Array} array the Uint8Array to convert to GLib.Bytes
129
+ * @returns {any} a GLib.Bytes boxed handle
130
+ */
131
+ function toGBytes(array) {
132
+ if (!(array instanceof Uint8Array)) throw new Error('Argument to ByteArray.toGBytes() must be a Uint8Array');
133
+ const GLib = requireGi('GLib', '2.0');
134
+ return new GLib.Bytes(array);
135
+ }
136
+
137
+ // The legacy ByteArray wrapper class, ported verbatim from
138
+ // refs/gjs/modules/script/byteArray.js (backwards compatibility only).
139
+ const ByteArray = class ByteArray {
140
+ constructor(arg = 0) {
141
+ if (arg instanceof Uint8Array) this._array = arg;
142
+ else this._array = new Uint8Array(arg);
143
+ return new Proxy(this, ByteArray);
144
+ }
145
+
146
+ static get(target, prop, receiver) {
147
+ if (!Number.isNaN(Number.parseInt(prop))) return Reflect.get(target._array, prop);
148
+ return Reflect.get(target, prop, receiver);
149
+ }
150
+
151
+ static set(target, prop, val, receiver) {
152
+ const ix = Number.parseInt(prop);
153
+ if (!Number.isNaN(ix)) {
154
+ if (ix >= target._array.length) {
155
+ const newArray = new Uint8Array(ix + 1);
156
+ newArray.set(target._array);
157
+ target._array = newArray;
158
+ }
159
+ return Reflect.set(target._array, prop, val);
160
+ }
161
+ return Reflect.set(target, prop, val, receiver);
162
+ }
163
+
164
+ get length() {
165
+ return this._array.length;
166
+ }
167
+
168
+ set length(newLength) {
169
+ if (newLength === this._array.length) return;
170
+ if (newLength < this._array.length) {
171
+ this._array = new Uint8Array(this._array.buffer, 0, newLength);
172
+ return;
173
+ }
174
+ const newArray = new Uint8Array(newLength);
175
+ newArray.set(this._array);
176
+ this._array = newArray;
177
+ }
178
+
179
+ toString(encoding = 'UTF-8') {
180
+ return toString(this._array, encoding);
181
+ }
182
+
183
+ toGBytes() {
184
+ return toGBytes(this._array);
185
+ }
186
+ };
187
+
188
+ /**
189
+ * @param {Iterable<number>} array an iterable to convert into a ByteArray wrapper
190
+ * @returns {InstanceType<ByteArray>}
191
+ */
192
+ function fromArray(array) {
193
+ return new ByteArray(Uint8Array.from(array));
194
+ }
195
+
196
+ return { ByteArray, fromArray, fromGBytes, fromString, toGBytes, toString };
197
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/node-gi",
3
- "version": "0.22.0",
3
+ "version": "0.23.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",
@@ -58,6 +58,12 @@
58
58
  "prebuilds"
59
59
  ],
60
60
  "gjsify": {
61
+ "platforms": [
62
+ "linux-x64",
63
+ "linux-arm64",
64
+ "darwin-arm64",
65
+ "win32-x64"
66
+ ],
61
67
  "runtimes": {
62
68
  "gjs": "none",
63
69
  "node": "polyfill",
@@ -79,6 +85,7 @@
79
85
  "test:dbus": "dbus-run-session -- env NODE_GI_NATIVE=build node --test --expose-gc test/dbus.test.mjs test/dbus-async.test.mjs",
80
86
  "test:bun": "node scripts/cross-runtime.mjs bun",
81
87
  "test:conformance": "node scripts/conformance.mjs",
88
+ "test:conformance:gjs-napi": "node scripts/conformance.mjs --runtimes=gjs-napi",
82
89
  "test:gimarshalling": "node scripts/gimarshalling.mjs",
83
90
  "test:deno": "node scripts/cross-runtime.mjs deno",
84
91
  "test:cairo": "NODE_GI_NATIVE=build node --test test/cairo.test.mjs",