@gjsify/node-gi 0.45.0 → 0.47.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
@@ -35,9 +35,10 @@ vendored as-is — gjsify ships its own dual (GJS + Node) example/test infra.
35
35
 
36
36
  - **Node.js ≥ 20, Bun ≥ 1.3, or Deno ≥ 2** — the engine is a Node-API addon, so
37
37
  ONE binary serves all three (see [Runtimes](#runtimes-node--bun--deno)).
38
- - **No C++ toolchain on the four prebuilt targets.** The published tarball ships
39
- `prebuilds/<platform>-<arch>/node_gi.node` for `linux-x64`, `linux-arm64`,
40
- `darwin-arm64` and `win32-x64` (`package.json#gjsify.platforms`), and the
38
+ - **No C++ toolchain on a prebuilt target.** The published tarball ships
39
+ `prebuilds/<platform>-<arch>/node_gi.node` for every target
40
+ `package.json#gjsify.platforms` declares — `linux-x64`, `linux-arm64`,
41
+ `darwin-arm64`, `darwin-x64`, `win32-x64` — and the
41
42
  `install` script (`scripts/install.mjs`) uses it instead of building: a source
42
43
  build is the FALLBACK for a host with no prebuild, not the default. Force it
43
44
  with `NODE_GI_BUILD_FROM_SOURCE=1` (or npm's `--build-from-source`); skip it
@@ -84,12 +85,25 @@ npm install @gjsify/gtk-runtime-darwin-arm64 # macOS Apple silicon
84
85
  npm install @gjsify/gtk-runtime-darwin-x64 # macOS Intel
85
86
  ```
86
87
 
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.
88
+ All three declare `os`/`cpu`, so npm/yarn/pnpm skip them off-platform.
89
+
90
+ **This package will not declare one, and that is a decision, not a gap** —
91
+ [ADR 0023 § 1 + § 3](../../../docs/adr/0023-gtk-source-precedence.md). Whoever
92
+ SHIPS an application declares the bundle (for the showcases that is the `gjsify`
93
+ CLI, which adds `@gjsify/gtk-runtime-<target>` to the tree it assembles); node-gi
94
+ declares it in no dependency field at all. The reason is #910: a CI job compiled
95
+ the addon against Homebrew GTK and then ran it on a bundle it had never been
96
+ linked against, producing wrong method entries and a 29-minute timeout. #920
97
+ reverted the dependency and the prohibition was written down. Pairing a
98
+ from-source addon with a bundle is not a preference that came out wrong, it is
99
+ an ABI error, so the bundle cannot arrive by default.
100
+
101
+ What that costs is worth stating rather than discovering: on a Mac or a Windows
102
+ box with **no** system GTK and **no** bundle installed, `@gjsify/node-gi` does
103
+ not load at all — its addon's own link closure (`libgirepository-2.0`,
104
+ `libglib-2.0`, `libcairo`) is unresolvable, and the failure names only the
105
+ `.node` file (#1063). The addon's rpath lists exactly the two places it accepts:
106
+ a sibling `gtk-runtime-<target>/gtk/lib`, and the host's `/usr/local/lib`.
93
107
 
94
108
  ## Usage
95
109
 
package/gi.js CHANGED
@@ -145,6 +145,18 @@ class GLibError extends Error {
145
145
  this.code = code;
146
146
  }
147
147
 
148
+ // g_error_new_literal, which GJS exposes as a static on GLib.Error because it is in
149
+ // `GLib-2.0.gir` — and which is the only spelling `@girs/glib-2.0` TYPES, its
150
+ // `constructor(properties?)` signature rejecting the three positional arguments the
151
+ // constructor above actually takes. So a cross-runtime source that has to build a GError
152
+ // (a test asserting on an error a host cannot produce, say a CoreText font map's
153
+ // G_IO_ERROR_NOT_SUPPORTED) type-checks only against this, and without it the gjs leg
154
+ // passes and the node leg dies with `Error.new_literal is not a function`. Same arguments,
155
+ // same result — the constructor is the implementation.
156
+ static new_literal(domain, code, message) {
157
+ return new GLibError(domain, code, message);
158
+ }
159
+
148
160
  // g_error_matches: true when the error's domain AND code both match.
149
161
  matches(domain, code) {
150
162
  const d = errorDomainOf(domain);
@@ -166,7 +178,12 @@ function buildGError(domainName, domainQuark, code, message) {
166
178
  error.domainQuark = domainQuark;
167
179
  return error;
168
180
  }
169
- native.setErrorBuilder(buildGError);
181
+ // The CLASS is registered with the builder, because the engine needs it the other
182
+ // way round too: a `GError`-typed IN argument recognises a caught (or `new`-ed)
183
+ // GLib.Error by `instanceof` and rebuilds a real GError from its fields, so an
184
+ // application can hand back the error it is holding (`GLib.propagate_error`,
185
+ // `Gst.Message.new_error`). One call, so builder and class cannot drift apart.
186
+ native.setErrorBuilder(buildGError, GLibError);
170
187
 
171
188
  // Stamps a makeClass prototype with its { namespace, typeName } so Gio._promisify can
172
189
  // record WHICH introspected class a registration belongs to — matched against the
@@ -1408,6 +1425,28 @@ function defineLazyGType(ctor, namespace, typeName) {
1408
1425
  });
1409
1426
  }
1410
1427
 
1428
+ // The GType a static call's `this` names, or `null` when the receiver names none —
1429
+ // a primitive, a plain object, a raw (unregistered) subclass. `null` means "no
1430
+ // receiver", and the engine then runs the method on the type the name was read from,
1431
+ // which is the pre-#1438 behaviour for every non-class-struct call.
1432
+ //
1433
+ // A class answers its own `$gtype`. An INSTANCE has none — `$gtype` is a
1434
+ // constructor-level member here — but gjs answers the instance's RUNTIME class for
1435
+ // it, and the difference is observable: measured, `GObject.Object.list_properties`
1436
+ // borrowed onto an instance of a registerClass'd subclass reads the SUBCLASS's 6
1437
+ // pspecs under gjs and read GObject's 0 here. So resolve it through the type system,
1438
+ // the route `Symbol.hasInstance` above already takes.
1439
+ function gtypeOf(receiver) {
1440
+ if (receiver === null || receiver === undefined) return null;
1441
+ if (typeof receiver !== 'function' && typeof receiver !== 'object') return null;
1442
+ const gt = receiver.$gtype;
1443
+ if (gt !== undefined) return gt;
1444
+ const handle = receiver[HANDLE];
1445
+ if (handle === undefined || !native.isGObjectHandle(handle)) return null;
1446
+ const G = requireGi('GObject', '2.0');
1447
+ return G.type_from_name(native.getTypeName(handle));
1448
+ }
1449
+
1411
1450
  // The GObject behind a prototype method's `this`. A prototype function is
1412
1451
  // reachable without an instance (`Cls.prototype.m()`, a detached reference), so
1413
1452
  // the engine must never be handed `undefined` for the instance argument.
@@ -1646,7 +1685,20 @@ function makeClass(namespace, typeName) {
1646
1685
  return objectStaticConstructor(prop);
1647
1686
  }
1648
1687
  const giName = camelToSnake(prop);
1649
- return (...args) => wrapReturn(native.callStaticMethod(namespace, typeName, giName, unwrapArgs(args)));
1688
+ // NOT an arrow: a class-struct method (GObjectClass's `list_properties`,
1689
+ // `find_property`, …) runs on the class the call went THROUGH, so `this` is
1690
+ // load-bearing and an arrow would discard it (#1438). What the engine does
1691
+ // with it — and why a plain static and an incompatible receiver ignore it —
1692
+ // is `CallStaticMethod`'s header in `src/calls.cc`.
1693
+ return function (...args) {
1694
+ // Short-circuit the direct `Ns.Class.method()` call: the engine would
1695
+ // compare the receiver against the same GType and ignore it anyway, and
1696
+ // the read would arm the lazy `$gtype` getter for nothing.
1697
+ const receiverGType = this === proxy ? null : gtypeOf(this);
1698
+ return wrapReturn(
1699
+ native.callStaticMethod(namespace, typeName, giName, unwrapArgs(args), receiverGType),
1700
+ );
1701
+ };
1650
1702
  },
1651
1703
  });
1652
1704
  // Replacing ctor.prototype dropped the automatic back-link; restore it pointing at
@@ -1728,6 +1780,17 @@ function makeEnum(namespace, typeName) {
1728
1780
  if (domain !== null) {
1729
1781
  Object.defineProperty(out, ERROR_DOMAIN, { value: domain, enumerable: false });
1730
1782
  }
1783
+ // `Ns.Enum.$gtype` — gjs answers a registered G_TYPE_ENUM/G_TYPE_FLAGS here, so
1784
+ // `GObject.type_name(Gio.BusType.$gtype)` is `GBusType` there and was `null` over
1785
+ // the bridge: an enum object carried no `$gtype` at all. EAGER rather than
1786
+ // defineLazyGType, because the object is frozen on the way out and a
1787
+ // self-redefining getter could never install its cache. Non-enumerable, so
1788
+ // `Object.keys` stays the member list; omitted entirely for an UNREGISTERED enum,
1789
+ // which has no GType and where gjs answers `undefined` too.
1790
+ const gtype = native.getGType(namespace, typeName);
1791
+ if (gtype !== null) {
1792
+ Object.defineProperty(out, '$gtype', { value: gtype, enumerable: false });
1793
+ }
1731
1794
  return Object.freeze(out);
1732
1795
  }
1733
1796
 
package/gtk-runtime.js CHANGED
@@ -460,6 +460,20 @@ export function maybeWireGtkWindowingEnv() {
460
460
  const scanner = join(bundle.dir, 'libexec', 'gstreamer-1.0', gstScannerLeaf());
461
461
  if (existsSync(scanner)) setIfUnset('GST_PLUGIN_SCANNER', scanner);
462
462
  }
463
+
464
+ // The GIO modules, and specifically the TLS backend. `GTlsConnection` has no
465
+ // implementation in GIO itself — glib-networking ships one as a module GIO
466
+ // g_module_opens out of ONE directory, whose compiled-in default is the BUILD
467
+ // machine's prefix. On a user's box that path is absent (or, worse, a foreign
468
+ // glib-networking against a different GLib), so a bundle-activated process fell back
469
+ // to the dummy backend and every https request failed: `souphttpsrc` reports
470
+ // "Internal data stream error", @gjsify/{tls,http2,ws} fail the same way one layer up.
471
+ //
472
+ // GIO_MODULE_DIR, not GIO_EXTRA_MODULES, for GST_PLUGIN_SYSTEM_PATH's reason: it
473
+ // REPLACES the compiled-in default rather than adding to it, and mixing a host's
474
+ // modules into the bundle's own GIO is the case we are avoiding, not one to preserve.
475
+ const gioModules = join(bundle.dir, 'lib', 'gio', 'modules');
476
+ if (existsSync(gioModules)) setIfUnset('GIO_MODULE_DIR', gioModules);
463
477
  }
464
478
 
465
479
  /** The plugin scanner's leaf name — the bundles ship the same tree on both OSes. */
package/index.d.ts CHANGED
@@ -57,10 +57,15 @@ export function getErrorDomain(namespace: string, name: string): { name: string;
57
57
 
58
58
  /**
59
59
  * Register the L1 GLib.Error factory the engine calls when a GI invoke fails, so
60
- * a failed sync call throws a real `GLib.Error` (instanceof, with `.matches()`).
60
+ * a failed sync call throws a real `GLib.Error` (instanceof, with `.matches()`),
61
+ * together with the CLASS it builds — which a `GError`-typed IN argument uses to
62
+ * recognise such an error coming back and rebuild a real `GError` from its
63
+ * fields. Without the class that direction accepts only a marshalled (boxed)
64
+ * error, so a caught one cannot be handed on.
61
65
  */
62
66
  export function setErrorBuilder(
63
67
  builder: (domainName: string, domainQuark: number, code: number, message: string) => Error,
68
+ errorClass?: new (domain: unknown, code: number, message: string) => Error,
64
69
  ): void;
65
70
 
66
71
  /** Prepend a directory to the GIRepository typelib search path. */
@@ -387,6 +392,14 @@ export function setBoxedField(handle: BoxedHandle, name: string, value: unknown)
387
392
  * registered GType. Lets L1 attach type-specific conveniences (GLib.Bytes.toArray).
388
393
  */
389
394
  export function boxedTypeName(value: unknown): string | null;
395
+ /**
396
+ * TEST-ONLY: the address a boxed handle wraps, as a hex string (`null` for anything
397
+ * else). Lets a test assert an IN-transfer ownership rule — "a transfer-full boxed
398
+ * IN arg is handed a COPY" — directly, instead of running the call many times and
399
+ * waiting for a double free that only aborts when the allocator has not recycled
400
+ * the block. Nothing on the runtime path may branch on it.
401
+ */
402
+ export function __boxedAddress(value: unknown): string | null;
390
403
 
391
404
  /**
392
405
  * Opaque handle to a GObject.ParamSpec (a GObject fundamental, ref-counted via
@@ -604,6 +617,7 @@ declare const native: {
604
617
  getBoxedField: typeof getBoxedField;
605
618
  setBoxedField: typeof setBoxedField;
606
619
  boxedTypeName: typeof boxedTypeName;
620
+ __boxedAddress: typeof __boxedAddress;
607
621
  isParamSpecHandle: typeof isParamSpecHandle;
608
622
  paramSpecProp: typeof paramSpecProp;
609
623
  variantNew: typeof variantNew;
package/index.js CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  maybeReexecForGtkRuntime,
17
17
  maybeWireGtkWindowingEnv,
18
18
  } from './gtk-runtime.js';
19
+ import { activateNativePrebuilds } from './native-prebuilds.js';
19
20
 
20
21
  // macOS bundled GTK: dyld reads DYLD_FALLBACK_LIBRARY_PATH only at launch, and GI
21
22
  // needs it to g_module_open typelib backers (get_type / Pango / Gdk / Graphene) by
@@ -124,6 +125,18 @@ try {
124
125
  // Never fatal: a missing/partial bundle just leaves the host GTK in charge.
125
126
  }
126
127
 
128
+ // Every OTHER staged prebuild typelib (@gjsify/webkit-native and its kin): same
129
+ // env-free treatment, so a namespace whose typelib is installed in node_modules
130
+ // resolves under a plain `node app.mjs` and not only under `gjsify run`. Deliberately
131
+ // AFTER the GTK activation — that one owns the bundle-vs-host policy (ADR 0023) and
132
+ // this one skips gtk-runtime-* for exactly that reason.
133
+ try {
134
+ activateNativePrebuilds(native);
135
+ } catch {
136
+ // Same contract as the calls around it: a failed discovery leaves the search
137
+ // path untouched, and the caller gets GI's own "typelib not found".
138
+ }
139
+
127
140
  // And where the LIBRARIES those typelibs name actually live. Separate from the
128
141
  // call above because it is not about a bundle: it applies to whichever GTK the
129
142
  // policy chose, and it is the only loader repair bun and deno get on macOS —
@@ -231,6 +244,9 @@ export const boxedMemberKind = native.boxedMemberKind;
231
244
  export const getBoxedField = native.getBoxedField;
232
245
  export const setBoxedField = native.setBoxedField;
233
246
  export const boxedTypeName = native.boxedTypeName;
247
+ /** TEST-ONLY: the address a boxed handle wraps, so an ownership rule about pointer
248
+ * IDENTITY can be asserted instead of waited on (see marshal.cc BoxedAddress). */
249
+ export const __boxedAddress = native.__boxedAddress;
234
250
  export const isParamSpecHandle = native.isParamSpecHandle;
235
251
 
236
252
  // True for a non-GObject GObject-fundamental handle (a GskRenderNode from