@gjsify/resolve-npm 0.23.0 → 0.24.2

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.
Files changed (2) hide show
  1. package/lib/index.mjs +136 -62
  2. package/package.json +1 -1
package/lib/index.mjs CHANGED
@@ -19,25 +19,44 @@ import { getDerivedAliasesSync as _getDerivedAliasesSync } from './runtime-alias
19
19
  *
20
20
  * ## Why this exists (ADR 0014)
21
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".
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.)
26
29
  *
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.
30
+ * ## Why HERE and not as a second pass in `aliasPlugin`
36
31
  *
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.
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).
41
60
  *
42
61
  * Evaluated LAZILY (on property read) and cached: `getDerivedAliasesSync` walks
43
62
  * the workspace, and importing `@gjsify/resolve-npm` must not pay for a
@@ -249,45 +268,93 @@ export const ALIASES_NODE_FOR_GJS = {
249
268
  * the routing in a second pass (`@gjsify/<X>` → `@gjsify/<X>/globals` or
250
269
  * `@gjsify/empty` depending on the slot declaration).
251
270
  *
252
- * `none`-slot entries are deliberately hardcoded to `@gjsify/empty` here
253
- * (e.g. `child_process`, `fs`, `net`, ...) rather than relying on the dynamic
254
- * runtime layer — bare `fs` / `net` / ... do NOT start with `@gjsify/` and
255
- * 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.
284
+ *
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.
256
310
  *
257
- * NOTE — UNWIRED IN THIS PR. The browser-app orchestrator
258
- * (`packages/infra/rolldown-plugin-gjsify/src/app/browser.ts`) does NOT
259
- * consume this map yet. PR-D (T-Plan Sektion 2b-ii + 2b-iii, Welle 3) flips
260
- * `browser.ts` to spread `ALIASES_NODE_FOR_BROWSER` (+ generated `node:*`
261
- * prefix-map) into its `aliasMap` UNDER `browserPolyfillAliases` and the user
262
- * aliases. Decoupled here so the table is exportable / reviewable in
263
- * isolation, while consumer wiring waits on the R1 wave delivering browser-
264
- * baubable `@gjsify/{process,buffer,stream,...}` builds. Adopt only when the
265
- * per-package browser slots are R1-validated.
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.)
266
333
  */
267
334
  const ALIASES_NODE_FOR_BROWSER_TABLE = {
268
335
  'assert': '@gjsify/assert',
269
336
  'assert/strict': '@gjsify/assert/strict',
270
337
  'async_hooks': '@gjsify/async_hooks',
271
338
  'buffer': '@gjsify/buffer',
272
- 'child_process': '@gjsify/empty', // none-slot — browser has no process model
273
- '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
274
341
  'console': '@gjsify/console',
275
342
  'constants': '@gjsify/constants',
276
- 'crypto': '@gjsify/crypto',
277
- '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
278
345
  'diagnostics_channel': '@gjsify/diagnostics_channel',
279
- 'dns': '@gjsify/empty',
280
- '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
281
348
  'domain': '@gjsify/domain',
282
349
  'events': '@gjsify/events',
283
- 'fs': '@gjsify/empty', // phase 1: stub; later @gjsify/fs/browser
284
- 'fs/promises': '@gjsify/empty',
285
- 'http': '@gjsify/empty', // browser fetch covers most cases
286
- 'http2': '@gjsify/empty',
287
- 'https': '@gjsify/empty',
288
- 'inspector': '@gjsify/empty',
289
- 'module': '@gjsify/empty',
290
- '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
291
358
  'os': '@gjsify/os',
292
359
  'path': '@gjsify/path',
293
360
  'path/posix': '@gjsify/path/posix',
@@ -296,9 +363,9 @@ const ALIASES_NODE_FOR_BROWSER_TABLE = {
296
363
  'process': '@gjsify/process', // PR-D: flips today's `@gjsify/empty`
297
364
  'punycode': '@gjsify/punycode',
298
365
  'querystring': '@gjsify/querystring',
299
- 'readline': '@gjsify/empty',
300
- 'readline/promises': '@gjsify/empty',
301
- '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
302
369
  'stream': '@gjsify/stream',
303
370
  'stream/web': '@gjsify/stream/web',
304
371
  'stream/consumers': '@gjsify/stream/consumers',
@@ -307,22 +374,22 @@ const ALIASES_NODE_FOR_BROWSER_TABLE = {
307
374
  'sys': '@gjsify/sys',
308
375
  'timers': '@gjsify/timers',
309
376
  'timers/promises': '@gjsify/timers/promises',
310
- 'tls': '@gjsify/empty',
311
- 'tty': '@gjsify/empty',
377
+ 'tls': '@gjsify/tls/browser', // none slot — NAMED throwing stub
378
+ 'tty': '@gjsify/empty', // (B) isatty()===false is honest — unbuilt
312
379
  'url': '@gjsify/url',
313
380
  'util': '@gjsify/util',
314
381
  'util/types': '@gjsify/util/types',
315
- 'v8': '@gjsify/empty',
382
+ 'v8': '@gjsify/empty', // (B) serialize/deserialize over structuredClone — unbuilt
316
383
  'vm': '@gjsify/vm',
317
- 'wasi': '@gjsify/empty',
318
- 'sqlite': '@gjsify/empty',
319
- 'worker_threads': '@gjsify/empty',
320
- '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
321
388
 
322
389
  // Third-party
323
- 'node-fetch': '@gjsify/empty', // browser native fetch
324
- 'ws': '@gjsify/empty', // browser native WebSocket
325
- '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
326
393
  }
327
394
 
328
395
  /**
@@ -351,11 +418,18 @@ export const ALIASES_NODE_FOR_BROWSER = withDerivedSlotRouting(
351
418
  * - **Mobile-tractable Node built-ins → `@gjsify/<X>`.** `assert`,
352
419
  * `async_hooks`, `buffer`, `crypto`, `events`, `fs`, `os`, `path`,
353
420
  * `process`, `stream`, `string_decoder`, `url`, `util`, `querystring`
354
- * route to their gjsify polyfills. The polyfills themselves may declare
355
- * `runtimes.nativescript = "polyfill" | "partial" | "none"`; the
356
- * triplet-driven `getDerivedAliasesSync('nativescript')` second pass
357
- * reads the declaration and either keeps the resolution as-is (polyfill /
358
- * 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.
359
433
  * - **`ws` / `isomorphic-ws` → `@gjsify/empty`.** NS apps use `WebSocket`
360
434
  * from the global runtime, not the `ws` package.
361
435
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/resolve-npm",
3
- "version": "0.23.0",
3
+ "version": "0.24.2",
4
4
  "description": "Resolve NPM package aliases",
5
5
  "type": "module",
6
6
  "main": "lib/index.mjs",