@gjsify/resolve-npm 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.
@@ -214,6 +214,23 @@ export const GJS_GLOBALS_MAP = {
214
214
  matchMedia: '@gjsify/dom-elements/register/match-media',
215
215
  location: '@gjsify/dom-elements/register/location',
216
216
  navigator: '@gjsify/dom-elements/register/navigator',
217
+
218
+ // --- Canvas 2D + IFrame + WebGL (GTK/WebKit-backed DOM classes) --------
219
+ //
220
+ // These live in `packages/framework/*` rather than a Web/DOM pillar because
221
+ // their implementations are GTK/Cairo-, WebKit- and Gwebgl/GLArea-bound
222
+ // (ADR 0012). They are DELIBERATELY NOT members of `GJS_GLOBALS_GROUPS.dom`:
223
+ // that group is the coarse `--globals auto,dom` safety net, and pulling
224
+ // these into it would make every `auto,dom` build hard-require
225
+ // `@gjsify/canvas2d` / `@gjsify/iframe` / `@gjsify/webgl` (and, for the
226
+ // second, WebKitGTK) as an installed dependency. Auto-detection injects them
227
+ // only when the identifier is actually referenced in the bundled,
228
+ // tree-shaken output.
229
+ ImageData: '@gjsify/canvas2d/register',
230
+ Path2D: '@gjsify/canvas2d/register',
231
+ HTMLIFrameElement: '@gjsify/iframe/register',
232
+ WebGLRenderingContext: '@gjsify/webgl/register',
233
+ WebGL2RenderingContext: '@gjsify/webgl/register',
217
234
  };
218
235
 
219
236
  /**
@@ -268,6 +285,19 @@ export const GJS_GI_BACKED_REGISTERS = {
268
285
  '@gjsify/webaudio/register': ['Gst', 'GstApp'],
269
286
  '@gjsify/webrtc/register': ['Gst', 'GstWebRTC', 'GstSdp'],
270
287
  '@gjsify/gamepad/register': ['Manette'],
288
+ // Imports `@gjsify/dom-elements/register/canvas` (Gdk/GdkPixbuf/Pango/
289
+ // PangoCairo) plus `@gjsify/canvas2d-core`, which uses the `cairo` foreign
290
+ // struct module.
291
+ '@gjsify/canvas2d/register': ['Gdk', 'GdkPixbuf', 'Pango', 'PangoCairo'],
292
+ '@gjsify/iframe/register': ['WebKit'],
293
+ // The WebGL context classes reach the GL driver through the `gwebgl` Vala
294
+ // bridge shipped as this package's own prebuild, and decode `texImage2D`
295
+ // sources via GdkPixbuf. Gtk/Gdk are NOT listed: they are pulled by
296
+ // `webgl-bridge.ts` (the `Gtk.GLArea` widget), which the register does not
297
+ // import — its chain is the two context classes only. As with the entries
298
+ // above, the always-present base namespaces (GLib) are omitted; only the
299
+ // ones whose absence breaks a GTK-less host are worth naming.
300
+ '@gjsify/webgl/register': ['GdkPixbuf', 'Gwebgl'],
271
301
  };
272
302
 
273
303
  // ─── Browser target ────────────────────────────────────────────────────────
package/lib/index.mjs CHANGED
@@ -11,6 +11,68 @@ export {
11
11
  resetRuntimeAliasesCache,
12
12
  } from './runtime-aliases.mjs';
13
13
 
14
+ import { getDerivedAliasesSync as _getDerivedAliasesSync } from './runtime-aliases.mjs';
15
+
16
+ /**
17
+ * Wrap a bare-specifier alias table so its `@gjsify/<X>` VALUES are passed
18
+ * through the derived per-target slot routing before the caller sees them.
19
+ *
20
+ * ## Why this exists (ADR 0014)
21
+ *
22
+ * The alias layer was designed as a TWO-PASS chain, and `app/browser.ts` says
23
+ * so in its own comment: the curated table maps a bare specifier
24
+ * (`os`, `node:os`) to `@gjsify/<X>`, and "the dynamic per-runtimes-triplet
25
+ * resolver (`getDerivedAliasesSync`) finishes the routing in a second pass".
26
+ *
27
+ * That second pass never fires for a bare specifier. `aliasPlugin` resolves a
28
+ * hit via `this.resolve(target, importer, { skipSelf: true })`, and `skipSelf`
29
+ * deliberately excludes the gjsify alias plugin from the nested resolution —
30
+ * so `os` → `@gjsify/os` lands on the package ROOT and the derived entry
31
+ * `@gjsify/os` → `@gjsify/os/browser` is never consulted. A direct
32
+ * `import … from '@gjsify/os'` in source DOES route (single hop, verified);
33
+ * a browser bundle importing bare `os` does not. Since every browser bundle
34
+ * reaches these packages through the bare/`node:` form, the platform-entry
35
+ * routing of ADR 0014 would have been inert without this.
36
+ *
37
+ * Composing the two maps here — at the point the bare table is materialised —
38
+ * collapses the chain to a single hop, keeps the curated table itself a pure
39
+ * bare-name → package statement (no slot policy duplicated into it), and needs
40
+ * no change in the bundler plugins.
41
+ *
42
+ * Evaluated LAZILY (on property read) and cached: `getDerivedAliasesSync` walks
43
+ * the workspace, and importing `@gjsify/resolve-npm` must not pay for a
44
+ * filesystem scan that a `--app gjs` build never needs. The trap set covers
45
+ * everything a consumer does with these tables — `Object.entries`, spread and
46
+ * direct indexing.
47
+ *
48
+ * @param {Record<string,string>} table
49
+ * @param {'gjs'|'node'|'browser'|'nativescript'} target
50
+ * @returns {Record<string,string>}
51
+ */
52
+ function withDerivedSlotRouting(table, target) {
53
+ /** @type {Record<string,string>|null} */
54
+ let routed = null;
55
+ const materialise = () => {
56
+ if (routed) return routed;
57
+ const derived = _getDerivedAliasesSync(target);
58
+ /** @type {Record<string,string>} */
59
+ const out = {};
60
+ for (const [bare, pkg] of Object.entries(table)) {
61
+ out[bare] = Object.prototype.hasOwnProperty.call(derived, pkg) ? derived[pkg] : pkg;
62
+ }
63
+ routed = out;
64
+ return routed;
65
+ };
66
+ return new Proxy(table, {
67
+ get: (_t, prop) =>
68
+ typeof prop === 'string' ? materialise()[prop] : Reflect.get(table, prop),
69
+ has: (_t, prop) => Reflect.has(materialise(), prop),
70
+ ownKeys: () => Reflect.ownKeys(materialise()),
71
+ getOwnPropertyDescriptor: (_t, prop) =>
72
+ Reflect.getOwnPropertyDescriptor(materialise(), prop),
73
+ });
74
+ }
75
+
14
76
  /** Array of Node.js build in module names */
15
77
  export const EXTERNALS_NODE = [
16
78
  'assert',
@@ -202,7 +264,7 @@ export const ALIASES_NODE_FOR_GJS = {
202
264
  * baubable `@gjsify/{process,buffer,stream,...}` builds. Adopt only when the
203
265
  * per-package browser slots are R1-validated.
204
266
  */
205
- export const ALIASES_NODE_FOR_BROWSER = {
267
+ const ALIASES_NODE_FOR_BROWSER_TABLE = {
206
268
  'assert': '@gjsify/assert',
207
269
  'assert/strict': '@gjsify/assert/strict',
208
270
  'async_hooks': '@gjsify/async_hooks',
@@ -263,6 +325,18 @@ export const ALIASES_NODE_FOR_BROWSER = {
263
325
  'isomorphic-ws': '@gjsify/empty',
264
326
  }
265
327
 
328
+ /**
329
+ * Bare Node-builtin specifier → polyfill / empty-stub mapping for `--app
330
+ * browser`, with ADR-0014 platform-entry routing already applied to the values
331
+ * (`os` → `@gjsify/os/browser` when that package's browser slot is `polyfill`
332
+ * and it exports a `./browser` subpath). See `withDerivedSlotRouting` above for
333
+ * why the two-pass chain has to be collapsed here.
334
+ */
335
+ export const ALIASES_NODE_FOR_BROWSER = withDerivedSlotRouting(
336
+ ALIASES_NODE_FOR_BROWSER_TABLE,
337
+ 'browser',
338
+ );
339
+
266
340
  /**
267
341
  * Bare Node-builtin specifier → polyfill / empty-stub mapping for `--app
268
342
  * nativescript` builds. Mirrors `ALIASES_NODE_FOR_BROWSER` in shape, with
@@ -559,6 +633,14 @@ export const ALIASES_WEB_FOR_NODE = {
559
633
  '@gjsify/dom-elements/register/match-media': '@gjsify/empty',
560
634
  '@gjsify/dom-elements/register/location': '@gjsify/empty',
561
635
  '@gjsify/dom-elements/register/navigator': '@gjsify/empty',
636
+ // Canvas 2D + IFrame + WebGL registers (GTK/Cairo-, WebKit- and
637
+ // Gwebgl/GLArea-backed, ADR 0012) — no-op on Node, exactly like the
638
+ // dom-elements registers above. Only the `@gjsify/*`-qualified form is
639
+ // mirrored: per ADR 0012 no bare-specifier `/register` alias is invented for
640
+ // a framework package.
641
+ '@gjsify/canvas2d/register': '@gjsify/empty',
642
+ '@gjsify/iframe/register': '@gjsify/empty',
643
+ '@gjsify/webgl/register': '@gjsify/empty',
562
644
  '@gjsify/buffer/register': '@gjsify/empty',
563
645
 
564
646
  // xmlhttprequest + DOMParser — no-op on Node
@@ -33,6 +33,32 @@ export const REGISTER_GLOBALS_CLOSURE = {
33
33
  "setTimeout",
34
34
  "structuredClone"
35
35
  ],
36
+ "@gjsify/canvas2d/register": [
37
+ "AbortController",
38
+ "AbortSignal",
39
+ "Blob",
40
+ "Buffer",
41
+ "ByteLengthQueuingStrategy",
42
+ "CountQueuingStrategy",
43
+ "DOMException",
44
+ "File",
45
+ "HTMLCanvasElement",
46
+ "Path2D",
47
+ "ReadableStream",
48
+ "TextDecoderStream",
49
+ "TextEncoderStream",
50
+ "TransformStream",
51
+ "WritableStream",
52
+ "atob",
53
+ "btoa",
54
+ "clearTimeout",
55
+ "document",
56
+ "performance",
57
+ "process",
58
+ "queueMicrotask",
59
+ "setTimeout",
60
+ "structuredClone"
61
+ ],
36
62
  "@gjsify/dom-elements/register/canvas": [
37
63
  "AbortController",
38
64
  "AbortSignal",
@@ -43,6 +69,7 @@ export const REGISTER_GLOBALS_CLOSURE = {
43
69
  "DOMException",
44
70
  "File",
45
71
  "HTMLCanvasElement",
72
+ "Path2D",
46
73
  "ReadableStream",
47
74
  "TextDecoderStream",
48
75
  "TextEncoderStream",
@@ -68,6 +95,7 @@ export const REGISTER_GLOBALS_CLOSURE = {
68
95
  "DOMException",
69
96
  "File",
70
97
  "HTMLCanvasElement",
98
+ "Path2D",
71
99
  "ReadableStream",
72
100
  "TextDecoderStream",
73
101
  "TextEncoderStream",
@@ -115,6 +143,7 @@ export const REGISTER_GLOBALS_CLOSURE = {
115
143
  "DOMException",
116
144
  "File",
117
145
  "HTMLCanvasElement",
146
+ "Path2D",
118
147
  "ReadableStream",
119
148
  "TextDecoderStream",
120
149
  "TextEncoderStream",
@@ -262,6 +291,33 @@ export const REGISTER_GLOBALS_CLOSURE = {
262
291
  "setTimeout",
263
292
  "structuredClone"
264
293
  ],
294
+ "@gjsify/iframe/register": [
295
+ "AbortController",
296
+ "AbortSignal",
297
+ "Blob",
298
+ "Buffer",
299
+ "ByteLengthQueuingStrategy",
300
+ "CountQueuingStrategy",
301
+ "DOMException",
302
+ "File",
303
+ "HTMLCanvasElement",
304
+ "HTMLIFrameElement",
305
+ "Path2D",
306
+ "ReadableStream",
307
+ "TextDecoderStream",
308
+ "TextEncoderStream",
309
+ "TransformStream",
310
+ "WritableStream",
311
+ "atob",
312
+ "btoa",
313
+ "clearTimeout",
314
+ "document",
315
+ "performance",
316
+ "process",
317
+ "queueMicrotask",
318
+ "setTimeout",
319
+ "structuredClone"
320
+ ],
265
321
  "@gjsify/node-globals/register/buffer": [
266
322
  "AbortController",
267
323
  "AbortSignal",
@@ -489,6 +545,33 @@ export const REGISTER_GLOBALS_CLOSURE = {
489
545
  "setTimeout",
490
546
  "structuredClone"
491
547
  ],
548
+ "@gjsify/webgl/register": [
549
+ "AbortController",
550
+ "AbortSignal",
551
+ "Blob",
552
+ "Buffer",
553
+ "ByteLengthQueuingStrategy",
554
+ "CountQueuingStrategy",
555
+ "DOMException",
556
+ "File",
557
+ "HTMLCanvasElement",
558
+ "Path2D",
559
+ "ReadableStream",
560
+ "TextDecoderStream",
561
+ "TextEncoderStream",
562
+ "TransformStream",
563
+ "WebGL2RenderingContext",
564
+ "WritableStream",
565
+ "atob",
566
+ "btoa",
567
+ "clearTimeout",
568
+ "document",
569
+ "performance",
570
+ "process",
571
+ "queueMicrotask",
572
+ "setTimeout",
573
+ "structuredClone"
574
+ ],
492
575
  "@gjsify/webrtc/register/data-channel": [
493
576
  "AbortController",
494
577
  "AbortSignal",
@@ -831,11 +914,8 @@ export const REGISTER_GLOBALS_CLOSURE = {
831
914
  "CountQueuingStrategy",
832
915
  "DOMException",
833
916
  "DecompressionStream",
834
- "Event",
835
- "EventTarget",
836
917
  "File",
837
918
  "Headers",
838
- "ProgressEvent",
839
919
  "ReadableStream",
840
920
  "Request",
841
921
  "Response",
@@ -16,7 +16,9 @@
16
16
  // The bundler's alias resolver consults this triplet when routing bare
17
17
  // `@gjsify/<X>` specifiers per `--app <target>`:
18
18
  //
19
- // slot=polyfill → keep as-is (`@gjsify/<X>` — our impl ships)
19
+ // slot=polyfill → `@gjsify/<X>/<target>` when the package's `exports` map
20
+ // declares that platform-entry subpath (ADR 0014), else keep
21
+ // as-is (`@gjsify/<X>` — the shared impl ships)
20
22
  // slot=native → redirect to `@gjsify/<X>/globals` (re-exports native value)
21
23
  // slot=partial → keep as-is (our impl, gracefully degrades at runtime)
22
24
  // slot=none → no rewrite (the package's polyfill resolves normally;
@@ -32,6 +34,30 @@
32
34
  // When a slot=native package is missing the corresponding `globals.mjs`
33
35
  // re-export file, the resolver emits a warn-once log and falls back to
34
36
  // `@gjsify/empty` (the current behavior) — surfacing the gap is desirable.
37
+ //
38
+ // ── Platform-entry routing (ADR 0014) ──────────────────────────────────────
39
+ //
40
+ // `polyfill` is the STRONG promise: "our implementation fully covers this API
41
+ // on this runtime". A package can only keep that promise off GJS if the code
42
+ // the target actually resolves is free of GLib/Gio. Several packages already
43
+ // ship a real per-target implementation (`src/browser.ts` → the `./browser`
44
+ // export subpath) but nothing ever routed to it: the root `.` export has no
45
+ // `"browser"` condition and the alias layer kept `polyfill` as a no-op, so the
46
+ // GJS body was bundled for the browser and every GLib call TypeError'd against
47
+ // the `{}` that `gjsImportsEmptyPlugin` substitutes for `@girs/*`.
48
+ //
49
+ // So: on a NON-gjs target, `slot=polyfill` routes to `@gjsify/<X>/<target>`
50
+ // whenever the package's `exports` map declares that subpath. The `gjs` target
51
+ // is never rerouted (`src/index.ts` IS the GJS implementation).
52
+ //
53
+ // `partial` deliberately does NOT route. `partial` is the weak promise ("works
54
+ // here, degrades") and the shared body's degradation IS its contract; the
55
+ // `partial` packages' platform entries are also not yet at export parity with
56
+ // their root entry (`@gjsify/fs`'s `src/browser.ts` is missing 34 value
57
+ // exports), so routing them would turn a call-time degradation into a
58
+ // build-time MISSING_EXPORT. Reaching parity is exactly the work that promotes
59
+ // a package from `partial` to `polyfill`; `scripts/audit-runtimes.mjs`
60
+ // (`platform-entry-parity` probe) gates that promotion.
35
61
 
36
62
  import { readdir, readFile } from 'node:fs/promises';
37
63
  import { existsSync, readdirSync, readFileSync } from 'node:fs';
@@ -41,6 +67,28 @@ import { fileURLToPath } from 'node:url';
41
67
  const VALID_SLOTS = new Set(['polyfill', 'native', 'partial', 'none']);
42
68
  const VALID_TARGETS = new Set(['gjs', 'node', 'browser', 'nativescript']);
43
69
 
70
+ /**
71
+ * Collect the target names for which `exports` declares a `./<target>`
72
+ * platform-entry subpath. Shared by the sync + async package ingesters so the
73
+ * two paths cannot drift.
74
+ *
75
+ * @param {unknown} exportsField
76
+ * @returns {Set<string>}
77
+ */
78
+ function collectPlatformEntries(exportsField) {
79
+ /** @type {Set<string>} */
80
+ const found = new Set();
81
+ if (!exportsField || typeof exportsField !== 'object') return found;
82
+ for (const target of VALID_TARGETS) {
83
+ // The `gjs` target is never a platform entry — `.` IS the GJS impl.
84
+ if (target === 'gjs') continue;
85
+ if (Object.prototype.hasOwnProperty.call(exportsField, `./${target}`)) {
86
+ found.add(target);
87
+ }
88
+ }
89
+ return found;
90
+ }
91
+
44
92
  /** @typedef {'polyfill'|'native'|'partial'|'none'} Slot */
45
93
  /**
46
94
  * Per-package runtime slot declaration. Quadruplet (gjs / node / browser /
@@ -50,7 +98,14 @@ const VALID_TARGETS = new Set(['gjs', 'node', 'browser', 'nativescript']);
50
98
  *
51
99
  * @typedef {{gjs?:Slot, node?:Slot, browser?:Slot, nativescript?:Slot}} RuntimeTriplet
52
100
  */
53
- /** @typedef {{name:string, dir:string, runtimes:RuntimeTriplet, hasGlobals:boolean}} PackageRecord */
101
+ /**
102
+ * @typedef {{name:string, dir:string, runtimes:RuntimeTriplet, hasGlobals:boolean,
103
+ * platformEntries:Set<string>}} PackageRecord
104
+ *
105
+ * `platformEntries` holds the target names (`browser` / `nativescript` / …)
106
+ * for which the package's `exports` map declares a `./<target>` subpath — the
107
+ * per-runtime implementation entry that `slot=polyfill` routes to (ADR 0014).
108
+ */
54
109
 
55
110
  let _cache = null;
56
111
  const _warned = new Set();
@@ -145,7 +200,8 @@ async function ingestPackageDir(dir, out) {
145
200
  }
146
201
  }
147
202
  const hasGlobals = existsSync(join(dir, 'globals.mjs'));
148
- out.set(name, { name, dir, runtimes: triplet, hasGlobals });
203
+ const platformEntries = collectPlatformEntries(json?.exports);
204
+ out.set(name, { name, dir, runtimes: triplet, hasGlobals, platformEntries });
149
205
  }
150
206
  } catch {
151
207
  // Ignore unreadable / invalid package.json
@@ -180,7 +236,8 @@ function ingestPackageDirSync(dir, out) {
180
236
  }
181
237
  }
182
238
  const hasGlobals = existsSync(join(dir, 'globals.mjs'));
183
- out.set(name, { name, dir, runtimes: triplet, hasGlobals });
239
+ const platformEntries = collectPlatformEntries(json?.exports);
240
+ out.set(name, { name, dir, runtimes: triplet, hasGlobals, platformEntries });
184
241
  }
185
242
  } catch {
186
243
  // Ignore unreadable / invalid package.json
@@ -328,8 +385,21 @@ function resolveSlot(rec, target) {
328
385
  if (!slot) return null; // Slot undeclared → no opinion, leave to hardcoded maps.
329
386
  switch (slot) {
330
387
  case 'polyfill':
388
+ // ADR 0014 — platform-entry routing. `polyfill` promises a FULL
389
+ // implementation on this runtime; when the package ships a
390
+ // per-target entry (`exports["./<target>"]`), that entry IS the
391
+ // implementation for the target and the shared `.` body (which on
392
+ // most packages is the GLib/Gio-backed GJS impl) must not be
393
+ // bundled. Never applies to `gjs` — `.` is the GJS impl.
394
+ if (target !== 'gjs' && rec.platformEntries?.has(target)) {
395
+ return `${rec.name}/${target}`;
396
+ }
397
+ // No platform entry → the shared body is the implementation.
398
+ return null;
331
399
  case 'partial':
332
400
  // Keep as-is — bundler resolves `@gjsify/<X>` to its `lib/esm/index.js`.
401
+ // `partial` does NOT route to a platform entry even when one exists;
402
+ // see the header note (parity is the promotion gate to `polyfill`).
333
403
  return null;
334
404
  case 'native':
335
405
  if (rec.hasGlobals) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/resolve-npm",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "Resolve NPM package aliases",
5
5
  "type": "module",
6
6
  "main": "lib/index.mjs",