@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.
- package/lib/app/gjs.d.ts +28 -54
- package/lib/app/gjs.js +124 -211
- package/lib/app/node.d.ts +31 -41
- package/lib/app/node.js +118 -221
- package/lib/plugins/css-as-string.js +10 -2
- package/lib/plugins/napi-node-addon.d.ts +46 -83
- package/lib/plugins/napi-node-addon.js +188 -308
- package/lib/plugins/process-stub.js +27 -1
- package/lib/plugins/rewrite-node-modules-paths.d.ts +12 -36
- package/lib/plugins/rewrite-node-modules-paths.js +60 -144
- package/lib/plugins/unresolved-workspace-import.d.ts +20 -38
- package/lib/plugins/unresolved-workspace-import.js +65 -157
- package/lib/shims/console-gjs.js +11 -19
- package/lib/utils/auto-globals.d.ts +56 -102
- package/lib/utils/auto-globals.js +100 -171
- package/lib/utils/detect-free-globals.d.ts +27 -46
- package/lib/utils/detect-free-globals.js +106 -185
- package/package.json +7 -7
|
@@ -1,72 +1,46 @@
|
|
|
1
|
-
// For `--app gjs`:
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// a
|
|
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`
|
|
27
|
-
//
|
|
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
|
-
//
|
|
33
|
-
// (
|
|
34
|
-
//
|
|
35
|
-
// `
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
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
|
-
//
|
|
54
|
-
//
|
|
55
|
-
// build
|
|
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
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
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
|
|
68
|
-
//
|
|
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`).
|
|
80
|
-
*
|
|
81
|
-
*
|
|
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
|
-
*
|
|
60
|
+
* `.`/`/`/`\`, no protocol (`node:fs`, `gi://Gtk`, `data:…`, `C:\…`), not a `\0`
|
|
61
|
+
* virtual id.
|
|
89
62
|
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
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,
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
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
|
|
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})
|
|
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
|
|
125
|
-
// `
|
|
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
|
-
//
|
|
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
|
-
*
|
|
264
|
-
* `prebuilds/<platform>-<arch>/<best tag>`. Throws {@link AddonNotBuiltError}
|
|
265
|
-
*
|
|
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
|
-
//
|
|
348
|
-
// `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
|
-
*
|
|
392
|
-
*
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
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:
|
|
411
|
-
*
|
|
412
|
-
*
|
|
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
|
-
*
|
|
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
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
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
|
-
*
|
|
440
|
-
*
|
|
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
|
-
*
|
|
464
|
-
*
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
*
|
|
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
|
-
*
|
|
494
|
-
*
|
|
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
|
-
*
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
*
|
|
504
|
-
*
|
|
505
|
-
*
|
|
506
|
-
*
|
|
507
|
-
*
|
|
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
|
-
*
|
|
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>` —
|
|
573
|
-
*
|
|
574
|
-
*
|
|
575
|
-
*
|
|
576
|
-
*
|
|
577
|
-
*
|
|
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
|
-
*
|
|
583
|
-
*
|
|
584
|
-
*
|
|
585
|
-
*
|
|
586
|
-
*
|
|
587
|
-
*
|
|
588
|
-
*
|
|
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
|
-
*
|
|
609
|
-
*
|
|
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`,
|
|
624
|
-
*
|
|
625
|
-
*
|
|
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 (
|
|
672
|
-
*
|
|
673
|
-
*
|
|
674
|
-
* the
|
|
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
|
|
683
|
-
//
|
|
684
|
-
//
|
|
685
|
-
//
|
|
686
|
-
//
|
|
687
|
-
//
|
|
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?")
|
|
694
|
-
//
|
|
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
|
|
698
|
-
// declining — measured on darwin
|
|
699
|
-
//
|
|
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
|
-
*
|
|
733
|
-
*
|
|
734
|
-
*
|
|
735
|
-
*
|
|
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
|
-
*
|
|
758
|
-
*
|
|
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
|
-
//
|
|
767
|
-
//
|
|
768
|
-
//
|
|
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
|
-
//
|
|
779
|
-
//
|
|
780
|
-
//
|
|
781
|
-
//
|
|
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 (
|
|
787
|
-
* - bare specifier → resolved through the full chain
|
|
788
|
-
*
|
|
789
|
-
*
|
|
790
|
-
*
|
|
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?
|
|
820
|
-
*
|
|
821
|
-
*
|
|
822
|
-
*
|
|
823
|
-
*
|
|
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
|
|
831
|
-
*
|
|
832
|
-
* `require('@gjsify/napi')` into a bundle whose package does not depend
|
|
833
|
-
*
|
|
834
|
-
* -
|
|
835
|
-
*
|
|
836
|
-
*
|
|
837
|
-
*
|
|
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
|
|
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
|
-
//
|
|
872
|
-
//
|
|
873
|
-
//
|
|
874
|
-
//
|
|
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
|
|
911
|
-
//
|
|
912
|
-
//
|
|
913
|
-
//
|
|
914
|
-
//
|
|
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;
|