@gjsify/rolldown-plugin-gjsify 0.36.0 → 0.38.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.
@@ -1,72 +1,46 @@
1
- // For `--app gjs`: transparently route a compiled N-API `.node` addon through
2
- // `@gjsify/napi`'s `loadAddon()` so `import Database from 'better-sqlite3'` /
3
- // `require('bufferutil')` "just works" in a GJS build — no per-project shim, no
4
- // hand-pinned addon path.
1
+ // For `--app gjs`: route a compiled N-API `.node` addon through `@gjsify/napi`'s
2
+ // `loadAddon()` so `import Database from 'better-sqlite3'` / `require('bufferutil')`
3
+ // works in a GJS build with no per-project shim. The FORWARD MIRROR of
4
+ // `gjsGiNodePlugin` (`gjs-gi-node.ts`), which runs GObject code on Node; both
5
+ // intercept the ONE specifier a bundler can see and replace it with a virtual
6
+ // module whose value is the runtime bridge.
5
7
  //
6
- // This is the FORWARD MIRROR of `gjsGiNodePlugin` (`gjs-gi-node.ts`): that
7
- // plugin rewrites `gi://Ns` → `requireGi` for `--app node` (run GObject code on
8
- // Node); this one rewrites a native-addon acquisition → `loadAddon` for
9
- // `--app gjs` (run Node's `.node` addons on GJS). Both intercept the ONE
10
- // specifier a bundler can see and replace it with a virtual module whose value
11
- // is the runtime bridge.
8
+ // The `.node` itself is always acquired with a DYNAMIC `require(<computed path>)`
9
+ // no bundler can rewrite, so the interception targets the well-known helper
10
+ // instead. Conventions handled, keyed by the import a bundler DOES see:
12
11
  //
13
- // The problem is always the same: the compiled `.node` is acquired with a
14
- // DYNAMIC `require(<computed path>)` no bundler can rewrite. So we intercept the
15
- // well-known helper the addon uses and replace it with a virtual module that
16
- // returns `loadAddon('<abs .node>')`. Handled conventions (keyed by the import
17
- // a bundler DOES see):
18
- //
19
- // - direct `.node` — a `source` (or a `@scope/pkg-<triple>` sibling that
20
- // resolves to a `.node`) ending in `.node`.
21
- // - `node-gyp-build` — prebuildify layout (bufferutil, utf-8-validate, …).
22
- // Default export is a `load(dir)` function.
23
- // - `bindings` — node-bindings layout (better-sqlite3, node-sqlite3).
24
- // Default export is a `bindings(name)` function.
12
+ // - direct `.node` — a source ending in `.node`.
13
+ // - `node-gyp-build` — prebuildify layout (bufferutil, …); default export is
14
+ // a `load(dir)` function.
15
+ // - `bindings` — node-bindings layout (better-sqlite3, …); default
16
+ // export is a `bindings(name)` function.
25
17
  // - napi-rs sibling — `@scope/pkg-<triple>` platform package (or local
26
- // `pkg.<triple>.node` fallback), whose exports ARE the
27
- // addon API.
28
- // - napi-rs ENTRY — the napi-rs GENERATED loader index
29
- // (`@node-rs/argon2/index.js`), detected by a
30
- // package.json signal and replaced WHOLESALE.
18
+ // `pkg.<triple>.node`), whose exports ARE the addon API.
19
+ // - napi-rs ENTRY — the GENERATED loader index, replaced WHOLESALE.
31
20
  //
32
- // napi-rs GENERATED-LOADER ENTRY replacement: a napi-rs generated index
33
- // (`@node-rs/argon2/index.js`) wraps its acquisition in a
34
- // `require('node:module')` + `createRequire(__filename)` + runtime
35
- // `existsSync`/`process` branch chain whose CJS body does NOT survive `--app gjs`
36
- // bundling (the top-level `require = createRequire(...)` reassignment throws
37
- // `ReferenceError: require`). The sibling/direct interceptions above correctly
38
- // LOCATE the `.node` but cannot rescue that body. So when the entry of a napi-rs
39
- // package resolves, we replace the WHOLE module with
40
- // `module.exports = loadAddon('<abs platform .node>')` (the `napi-rs-entry`
41
- // kind) — the platform `.node` is the current-triple sibling package (only the
42
- // host's optionalDependency is installed) or a local `pkg.<triple>.node`. The
43
- // sibling is named `<self>-<triple>` OR `<napi.packageName>-<triple>`; both
44
- // schemes ship in the wild and `isNapiRsSibling` knows both, which is what
45
- // makes `rolldown`/`oxfmt`/`oxlint` (all `@<scope>/binding-<triple>`) reachable
46
- // and not just `lightningcss`/`@node-rs/*`. Detection is CONSERVATIVE (package.json signal +
47
- // the file must be the package's own native `main` entry + a real host `.node`
48
- // must resolve) so it never rewrites an unrelated package's entry; when no
49
- // current-platform `.node` resolves we fall through to normal resolution rather
50
- // than emit a shim over a missing file. The C/C++ path (node-gyp-build +
51
- // bindings) and napi-rs are all proven byte-identical in the transparent gate.
21
+ // The ENTRY case exists because a napi-rs generated index wraps its acquisition in
22
+ // a `require('node:module')` + `createRequire(__filename)` + runtime-branch chain
23
+ // whose CJS body does not survive `--app gjs` bundling: the top-level
24
+ // `require = createRequire(...)` reassignment throws `ReferenceError: require`. The
25
+ // other interceptions LOCATE the `.node` correctly but cannot rescue that body, so
26
+ // the whole module becomes `module.exports = loadAddon('<abs platform .node>')`.
27
+ // Detection is CONSERVATIVE — package.json signal + the file must be the package's
28
+ // own native `main` + a real host `.node` must resolve — and falls through to
29
+ // normal resolution otherwise, never shimming over a missing file.
52
30
  //
53
- // The addon's compiled `.node` is located by `resolveAddonPath()`, which
54
- // replicates node-gyp-build's OWN selection algorithm (build/Release →
55
- // build/Debug → prebuilds/<platform>-<arch>/<best tag>) so the GJS build routes
56
- // the SAME binary Node would load.
31
+ // `resolveAddonPath()` replicates node-gyp-build's OWN selection algorithm
32
+ // (build/Release → build/Debug → prebuilds/<platform>-<arch>/<best tag>) so the
33
+ // GJS build routes the SAME binary Node would load.
57
34
  //
58
- // KEY: the shims import `@gjsify/napi` by BARE SPECIFIER (`require('@gjsify/napi')`).
59
- // `@gjsify/napi`'s L1 is a `gjs:polyfill` package reading `imports.gi`; it
60
- // bundles normally, and its native typelib is auto-added to `GI_TYPELIB_PATH` by
61
- // the CLI's `detectNativePackages` (because it declares `gjsify.prebuilds`). The
62
- // callable-helper shims (`node-gyp-build`/`bindings`) stay CJS
63
- // (`module.exports = fn`) so the consumer's `require('node-gyp-build')(dir)`
64
- // call site keeps working through gjsify's cjs-compat interop — a callable, not
65
- // an ESM namespace.
35
+ // The shims import `@gjsify/napi` by BARE SPECIFIER: it is a `gjs:polyfill`
36
+ // package that bundles normally, and its native typelib is auto-added to
37
+ // `GI_TYPELIB_PATH` by the CLI's `detectNativePackages` (it declares
38
+ // `gjsify.prebuilds`). The callable-helper shims stay CJS (`module.exports = fn`)
39
+ // so a consumer's `require('node-gyp-build')(dir)` call site keeps working through
40
+ // cjs-compat interop — a callable, not an ESM namespace.
66
41
  //
67
- // Portability note (same as `gjs-gi-node.ts`): the `filter` is a Rolldown
68
- // fast-path; the internal guard in the handler is the load-bearing check so the
69
- // plugin is correct even when the filter does not pre-filter.
42
+ // Portability (same as `gjs-gi-node.ts`): the `filter` is a Rolldown fast path;
43
+ // the handler's internal guard is the load-bearing check.
70
44
  import { existsSync, readdirSync, readFileSync } from 'node:fs';
71
45
  import { dirname, isAbsolute, join, resolve } from 'node:path';
72
46
  import { GJSIFY_VIRTUAL_PREFIX } from '../utils/virtual-module-id.js';
@@ -76,58 +50,46 @@ const NAPI_BARE_SPECIFIER = '@gjsify/napi';
76
50
  /**
77
51
  * Platform-triple tail of a napi-rs sibling platform package
78
52
  * (`@node-rs/argon2-linux-x64-gnu`) or its local fallback
79
- * (`argon2.linux-x64-gnu.node`). Deliberately narrow — an interception on a
80
- * false match is self-correcting (it only fires when the specifier ALSO
81
- * resolves to a real `.node`), but keeping the shape tight avoids needless
82
- * `this.resolve` probes on ordinary deps.
53
+ * (`argon2.linux-x64-gnu.node`). Narrow on purpose: a false match is
54
+ * self-correcting (interception needs a real `.node` too), but a tight shape
55
+ * avoids needless `this.resolve` probes on ordinary deps.
83
56
  */
84
57
  const NAPI_RS_TRIPLE_RE = /-(?:linux|darwin|win32|freebsd|openbsd|sunos|android|aix)-(?:x64|arm64|arm|ia32|ppc64|s390x|riscv64|loong64)(?:-(?:gnu|musl|msvc|eabi|eabihf|androideabi|gnueabihf))?$/;
85
58
  /**
86
59
  * A BARE package specifier (`lightningcss`, `@rolldown/binding-…`) — no leading
87
- * `.`/`/`/`\`, no protocol (`node:fs`, `gi://Gtk`, `data:…`, `C:\…`), not a
88
- * `\0` virtual id.
60
+ * `.`/`/`/`\`, no protocol (`node:fs`, `gi://Gtk`, `data:…`, `C:\…`), not a `\0`
61
+ * virtual id.
89
62
  *
90
- * Needed because a napi-rs generated loader acquires its binary through a
91
- * specifier the bundler CANNOT see: `require(`lightningcss-${parts.join('-')}`)`
92
- * / `require(`../lightningcss.${parts.join('-')}.node`)` are template literals
93
- * computed at runtime. So neither the sibling nor the direct-`.node`
94
- * interception can fire, and the only lever is replacing the loader ENTRY —
95
- * which the caller reaches by its BARE name. Matching only path-shaped ids (as
96
- * before) meant `import { transform } from 'lightningcss'` was never even
97
- * offered to the handler, and the addon gate had to hand-alias every bare
98
- * specifier to its native entry to get coverage.
63
+ * Bare ids must be offered to the handler because a napi-rs generated loader
64
+ * acquires its binary through runtime-computed template literals
65
+ * (`require(`lightningcss-${parts.join('-')}`)`) that no bundler can see: neither
66
+ * the sibling nor the direct-`.node` interception can fire, so the only lever is
67
+ * replacing the loader ENTRY, which the caller reaches by its BARE name.
99
68
  *
100
- * PORTABILITY, and it is load-bearing: this source feeds `ADDON_FILTER_RE`,
101
- * which `@gjsify/rolldown-native` hands to the Rust core as an `idFilter`
102
- * STRING. Rust's `regex` crate has no lookaround and no `\0` escape, and it
103
- * rejects the WHOLE combined filter rather than the offending branch — which
104
- * silently disables every interception under the GJS engine while npm
105
- * `rolldown` on Node keeps working. So: no `(?!…)`, and NUL is spelled `\x00`.
106
- * The asymmetry is invisible to the addon gate, which drives the Node CLI
107
- * entry, so only a real `--app gjs` build under `gjs` catches a regression here.
69
+ * PORTABILITY, load-bearing: this source feeds `ADDON_FILTER_RE`, which
70
+ * `@gjsify/rolldown-native` hands to the Rust core as an `idFilter` STRING. Rust's
71
+ * `regex` crate has no lookaround and no `\0` escape, and it rejects the WHOLE
72
+ * combined filter rather than the offending branch — silently disabling every
73
+ * interception under the GJS engine while npm `rolldown` on Node keeps working. So:
74
+ * no `(?!…)`, and NUL is spelled `\x00`. Only a real `--app gjs` build under `gjs`
75
+ * catches a regression here; the addon gate drives the Node CLI entry.
108
76
  */
109
77
  // oxlint-disable-next-line no-control-regex -- the NUL is the point: it marks a bundler VIRTUAL id (`\0gjsify-…`), which is never a package specifier and must not be probed.
110
78
  const BARE_SPECIFIER_RE = /^[^.\\/:\s\x00][^:\s\x00]*$/;
111
79
  /**
112
- * Fast-path filter (superset of what `claimSpecifier` claims). Handler re-checks.
113
- *
80
+ * Fast-path filter — a superset of what the handler claims; the handler re-checks.
114
81
  * Exported so `napi-node-addon.spec.ts` can assert its Rust-compatibility (see
115
- * {@link BARE_SPECIFIER_RE}) — a JS-only construct here disables the plugin
116
- * entirely under `@gjsify/rolldown-native`, and no build-level test would see
117
- * it, because every addon gate drives the Node CLI entry.
82
+ * {@link BARE_SPECIFIER_RE}).
118
83
  */
119
84
  export const ADDON_FILTER_RE = new RegExp([
120
85
  /\.node$/.source, // direct .node
121
86
  /^node-gyp-build(?:\/index\.js)?$/.source, // node-gyp-build helper
122
87
  /^bindings(?:\/bindings\.js)?$/.source, // bindings helper
123
88
  NAPI_RS_TRIPLE_RE.source, // napi-rs platform sibling
124
- // napi-rs generated-loader ENTRY: the conventional package-root
125
- // `index.{js,cjs,mjs}` a napi-rs build emits. Narrow on purpose (only
126
- // `index.*`, not every `.js`) — the handler's `detectNapiRsEntry` gate
127
- // (package.json signal + native-main match) is the load-bearing check.
89
+ // napi-rs generated-loader ENTRY. Only `index.*`, not every `.js` — the
90
+ // handler's `detectNapiRsEntry` gate is the load-bearing check.
128
91
  /[/\\]index\.[cm]?js$/.source,
129
- // A bare specifier, so the handler can resolve it and test the RESOLVED
130
- // file. Broad by design; the handler's fast rejects keep it cheap.
92
+ // Bare specifier, so the handler can resolve it and test the RESOLVED file.
131
93
  BARE_SPECIFIER_RE.source,
132
94
  ].join('|'));
133
95
  function isMusl(platform) {
@@ -259,13 +221,10 @@ export class AddonNotBuiltError extends Error {
259
221
  }
260
222
  }
261
223
  /**
262
- * Locate the compiled `.node` for an addon package root, matching
263
- * node-gyp-build's probe order: `build/Release` → `build/Debug` →
264
- * `prebuilds/<platform>-<arch>/<best tag>`. Throws {@link AddonNotBuiltError}
265
- * when nothing is found.
266
- *
267
- * @param pkgRoot Absolute path to the addon package root (dir with package.json).
268
- * @param opts.warn Optional sink for a non-fatal warning (ambiguous build dir).
224
+ * Locate the compiled `.node` for an addon package root, matching node-gyp-build's
225
+ * probe order: `build/Release` → `build/Debug` →
226
+ * `prebuilds/<platform>-<arch>/<best tag>`. Throws {@link AddonNotBuiltError} when
227
+ * nothing is found. `opts.warn` sinks the non-fatal ambiguous-build-dir warning.
269
228
  */
270
229
  export function resolveAddonPath(pkgRoot, opts) {
271
230
  const host = hostTarget();
@@ -286,11 +245,7 @@ export function resolveAddonPath(pkgRoot, opts) {
286
245
  return prebuild;
287
246
  throw new AddonNotBuiltError(pkgRoot);
288
247
  }
289
- /**
290
- * Walk up from an importer file to the nearest `package.json` directory (the
291
- * addon package root). Returns null when none is found (should not happen for a
292
- * real node_modules dep).
293
- */
248
+ /** Nearest ancestor directory of `importerFile` holding a `package.json`. */
294
249
  export function nearestPackageRoot(importerFile) {
295
250
  let dir = isAbsolute(importerFile) ? dirname(importerFile) : dirname(resolve(importerFile));
296
251
  // Guard against an infinite loop at the filesystem root.
@@ -304,12 +259,6 @@ export function nearestPackageRoot(importerFile) {
304
259
  }
305
260
  return null;
306
261
  }
307
- // ---------------------------------------------------------------------------
308
- // Shim generators — the virtual-module bodies. All import `@gjsify/napi` by BARE
309
- // specifier. The callable-helper shims are CJS (`module.exports = fn`) so the
310
- // consumer's `require('node-gyp-build')(dir)` / `require('bindings')(name)` stays
311
- // callable through cjs-compat interop.
312
- // ---------------------------------------------------------------------------
313
262
  /** Direct `.node` import → the addon's exports (ESM default). */
314
263
  export function directNodeShim(addonPath) {
315
264
  return (`import { loadAddon } from ${JSON.stringify(NAPI_BARE_SPECIFIER)};\n` +
@@ -344,9 +293,8 @@ function shimFor(kind, addonPath) {
344
293
  return bindingsShim(addonPath);
345
294
  case 'napi-rs':
346
295
  case 'napi-rs-entry':
347
- // Both hand back the raw native exports as the module value. The
348
- // `napi-rs-entry` kind replaces the whole GENERATED loader; `napi-rs`
349
- // replaces a directly-imported platform sibling. Same body.
296
+ // Same body: `napi-rs-entry` replaces the whole GENERATED loader,
297
+ // `napi-rs` a directly-imported platform sibling.
350
298
  return napiRsShim(addonPath);
351
299
  }
352
300
  }
@@ -364,12 +312,7 @@ function decodeVirtual(id) {
364
312
  const addonPath = rest.slice(sep + 1);
365
313
  return { kind, addonPath };
366
314
  }
367
- /**
368
- * Classify a specifier for interception. Pure decision logic (no filesystem
369
- * probing beyond what `resolveAddonPath` does via the importer) — exported for
370
- * the unit tests. Returns the shim kind + the package root to probe, or the
371
- * literal `.node` path for the direct case.
372
- */
315
+ /** Classify a specifier for interception — pure decision logic, no filesystem. */
373
316
  export function classifySpecifier(source) {
374
317
  if (source === 'node-gyp-build' || source === 'node-gyp-build/index.js')
375
318
  return { kind: 'node-gyp-build' };
@@ -387,31 +330,22 @@ export function classifySpecifier(source) {
387
330
  /**
388
331
  * Is this manifest a gjsify NATIVE BRIDGE whose prebuilds live in per-target
389
332
  * sibling packages (ADR 0017) — `@gjsify/webgl` + `@gjsify/webgl-darwin-x64`, …?
333
+ * Such a bridge ships a GI typelib plus a `.so`/`.dylib`/`.dll` loaded through
334
+ * `gi://`, NEVER a Node-API `.node`, so it must never reach `loadAddon()`.
390
335
  *
391
- * Such a bridge ships a GObject-Introspection typelib plus a `.so`/`.dylib`/`.dll`
392
- * loaded through `gi://`, NEVER a Node-API `.node` addon, so it must never be
393
- * routed through `loadAddon()`.
394
- *
395
- * It has to be excluded EXPLICITLY because the two conventions are
396
- * indistinguishable by name: ADR 0017 spells a target `<os>-<arch>` and names the
397
- * package `<self>-<os>-<arch>` in `optionalDependencies` — which is exactly
398
- * napi-rs' `<self>-<triple>` sibling scheme whenever the target happens to be a
399
- * name napi-rs also uses. On Linux the two vocabularies differ by the libc token
400
- * (`linux-x64` vs napi-rs' `linux-x64-gnu`), so `hostNapiRsTriple()` never matched
401
- * and nothing fired; on macOS napi-rs' triple IS `darwin-x64` / `darwin-arm64`,
402
- * so every split bridge looked like a napi-rs package to
403
- * {@link isNapiRsPackageJson} and {@link resolveNapiRsEntryAddon} then probed
404
- * `ctx.resolve('@gjsify/webgl-darwin-x64')` — an artifact-only package with no JS
405
- * entry at all. That probe does not return null: the `unresolved-workspace-import`
406
- * guard (correctly) treats an unresolvable bare `@gjsify/*` as FATAL, so
407
- * `gjsify build --app gjs` of any consumer of any split bridge died on darwin with
408
- * "cannot resolve the workspace package" — invisible to every Linux CI job.
336
+ * The exclusion must be EXPLICIT because the two conventions are indistinguishable
337
+ * by name: ADR 0017's `<self>-<os>-<arch>` optionalDependency is exactly napi-rs'
338
+ * `<self>-<triple>` scheme wherever the vocabularies agree. On Linux they differ by
339
+ * the libc token (`linux-x64` vs `linux-x64-gnu`) so nothing fired, but on macOS
340
+ * napi-rs' triple IS `darwin-arm64`: every split bridge looked like a napi-rs
341
+ * package, {@link resolveNapiRsEntryAddon} probed an artifact-only package with no
342
+ * JS entry, and the `unresolved-workspace-import` guard (correctly) made that
343
+ * unresolvable bare `@gjsify/*` FATAL — so `--app gjs` of any consumer of any split
344
+ * bridge died on darwin, invisible to every Linux CI job.
409
345
  *
410
- * `gjsify.platforms` is the right discriminator: it is the OS-axis declaration
411
- * every native bridge carries (§ Runtime & platform model) and no napi-rs package
412
- * has any reason to declare it.
413
- *
414
- * Exported for the unit tests.
346
+ * `gjsify.platforms` is the right discriminator: the OS-axis declaration every
347
+ * native bridge carries (§ Runtime & platform model), which no napi-rs package has
348
+ * reason to declare.
415
349
  */
416
350
  export function isGjsifyNativeBridge(pkg) {
417
351
  const gjsify = pkg?.gjsify;
@@ -421,23 +355,18 @@ export function isGjsifyNativeBridge(pkg) {
421
355
  return Array.isArray(platforms) || typeof prebuilds === 'string';
422
356
  }
423
357
  /**
424
- * A package.json describes a napi-rs generated-loader package when EITHER
425
- * signal holds — both are package.json-level (not source sniffing):
358
+ * A package.json describes a napi-rs generated-loader package when EITHER signal
359
+ * holds:
426
360
  *
427
361
  * (a) a top-level `napi` config OBJECT (`{ binaryName, targets, … }`) — the
428
- * napi-rs CLI's own build block. An ordinary npm package never declares a
429
- * `napi` object at the manifest root.
430
- * (b) at least one `optionalDependencies` entry that is a platform SIBLING
431
- * (see {@link isNapiRsSibling}) — the napi-rs prebuilt-binary convention,
432
- * under either naming scheme: `<self>-<triple>` (`@node-rs/argon2` ships
433
- * `@node-rs/argon2-linux-x64-gnu`) or `<napi.packageName>-<triple>`
434
- * (`rolldown` ships `@rolldown/binding-linux-x64-gnu`). The key MUST
435
- * carry a known prefix AND end in a platform triple, so a normal optional
436
- * dep (`fsevents`) can't match.
362
+ * napi-rs CLI's build block, which no ordinary npm package declares.
363
+ * (b) an `optionalDependencies` entry that is a platform SIBLING (see
364
+ * {@link isNapiRsSibling}) — the key must carry a known prefix AND end in a
365
+ * platform triple, so a normal optional dep (`fsevents`) can't match.
437
366
  *
438
- * Neither fires on a normal package, so pairing this with the "resolved file IS
439
- * the package's own native main entry" check (see {@link detectNapiRsEntry})
440
- * makes entry-replacement safe. Exported for the unit tests.
367
+ * Neither fires on a normal package, so pairing this with the "resolved file IS the
368
+ * package's own native main entry" check ({@link detectNapiRsEntry}) makes
369
+ * entry-replacement safe.
441
370
  */
442
371
  export function isNapiRsPackageJson(pkg) {
443
372
  if (pkg && typeof pkg.napi === 'object' && pkg.napi !== null)
@@ -460,12 +389,11 @@ function readPackageJsonSafe(pkgRoot) {
460
389
  }
461
390
  }
462
391
  /**
463
- * Decide whether an absolute file path is the NATIVE generated-loader entry of a
464
- * napi-rs package. Returns the package root + parsed manifest when it is, else
465
- * null. "Native entry" = the package's `main` (default `index.js`) — NOT its
466
- * `browser`/wasm fallback and NOT a deep file — so we only ever replace the one
467
- * generated loader, never an unrelated module. Filesystem-based (reads the
468
- * nearest package.json); the plugin memoizes calls. Exported for the unit tests.
392
+ * Is this absolute file path the NATIVE generated-loader entry of a napi-rs package?
393
+ * Returns the package root + parsed manifest, else null. "Native entry" = a path the
394
+ * manifest names as its entry — NOT its `browser`/wasm fallback and NOT a deep file
395
+ * — so only the one generated loader is ever replaced. Reads the nearest
396
+ * package.json; the plugin memoizes calls.
469
397
  */
470
398
  export function detectNapiRsEntry(entryFile) {
471
399
  if (!isAbsolute(entryFile) || !/\.[cm]?js$/.test(entryFile))
@@ -489,27 +417,19 @@ function stripJsExt(p) {
489
417
  return p.replace(/\.[cm]?js$/, '');
490
418
  }
491
419
  /**
492
- * Every path a napi-rs manifest names as its NATIVE entry — `main`, `module`,
493
- * and the `exports["."]` target(s) — with the `browser` condition deliberately
494
- * excluded (that is the wasm / pure-JS fallback, which must keep resolving
495
- * normally: it needs no addon and works on GJS as-is).
496
- *
497
- * Both halves of this matter, and each was a real miss:
420
+ * Every path a napi-rs manifest names as its NATIVE entry — `main`, `module` and the
421
+ * `exports["."]` target(s) — with `browser` deliberately excluded: that is the wasm /
422
+ * pure-JS fallback, which needs no addon and must keep resolving normally.
498
423
  *
499
- * - `main` ALONE is not enough. A dual package declares the CJS twin in
500
- * `main` while a bundler resolves the ESM one — `lightningcss` says
501
- * `main: "node/index.js"` and ships `node/index.mjs` beside it, which is
502
- * what `--app gjs` actually loads. Comparing only against `main` rejected
503
- * the very file being bundled, so the rewrite silently never happened and
504
- * the generated loader's runtime `require(\`lightningcss-${…}\`)` shipped
505
- * into the bundle.
506
- * - Matching is therefore extension-INSENSITIVE (`stripJsExt`): the `.js` /
507
- * `.mjs` / `.cjs` twins of a declared entry are the same generated loader.
508
- * It stays narrow because the path itself must still be one the manifest
509
- * names — a deep module inside the package never matches.
510
- *
511
- * Defaults to `index.js` (node's own default `main`) when the manifest names
512
- * nothing at all.
424
+ * `main` alone is NOT enough, and matching is extension-INSENSITIVE
425
+ * (`stripJsExt`), because a dual package declares the CJS twin in `main` while the
426
+ * bundler resolves the ESM one: `lightningcss` says `main: "node/index.js"` and
427
+ * ships `node/index.mjs` beside it, which is what `--app gjs` loads. Comparing only
428
+ * against `main` rejected the very file being bundled, so the rewrite silently never
429
+ * happened and the generated loader's runtime `require(\`lightningcss-${…}\`)`
430
+ * shipped into the bundle. Still narrow: the path must be one the manifest names, so
431
+ * a deep module never matches. Defaults to node's own `index.js` when the manifest
432
+ * names nothing.
513
433
  */
514
434
  function nativeEntrySpecs(pkg) {
515
435
  const specs = [];
@@ -563,33 +483,24 @@ function napiBinaryName(pkg) {
563
483
  return null;
564
484
  }
565
485
  /**
566
- * The name PREFIXES a napi-rs package's platform siblings can carry, most
567
- * specific first. A sibling is always `<prefix>-<triple>`.
568
- *
569
- * Two conventions are in the wild, and only knowing both covers the toolchain
570
- * this project builds on:
486
+ * The name PREFIXES a napi-rs package's platform siblings can carry, most specific
487
+ * first; a sibling is always `<prefix>-<triple>`. Two conventions ship in the wild
488
+ * and BOTH must be known:
571
489
  *
572
- * - `<self>-<triple>` — the sibling is named after the package itself
573
- * (`lightningcss` → `lightningcss-linux-x64-gnu`; `@node-rs/argon2` →
574
- * `@node-rs/argon2-linux-x64-gnu`). This was the only shape handled.
575
- * - `<napi.packageName>-<triple>` — napi-rs lets a package publish its
576
- * binaries under a SEPARATE scope, declared in the manifest's own napi
577
- * block. `rolldown` ships `@rolldown/binding-linux-x64-gnu`, `oxfmt`
578
- * ships `@oxfmt/binding-linux-x64-gnu`, `oxlint` ships
579
- * `@oxlint/binding-linux-x64-gnu` — none of which starts with the
580
- * package's own name, so the `<self>-` test missed every one of them.
490
+ * - `<self>-<triple>` — named after the package itself (`lightningcss` →
491
+ * `lightningcss-linux-x64-gnu`).
492
+ * - `<napi.packageName>-<triple>` — binaries published under a SEPARATE scope
493
+ * declared in the manifest's napi block (`rolldown` →
494
+ * `@rolldown/binding-linux-x64-gnu`, likewise `oxfmt`/`oxlint`), which does not
495
+ * start with the package's own name.
581
496
  *
582
- * That gap is why the three packages at the centre of this build chain
583
- * (bundler, formatter, linter) could not load transparently under GJS while
584
- * `lightningcss` could: `isNapiRsPackageJson` recognises all four (they all
585
- * carry a `napi` config object), so the ENTRY is detected — but with no
586
- * sibling resolvable, `resolveNapiRsEntryAddon` returns null and the rewrite
587
- * is skipped, leaving the generated loader's CJS body to ship into the bundle
588
- * and throw. Conservative by construction either way: a prefix only matters
589
- * when it also resolves to a real `.node`.
590
- *
591
- * `napi.packageName` is read defensively — an arbitrary manifest may put
592
- * anything there, and a non-string must not poison the prefix list.
497
+ * Knowing only `<self>-` is why the three packages at the centre of this build chain
498
+ * would not load under GJS while `lightningcss` did: their ENTRY is detected (all
499
+ * carry a `napi` object) but no sibling resolves, so `resolveNapiRsEntryAddon`
500
+ * returns null, the rewrite is skipped, and the generated loader's CJS body ships
501
+ * into the bundle and throws. Conservative either way — a prefix only matters when it
502
+ * also resolves to a real `.node`. `napi.packageName` is read defensively: a
503
+ * non-string in an arbitrary manifest must not poison the prefix list.
593
504
  */
594
505
  function napiSiblingPrefixes(pkg) {
595
506
  const prefixes = [];
@@ -604,10 +515,9 @@ function napiSiblingPrefixes(pkg) {
604
515
  return prefixes;
605
516
  }
606
517
  /**
607
- * Is `dep` a platform sibling of `pkg` — `<prefix>-<triple>` for one of the
608
- * prefixes above? Both halves must hold: a bare prefix match would claim an
609
- * ordinary dependency, and a bare triple match would claim an unrelated
610
- * package's binaries. Exported for the unit tests.
518
+ * Is `dep` a platform sibling of `pkg` — `<prefix>-<triple>` for one of the prefixes
519
+ * above? Both halves must hold: a bare prefix match would claim an ordinary
520
+ * dependency, a bare triple match an unrelated package's binaries.
611
521
  */
612
522
  export function isNapiRsSibling(pkg, dep) {
613
523
  if (!NAPI_RS_TRIPLE_RE.test(dep))
@@ -620,12 +530,9 @@ export function isNapiRsSibling(pkg, dep) {
620
530
  }
621
531
  /**
622
532
  * The napi-rs short platform triple for the CURRENT host (`linux-x64-gnu`,
623
- * `darwin-arm64`, `win32-x64-msvc`, …) — the tail napi-rs stamps into a sibling
624
- * package name (`@node-rs/argon2-linux-x64-gnu`) and a local binary
625
- * (`argon2.linux-x64-gnu.node`). Selects BOTH the sibling package and the
626
- * local-file fallback — see `resolveNapiRsEntryAddon` for why the sibling path
627
- * cannot just take whatever resolves. Returns null for a host napi-rs doesn't
628
- * name, and the caller then widens rather than guessing.
533
+ * `darwin-arm64`, …) — the tail napi-rs stamps into a sibling package name and a
534
+ * local binary, and the selector for both. Returns null for a host napi-rs does not
535
+ * name; the caller then widens rather than guessing.
629
536
  */
630
537
  export function hostNapiRsTriple() {
631
538
  const platform = process.platform;
@@ -668,36 +575,29 @@ function rawAddonPath(id) {
668
575
  }
669
576
  /**
670
577
  * Resolve the current-platform compiled `.node` for a napi-rs generated-loader
671
- * package: the current-triple sibling package (`@scope/pkg-<triple>`, whose own
672
- * `main` IS the `.node` — npm installs ONLY the host's optionalDependency, so
673
- * the one that resolves is the host's), then a local `pkg.<triple>.node`. Reuses
674
- * the plugin's `ctx.resolve` sibling-resolution path (`skipSelf` so this
675
- * plugin's own napi-rs-candidate interception is bypassed → a raw `.node` id).
676
- * Returns null when no current-platform binary is present — the caller then
677
- * DOES NOT rewrite, so we never shim over a missing file.
578
+ * package: the current-triple sibling package (whose own `main` IS the `.node`),
579
+ * then a local `pkg.<triple>.node`. `skipSelf` bypasses this plugin's own
580
+ * napi-rs-candidate interception so a raw `.node` id comes back. Returns null when
581
+ * no current-platform binary is present, so the caller never shims a missing file.
678
582
  */
679
583
  async function resolveNapiRsEntryAddon(ctx, pkgRoot, pkg, importer) {
680
584
  const siblings = Object.keys(pkg.optionalDependencies ?? {}).filter((dep) => isNapiRsSibling(pkg, dep));
681
585
  const triple = hostNapiRsTriple();
682
- // HOST TRIPLE FIRST, and alone whenever we can name it. The older code took
683
- // the first sibling that resolved, on the premise that "npm installs only
684
- // the host's optionalDependency, so the one that resolves is the host's".
685
- // That premise does not hold here: `gjsify install` materialises EVERY
686
- // platform package, so `lightningcss`'s `darwin-x64` sibling resolves on a
687
- // Linux box and won — baking a Mach-O `.dylib`-bearing `.node` into a linux
688
- // GJS bundle, which `loadAddon` can only fail on at runtime. Selecting by
689
- // the host triple is also what node-gyp-build and napi-rs' own generated
690
- // loaders do, so this matches the binary Node would have loaded.
586
+ // HOST TRIPLE ONLY whenever we can name it — never "the first sibling that
587
+ // resolves". `gjsify install` materialises EVERY platform package, so on a Linux
588
+ // box `lightningcss`'s `darwin-x64` sibling also resolves, and taking it bakes a
589
+ // Mach-O `.node` into a linux GJS bundle that `loadAddon` can only fail on at
590
+ // runtime. Host-triple selection is what node-gyp-build and napi-rs' own
591
+ // generated loaders do, so this matches the binary Node would have loaded.
691
592
  const ordered = triple === null ? siblings : siblings.filter((dep) => dep.endsWith(`-${triple}`));
692
593
  for (const dep of ordered) {
693
- // This resolve is a QUESTION ("is a host-triple binary installed?"), but it
694
- // can THROW rather than answer: `skipSelf` skips only THIS plugin, so the
594
+ // This resolve is a QUESTION ("is a host-triple binary installed?") that can
595
+ // THROW instead of answering: `skipSelf` skips only THIS plugin, so the
695
596
  // `unresolved-workspace-import` guard still runs at `order:'post'` and
696
597
  // (correctly, for a real import) makes an unresolvable bare `@gjsify/*`
697
- // FATAL. A misread sibling name then kills the whole build instead of
698
- // declining — measured on darwin, where every ADR-0017 platform package
699
- // matched this path. The name check above is the fix; this keeps the class
700
- // from ever being fatal again, and says which specifier was asked about.
598
+ // FATAL. A misread sibling name would then kill the build instead of
599
+ // declining — measured on darwin. `isNapiRsSibling` is the fix; catching here
600
+ // keeps the class from ever being fatal again.
701
601
  let resolved = null;
702
602
  try {
703
603
  resolved = await ctx.resolve(dep, importer, { skipSelf: true });
@@ -728,12 +628,11 @@ function warnSafe(ctx, msg) {
728
628
  ctx.warn(msg);
729
629
  }
730
630
  /**
731
- * Resolve a direct `.node` specifier to an EXISTING absolute path
732
- * (relative/bare/abs), or null when it does not resolve to a real file. Null is
733
- * load-bearing: a napi-rs generated loader statically references a local
734
- * `./pkg.<triple>.node` fallback that is absent when the binary ships in the
735
- * sibling platform package — that dead branch must fall through to normal
736
- * (external) resolution, NOT be rewritten to a shim over a missing file.
631
+ * Resolve a direct `.node` specifier to an EXISTING absolute path, else null. The
632
+ * null is load-bearing: a napi-rs generated loader statically references a local
633
+ * `./pkg.<triple>.node` fallback that is absent when the binary ships in the sibling
634
+ * platform package, and that dead branch must fall through to normal resolution
635
+ * rather than get a shim over a missing file.
737
636
  */
738
637
  async function resolveNodeFile(ctx, source, importer) {
739
638
  if (isAbsolute(source))
@@ -752,20 +651,17 @@ async function resolveNodeFile(ctx, source, importer) {
752
651
  return null;
753
652
  }
754
653
  /**
755
- * Transparent `.node`-addon loader for `gjsify build --app gjs`.
756
- *
757
- * resolveId (`order: 'pre'`, importer-aware) intercepts a native-addon
758
- * acquisition and rewrites it to a virtual module whose value is
759
- * `loadAddon('<abs .node>')`. Inert unless such a specifier is in the graph —
760
- * every other id returns null. Register ONLY for `--app gjs` (the C-ABI runs
761
- * under `@gjsify/napi`); never for node/browser/nativescript.
654
+ * Transparent `.node`-addon loader for `gjsify build --app gjs`: resolveId
655
+ * (`order: 'pre'`, importer-aware) rewrites a native-addon acquisition to a virtual
656
+ * module whose value is `loadAddon('<abs .node>')`, and returns null for everything
657
+ * else. Register ONLY for `--app gjs` — the C-ABI runs under `@gjsify/napi`.
762
658
  */
763
659
  export function napiNodeAddonPlugin(options = {}) {
764
660
  const warnOnMissingNapi = options.warnOnMissingNapi !== false;
765
661
  let missingNapiChecked = false;
766
- // Memoize napi-rs entry detection per resolved file — the `index.*` filter
767
- // fires the handler for every package's index entry across every build pass;
768
- // this bounds the package.json reads to one per unique entry file.
662
+ // Memoized per resolved file: the `index.*` filter fires the handler for every
663
+ // package's index entry on every build pass, so this bounds the package.json
664
+ // reads to one per unique entry file.
769
665
  const napiEntryCache = new Map();
770
666
  function detectNapiRsEntryCached(entryFile) {
771
667
  const cached = napiEntryCache.get(entryFile);
@@ -775,20 +671,18 @@ export function napiNodeAddonPlugin(options = {}) {
775
671
  napiEntryCache.set(entryFile, info);
776
672
  return info;
777
673
  }
778
- // Bare-specifier → entry-file resolution, memoized per (specifier, importer
779
- // DIRECTORY) since that pair is what node resolution actually depends on.
780
- // The widened filter offers every bare import here and almost none of them
781
- // is a native addon, so this cache is what keeps the extra `ctx.resolve` to
782
- // one per unique pair rather than one per import site.
674
+ // Memoized per (specifier, importer DIRECTORY) — the pair node resolution
675
+ // actually depends on. The filter offers every bare import and almost none is a
676
+ // native addon, so this keeps the extra `ctx.resolve` to one per unique pair
677
+ // rather than one per import site.
783
678
  const bareEntryCache = new Map();
784
679
  /**
785
680
  * The FILE a specifier denotes, for napi-rs entry detection:
786
- * - absolute path → itself (an already-resolved or hand-aliased entry).
787
- * - bare specifier → resolved through the full chain (so aliases and
788
- * export conditions apply — under `--app gjs` that is what picks a
789
- * package's `browser`/wasm fallback over its native entry, and
790
- * `detectNapiRsEntry` then correctly declines to rewrite it).
791
- * - anything else (relative paths) → null, unchanged from before.
681
+ * - absolute path → itself (already-resolved or hand-aliased entry).
682
+ * - bare specifier → resolved through the full chain, so aliases and export
683
+ * conditions apply — under `--app gjs` that is what picks a package's
684
+ * `browser`/wasm fallback, which `detectNapiRsEntry` then declines.
685
+ * - anything else (relative paths) → null.
792
686
  */
793
687
  async function entryFileFor(ctx, source, importer) {
794
688
  if (isAbsolute(source))
@@ -816,29 +710,23 @@ export function napiNodeAddonPlugin(options = {}) {
816
710
  return file;
817
711
  }
818
712
  /**
819
- * Is `@gjsify/napi` resolvable from the consumer graph? This is the GATE on
820
- * every rewrite, not a diagnostic — installing it (`gjsify install
821
- * @gjsify/napi`) is what a project uses to opt into napi routing.
822
- *
823
- * It used to only WARN, and the warning said the quiet part out loud: "the
824
- * bundle will fail at load". Emitting a knowingly-unloadable artifact is
825
- * never better than declining to rewrite — the untouched module at least
826
- * gets the default resolution, and a package with a JS/wasm fallback then
827
- * works. Two concrete failures made this a gate rather than a nicety:
713
+ * Is `@gjsify/napi` resolvable from the consumer graph? A GATE on every rewrite,
714
+ * not a diagnostic — installing it is how a project opts into napi routing.
715
+ * Declining to rewrite beats emitting a knowingly-unloadable artifact: the
716
+ * untouched module still gets default resolution, so a package with a JS/wasm
717
+ * fallback works. Two failures forced the gate:
828
718
  *
829
719
  * - The CLI's own `--app gjs` bundle reaches npm `lightningcss` through
830
- * `css-as-string`'s Node fallback branch (under GJS it deliberately
831
- * prefers `@gjsify/lightningcss-native`). Rewriting that branch put a
832
- * `require('@gjsify/napi')` into a bundle whose package does not depend
833
- * on `@gjsify/napi` at all.
834
- * - Worse, it made the COMMITTED bundle environment-dependent: with the
835
- * package present the shim inlines, without it the import goes external,
836
- * and the two module graphs minify to different variable names. That is
837
- * a `verify-committed-bundles` failure — the artifact stops reproducing
838
- * from its own source.
720
+ * `css-as-string`'s Node fallback branch (under GJS it prefers
721
+ * `@gjsify/lightningcss-native`). Rewriting that branch put a
722
+ * `require('@gjsify/napi')` into a bundle whose package does not depend on
723
+ * `@gjsify/napi`.
724
+ * - It made the COMMITTED bundle environment-dependent: present → the shim
725
+ * inlines, absent → the import goes external, and the two module graphs
726
+ * minify to different variable names. `verify-committed-bundles` then fails —
727
+ * the artifact stops reproducing from its own source.
839
728
  *
840
- * Probed once per build (interception is rare) and cached, so the extra
841
- * resolve costs nothing on the common path.
729
+ * Probed once per build and cached.
842
730
  */
843
731
  let napiAvailable = null;
844
732
  async function ensureNapiAvailable(ctx, importer) {
@@ -867,13 +755,11 @@ export function napiNodeAddonPlugin(options = {}) {
867
755
  filter: { id: ADDON_FILTER_RE },
868
756
  async handler(source, rawImporter) {
869
757
  const ctx = this;
870
- // The two engines disagree on "no importer": npm `rolldown`
871
- // passes `undefined`, `@gjsify/rolldown-native` passes `null`
872
- // (its hook payload round-trips through JSON, which has no
873
- // `undefined`). Every guard below is written `=== undefined`,
874
- // and `null` passes all of them — `dirname(null)` then threw
875
- // `The "path" argument must be of type string. Received type
876
- // object` and took the whole GJS build down as an
758
+ // The two engines disagree on "no importer": npm `rolldown` passes
759
+ // `undefined`, `@gjsify/rolldown-native` passes `null` (its hook
760
+ // payload round-trips through JSON, which has no `undefined`). Every
761
+ // guard below is `=== undefined`, which `null` passes, and
762
+ // `dirname(null)` then took the whole GJS build down as an
877
763
  // UNHANDLEABLE_ERROR. Normalise once, at the boundary.
878
764
  const importer = typeof rawImporter === 'string' ? rawImporter : undefined;
879
765
  const cls = classifySpecifier(source);
@@ -907,17 +793,11 @@ export function napiNodeAddonPlugin(options = {}) {
907
793
  const addonPath = resolveAddonPath(pkgRoot, { warn: (m) => warnSafe(ctx, m) }); // throws → build error
908
794
  return { id: encodeVirtual(cls.kind, addonPath) };
909
795
  }
910
- // napi-rs GENERATED-LOADER ENTRY (`index.*` filter). Replace the
911
- // whole module only when the resolved file IS the native `main`
912
- // of a napi-rs package (package.json signal) AND a current-platform
913
- // `.node` resolves. Otherwise fall through (never a shim over a
914
- // missing file, never an unrelated package's entry rewritten).
915
- // The specifier may be the entry PATH (an internal import, or a
916
- // caller that already aliased it) or the package's BARE name —
917
- // the ordinary `import { transform } from 'lightningcss'`. For
918
- // the bare form the entry file is only known after resolution,
919
- // so resolve first and test the resolved id. `entryFileFor`
920
- // memoizes both the resolve and the rejection.
796
+ // napi-rs GENERATED-LOADER ENTRY. The specifier may be the entry PATH
797
+ // (an internal or already-aliased import) or the package's BARE name,
798
+ // whose entry file is only known after resolution — so resolve first
799
+ // and test the resolved id. `entryFileFor` memoizes both the resolve
800
+ // and the rejection.
921
801
  const entryFile = await entryFileFor(ctx, source, importer);
922
802
  if (entryFile === null)
923
803
  return null;