@gjsify/resolve-npm 0.22.0 → 0.24.1

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,87 @@ 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
+ * A curated table maps a bare specifier (`os`, `node:os`) to `@gjsify/<X>`,
23
+ * while the derived per-runtimes-triplet map (`getDerivedAliasesSync`) maps
24
+ * `@gjsify/os` → `@gjsify/os/browser`. Applying the second to the values of
25
+ * the first is what makes ADR 0014's platform-entry routing reach a browser
26
+ * bundle at all: those bundles hit the packages through the bare/`node:` form,
27
+ * not by package name. (A direct `import … from '@gjsify/os'` routes on its own
28
+ * — it is a plain key hit in the merged map.)
29
+ *
30
+ * ## Why HERE and not as a second pass in `aliasPlugin`
31
+ *
32
+ * This looks like it wants to be a resolver chain — `os` → `@gjsify/os` →
33
+ * `@gjsify/os/browser` — but it must not be one. Slot routing may only be
34
+ * applied to values that CAME FROM a curated/derived table, and by the time
35
+ * `aliasPlugin` sees the map it has been merged flat across four tiers
36
+ * (derived → curated `ALIASES_*` → per-target overrides → user `--alias`,
37
+ * later wins — see `app/*.ts`). Tier information is gone, so a chain there
38
+ * would re-route USER aliases too:
39
+ *
40
+ * - `scripts/node-gi-consumer-harness.mjs` builds `--alias node:<x>=@gjsify/<x>`
41
+ * to put the POLYFILL under test behind a Node builtin. 34 of those
42
+ * polyfills declare `runtimes.node: "native"`, so a second hop hands back
43
+ * `@gjsify/<x>/globals` — Node's own builtin — and the suite measures
44
+ * nothing while reporting green.
45
+ * - Value-level chaining also has no notion of the documented KEY-level
46
+ * "curated wins over derived" precedence. Measured on the composed maps:
47
+ * `gjs` 0 of 114 entries would change, `node` 0 of 148 — and on
48
+ * `nativescript` the only 4 changes are regressions (`fs` →
49
+ * `@gjsify/native-fs-bridge` → `@gjsify/empty`, because a bridge package
50
+ * that IS the native implementation declares `nativescript: "native"`,
51
+ * which routes to a `globals.mjs` it does not ship).
52
+ *
53
+ * So composition happens at materialisation, where the tiers are still
54
+ * distinguishable, the curated table stays a pure bare-name → package
55
+ * statement (no slot policy duplicated into it), and `aliasPlugin` keeps its
56
+ * one-hop `skipSelf: true` contract. Composing a table is therefore an
57
+ * explicit, per-table decision; `ALIASES_NODE_FOR_NATIVESCRIPT` is
58
+ * deliberately NOT composed (see the STATUS.md TODO on the `native` slot's
59
+ * double meaning for the bridge packages).
60
+ *
61
+ * Evaluated LAZILY (on property read) and cached: `getDerivedAliasesSync` walks
62
+ * the workspace, and importing `@gjsify/resolve-npm` must not pay for a
63
+ * filesystem scan that a `--app gjs` build never needs. The trap set covers
64
+ * everything a consumer does with these tables — `Object.entries`, spread and
65
+ * direct indexing.
66
+ *
67
+ * @param {Record<string,string>} table
68
+ * @param {'gjs'|'node'|'browser'|'nativescript'} target
69
+ * @returns {Record<string,string>}
70
+ */
71
+ function withDerivedSlotRouting(table, target) {
72
+ /** @type {Record<string,string>|null} */
73
+ let routed = null;
74
+ const materialise = () => {
75
+ if (routed) return routed;
76
+ const derived = _getDerivedAliasesSync(target);
77
+ /** @type {Record<string,string>} */
78
+ const out = {};
79
+ for (const [bare, pkg] of Object.entries(table)) {
80
+ out[bare] = Object.prototype.hasOwnProperty.call(derived, pkg) ? derived[pkg] : pkg;
81
+ }
82
+ routed = out;
83
+ return routed;
84
+ };
85
+ return new Proxy(table, {
86
+ get: (_t, prop) =>
87
+ typeof prop === 'string' ? materialise()[prop] : Reflect.get(table, prop),
88
+ has: (_t, prop) => Reflect.has(materialise(), prop),
89
+ ownKeys: () => Reflect.ownKeys(materialise()),
90
+ getOwnPropertyDescriptor: (_t, prop) =>
91
+ Reflect.getOwnPropertyDescriptor(materialise(), prop),
92
+ });
93
+ }
94
+
14
95
  /** Array of Node.js build in module names */
15
96
  export const EXTERNALS_NODE = [
16
97
  'assert',
@@ -187,45 +268,93 @@ export const ALIASES_NODE_FOR_GJS = {
187
268
  * the routing in a second pass (`@gjsify/<X>` → `@gjsify/<X>/globals` or
188
269
  * `@gjsify/empty` depending on the slot declaration).
189
270
  *
190
- * `none`-slot entries are deliberately hardcoded to `@gjsify/empty` here
191
- * (e.g. `child_process`, `fs`, `net`, ...) rather than relying on the dynamic
192
- * runtime layer — bare `fs` / `net` / ... do NOT start with `@gjsify/` and
193
- * therefore never enter `getDerivedAliasesSync`. This table is the bridge.
271
+ * `none`-slot entries are deliberately hardcoded here rather than relying on the
272
+ * dynamic runtime layer — bare `fs` / `net` / ... do NOT start with `@gjsify/`
273
+ * and therefore never enter `getDerivedAliasesSync`. This table is the bridge.
274
+ *
275
+ * ## `@gjsify/empty` is a LAST resort, and every remaining one is classified
276
+ *
277
+ * A value of `@gjsify/empty` is an ANONYMOUS redirect: a shared
278
+ * `export default {}` that a dozen unrelated specifiers also resolve to. That
279
+ * makes it indistinguishable, in an emitted bundle, from an ACCIDENTAL alias —
280
+ * which is how `@gjsify/canvas2d`'s build could start emitting a stray
281
+ * `@gjsify/empty` import without anything noticing. It is also export-less, so
282
+ * `import { spawn } from 'child_process'` binds `undefined` and dies later at
283
+ * `spawn is not a function`, naming neither the module nor the platform.
194
284
  *
195
- * NOTE — UNWIRED IN THIS PR. The browser-app orchestrator
196
- * (`packages/infra/rolldown-plugin-gjsify/src/app/browser.ts`) does NOT
197
- * consume this map yet. PR-D (T-Plan Sektion 2b-ii + 2b-iii, Welle 3) flips
198
- * `browser.ts` to spread `ALIASES_NODE_FOR_BROWSER` (+ generated `node:*`
199
- * prefix-map) into its `aliasMap` UNDER `browserPolyfillAliases` and the user
200
- * aliases. Decoupled here so the table is exportable / reviewable in
201
- * isolation, while consumer wiring waits on the R1 wave delivering browser-
202
- * baubable `@gjsify/{process,buffer,stream,...}` builds. Adopt only when the
203
- * per-package browser slots are R1-validated.
285
+ * So a redirect is NAMED wherever a name exists, and each entry still pointing
286
+ * at `@gjsify/empty` carries an inline classification so the remaining work is
287
+ * visible rather than implied:
288
+ *
289
+ * (A) wireable now — the package declares a browser slot and ships the entry;
290
+ * the value names that entry. No `(A)` entries remain except
291
+ * `dns/promises`, which needs a `./browser/promises` subpath first.
292
+ * (A') native-available — the browser HAS the API (`fetch`, `WebSocket`); the
293
+ * honest target is a `/globals` re-export, not an empty module.
294
+ * (B) simulatable but unbuilt — a real Web analogue exists (`cluster` over
295
+ * Web Workers, `dgram` over WebRTC data channels, `v8.serialize` over
296
+ * `structuredClone`, `readline` over any stream). `@gjsify/empty` until
297
+ * someone builds it.
298
+ * (C) impossible — no Web API can ever back it. These get a NAMED throwing
299
+ * stub at `@gjsify/<X>/browser` (today: `child_process`, `net`, `tls`).
300
+ * `http2` / `inspector` are also `(C)` but not yet written; `repl` /
301
+ * `wasi` are `(C)` with no owning `@gjsify/*` package to host a stub.
302
+ *
303
+ * A `(C)` stub keeps its package's `runtimes.browser: "none"` declaration: the
304
+ * MODULE genuinely is not usable on the runtime. The stub does not make it
305
+ * usable, it makes the unusability legible — it exports the module's real named
306
+ * shape (so linking succeeds) and throws a structured `ENOTSUP` naming the
307
+ * module, the runtime and the calling site. Exports whose answer IS
308
+ * platform-independent (`net.isIP`, `tls.checkServerIdentity`) stay real; a
309
+ * blanket thrower would replace one lie with another.
310
+ *
311
+ * A `partial`-slot package names its `./browser` PLATFORM ENTRY here, not its
312
+ * package root. `withDerivedSlotRouting` only rewrites a value when the slot is
313
+ * `polyfill` (ADR 0014), so a curated value of `@gjsify/<X>` for a `partial`
314
+ * package would hand the bundler the GJS root body — whose `@girs/*` imports
315
+ * `gjsImportsEmptyPlugin` replaces with `{}`, producing the silent
316
+ * `GLib.Checksum is not a constructor` failure ADR 0014 exists to eliminate.
317
+ * `partial` must mean "degrades at call time", never "crashes at first use".
318
+ * Naming the subpath is safe precisely where the platform entry is already at
319
+ * VALUE-export parity with the root (else routing would trade a call-time
320
+ * degradation for a build-time MISSING_EXPORT) — which is why this is a
321
+ * per-package curated decision and NOT a blanket change to `resolveSlot`'s
322
+ * `partial` case. Machine-checked by `scripts/audit-runtimes.mjs`'
323
+ * `curated-alias-routing` invariant.
324
+ *
325
+ * WIRED. The browser-app orchestrator
326
+ * (`packages/infra/rolldown-plugin-gjsify/src/app/browser.ts`) spreads this map
327
+ * — plus a generated `node:*` prefix variant of every key — into its `aliasMap`
328
+ * between the derived slot routing and the per-target / user aliases. So every
329
+ * value here is a resolve target a `--app browser` build actually takes; a
330
+ * mistake in this table is a shipping bug, not a staged proposal. (The former
331
+ * "UNWIRED IN THIS PR" note dated from before that wiring landed and is what
332
+ * made the `crypto` / `zlib` root-body routing read as latent.)
204
333
  */
205
- export const ALIASES_NODE_FOR_BROWSER = {
334
+ const ALIASES_NODE_FOR_BROWSER_TABLE = {
206
335
  'assert': '@gjsify/assert',
207
336
  'assert/strict': '@gjsify/assert/strict',
208
337
  'async_hooks': '@gjsify/async_hooks',
209
338
  'buffer': '@gjsify/buffer',
210
- 'child_process': '@gjsify/empty', // none-slot — browser has no process model
211
- 'cluster': '@gjsify/empty',
339
+ 'child_process': '@gjsify/child_process/browser', // none slot — NAMED throwing stub
340
+ 'cluster': '@gjsify/empty', // (B) simulatable over Web Workers — unbuilt
212
341
  'console': '@gjsify/console',
213
342
  'constants': '@gjsify/constants',
214
- 'crypto': '@gjsify/crypto',
215
- 'dgram': '@gjsify/empty',
343
+ 'crypto': '@gjsify/crypto/browser', // partial slot — name the platform entry
344
+ 'dgram': '@gjsify/empty', // (B) simulatable over WebRTC data channels — unbuilt
216
345
  'diagnostics_channel': '@gjsify/diagnostics_channel',
217
- 'dns': '@gjsify/empty',
218
- 'dns/promises': '@gjsify/empty',
346
+ 'dns': '@gjsify/dns/browser', // partial slot — name the platform entry
347
+ 'dns/promises': '@gjsify/empty', // (A) blocked: no ./browser/promises subpath yet
219
348
  'domain': '@gjsify/domain',
220
349
  'events': '@gjsify/events',
221
- 'fs': '@gjsify/empty', // phase 1: stub; later @gjsify/fs/browser
222
- 'fs/promises': '@gjsify/empty',
223
- 'http': '@gjsify/empty', // browser fetch covers most cases
224
- 'http2': '@gjsify/empty',
225
- 'https': '@gjsify/empty',
226
- 'inspector': '@gjsify/empty',
227
- 'module': '@gjsify/empty',
228
- 'net': '@gjsify/empty',
350
+ 'fs': '@gjsify/fs/browser', // partial slot — name the platform entry
351
+ 'fs/promises': '@gjsify/fs/browser/promises',
352
+ 'http': '@gjsify/http/browser', // partial slot — name the platform entry
353
+ 'http2': '@gjsify/empty', // (C) impossible — needs a named throwing stub
354
+ 'https': '@gjsify/https/browser', // partial slot — name the platform entry
355
+ 'inspector': '@gjsify/empty', // (C) impossible — needs a named throwing stub
356
+ 'module': '@gjsify/module/browser', // partial slot — name the platform entry
357
+ 'net': '@gjsify/net/browser', // none slot — NAMED throwing stub
229
358
  'os': '@gjsify/os',
230
359
  'path': '@gjsify/path',
231
360
  'path/posix': '@gjsify/path/posix',
@@ -234,9 +363,9 @@ export const ALIASES_NODE_FOR_BROWSER = {
234
363
  'process': '@gjsify/process', // PR-D: flips today's `@gjsify/empty`
235
364
  'punycode': '@gjsify/punycode',
236
365
  'querystring': '@gjsify/querystring',
237
- 'readline': '@gjsify/empty',
238
- 'readline/promises': '@gjsify/empty',
239
- 'repl': '@gjsify/empty',
366
+ 'readline': '@gjsify/empty', // (B) stream-generic; simulatable — unbuilt
367
+ 'readline/promises': '@gjsify/empty', // (B) stream-generic; simulatable — unbuilt
368
+ 'repl': '@gjsify/empty', // (C) impossible, and no @gjsify/repl package exists
240
369
  'stream': '@gjsify/stream',
241
370
  'stream/web': '@gjsify/stream/web',
242
371
  'stream/consumers': '@gjsify/stream/consumers',
@@ -245,24 +374,36 @@ export const ALIASES_NODE_FOR_BROWSER = {
245
374
  'sys': '@gjsify/sys',
246
375
  'timers': '@gjsify/timers',
247
376
  'timers/promises': '@gjsify/timers/promises',
248
- 'tls': '@gjsify/empty',
249
- 'tty': '@gjsify/empty',
377
+ 'tls': '@gjsify/tls/browser', // none slot — NAMED throwing stub
378
+ 'tty': '@gjsify/empty', // (B) isatty()===false is honest — unbuilt
250
379
  'url': '@gjsify/url',
251
380
  'util': '@gjsify/util',
252
381
  'util/types': '@gjsify/util/types',
253
- 'v8': '@gjsify/empty',
382
+ 'v8': '@gjsify/empty', // (B) serialize/deserialize over structuredClone — unbuilt
254
383
  'vm': '@gjsify/vm',
255
- 'wasi': '@gjsify/empty',
256
- 'sqlite': '@gjsify/empty',
257
- 'worker_threads': '@gjsify/empty',
258
- 'zlib': '@gjsify/zlib',
384
+ 'wasi': '@gjsify/empty', // (C) impossible, and no @gjsify/wasi package exists
385
+ 'sqlite': '@gjsify/sqlite/browser', // partial slot — name the platform entry
386
+ 'worker_threads': '@gjsify/worker_threads/browser', // partial slot — name the platform entry
387
+ 'zlib': '@gjsify/zlib/browser', // partial slot — name the platform entry
259
388
 
260
389
  // Third-party
261
- 'node-fetch': '@gjsify/empty', // browser native fetch
262
- 'ws': '@gjsify/empty', // browser native WebSocket
263
- 'isomorphic-ws': '@gjsify/empty',
390
+ 'node-fetch': '@gjsify/empty', // (A') native-available: wants @gjsify/fetch/globals
391
+ 'ws': '@gjsify/ws/browser', // partial slot — name the platform entry
392
+ 'isomorphic-ws': '@gjsify/empty', // (A') native-available: wants a WebSocket re-export
264
393
  }
265
394
 
395
+ /**
396
+ * Bare Node-builtin specifier → polyfill / empty-stub mapping for `--app
397
+ * browser`, with ADR-0014 platform-entry routing already applied to the values
398
+ * (`os` → `@gjsify/os/browser` when that package's browser slot is `polyfill`
399
+ * and it exports a `./browser` subpath). See `withDerivedSlotRouting` above for
400
+ * why the two-pass chain has to be collapsed here.
401
+ */
402
+ export const ALIASES_NODE_FOR_BROWSER = withDerivedSlotRouting(
403
+ ALIASES_NODE_FOR_BROWSER_TABLE,
404
+ 'browser',
405
+ );
406
+
266
407
  /**
267
408
  * Bare Node-builtin specifier → polyfill / empty-stub mapping for `--app
268
409
  * nativescript` builds. Mirrors `ALIASES_NODE_FOR_BROWSER` in shape, with
@@ -277,11 +418,18 @@ export const ALIASES_NODE_FOR_BROWSER = {
277
418
  * - **Mobile-tractable Node built-ins → `@gjsify/<X>`.** `assert`,
278
419
  * `async_hooks`, `buffer`, `crypto`, `events`, `fs`, `os`, `path`,
279
420
  * `process`, `stream`, `string_decoder`, `url`, `util`, `querystring`
280
- * route to their gjsify polyfills. The polyfills themselves may declare
281
- * `runtimes.nativescript = "polyfill" | "partial" | "none"`; the
282
- * triplet-driven `getDerivedAliasesSync('nativescript')` second pass
283
- * reads the declaration and either keeps the resolution as-is (polyfill /
284
- * partial) or redirects to `/globals` (native) or `@gjsify/empty` (none).
421
+ * route to their gjsify polyfills. NOTE: unlike `ALIASES_NODE_FOR_BROWSER`,
422
+ * this table is deliberately NOT wrapped in `withDerivedSlotRouting` — the
423
+ * VALUES here are taken as final. Composing it today would only change 4 of
424
+ * 122 entries and all four are regressions: `fs`/`fs/promises` point at
425
+ * `@gjsify/native-fs-bridge`, which declares `nativescript: "native"` meaning
426
+ * "this package IS the native implementation", while the slot vocabulary's
427
+ * `native` means "the RUNTIME provides it, route to `<pkg>/globals`" — a file
428
+ * the bridge does not ship, so the composed value degrades to
429
+ * `@gjsify/empty`. Settle the bridge packages' slot declarations first (see
430
+ * STATUS.md `Open TODOs`), then compose. An import by PACKAGE NAME does route
431
+ * per the declared slot (plain key hit in the merged map) — which is why the
432
+ * bridges are ALREADY emptied on that path today, same TODO.
285
433
  * - **`ws` / `isomorphic-ws` → `@gjsify/empty`.** NS apps use `WebSocket`
286
434
  * from the global runtime, not the `ws` package.
287
435
  *
@@ -559,6 +707,14 @@ export const ALIASES_WEB_FOR_NODE = {
559
707
  '@gjsify/dom-elements/register/match-media': '@gjsify/empty',
560
708
  '@gjsify/dom-elements/register/location': '@gjsify/empty',
561
709
  '@gjsify/dom-elements/register/navigator': '@gjsify/empty',
710
+ // Canvas 2D + IFrame + WebGL registers (GTK/Cairo-, WebKit- and
711
+ // Gwebgl/GLArea-backed, ADR 0012) — no-op on Node, exactly like the
712
+ // dom-elements registers above. Only the `@gjsify/*`-qualified form is
713
+ // mirrored: per ADR 0012 no bare-specifier `/register` alias is invented for
714
+ // a framework package.
715
+ '@gjsify/canvas2d/register': '@gjsify/empty',
716
+ '@gjsify/iframe/register': '@gjsify/empty',
717
+ '@gjsify/webgl/register': '@gjsify/empty',
562
718
  '@gjsify/buffer/register': '@gjsify/empty',
563
719
 
564
720
  // 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.24.1",
4
4
  "description": "Resolve NPM package aliases",
5
5
  "type": "module",
6
6
  "main": "lib/index.mjs",