@gjsify/rolldown-plugin-gjsify 0.52.0 → 0.54.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/README.md +11 -0
- package/lib/app/browser.js +1 -0
- package/lib/app/gjs.js +18 -2
- package/lib/app/nativescript.js +1 -1
- package/lib/app/node.d.ts +1 -1
- package/lib/app/node.js +34 -10
- package/lib/index.d.ts +5 -1
- package/lib/index.js +4 -1
- package/lib/plugins/console-assign.d.ts +13 -0
- package/lib/plugins/console-assign.js +125 -0
- package/lib/plugins/css-as-string.js +128 -44
- package/lib/plugins/gi-optional.d.ts +60 -0
- package/lib/plugins/gi-optional.js +152 -0
- package/lib/plugins/gi-renderer.d.ts +1 -1
- package/lib/plugins/gi-renderer.js +1 -1
- package/lib/plugins/gi-runtime-paths.js +5 -2
- package/lib/plugins/napi-node-addon.d.ts +48 -4
- package/lib/plugins/napi-node-addon.js +261 -85
- package/lib/plugins/node-native-external.d.ts +20 -0
- package/lib/plugins/node-native-external.js +145 -0
- package/lib/plugins/rewrite-node-modules-paths.d.ts +18 -1
- package/lib/plugins/rewrite-node-modules-paths.js +181 -25
- package/lib/plugins/unresolved-workspace-import.js +89 -68
- package/lib/shims/addon-resolve.d.ts +19 -0
- package/lib/shims/addon-resolve.js +193 -0
- package/lib/utils/addon-platform.d.ts +23 -0
- package/lib/utils/addon-platform.js +50 -0
- package/lib/utils/auto-globals.d.ts +6 -0
- package/lib/utils/auto-globals.js +13 -10
- package/lib/utils/declare-build-input.d.ts +13 -0
- package/lib/utils/declare-build-input.js +37 -0
- package/lib/utils/detect-free-globals.d.ts +3 -0
- package/lib/utils/detect-free-globals.js +1 -1
- package/lib/utils/entry-wrapper.js +6 -1
- package/lib/utils/index.d.ts +1 -0
- package/lib/utils/index.js +1 -0
- package/lib/utils/inline-static-reads.d.ts +15 -1
- package/lib/utils/inline-static-reads.js +16 -8
- package/lib/utils/scan-globals.d.ts +2 -2
- package/lib/utils/scan-globals.js +20 -14
- package/lib/utils/zip-path.d.ts +9 -0
- package/lib/utils/zip-path.js +12 -0
- package/package.json +13 -9
|
@@ -28,9 +28,14 @@
|
|
|
28
28
|
// own native `main` + a real host `.node` must resolve — and falls through to
|
|
29
29
|
// normal resolution otherwise, never shimming over a missing file.
|
|
30
30
|
//
|
|
31
|
-
//
|
|
32
|
-
// (
|
|
33
|
-
//
|
|
31
|
+
// What is baked into the bundle is the addon's PACKAGE IDENTITY, never a path
|
|
32
|
+
// (ADR 0084): `enumerateAddonTargets()` records every `.node` the addon package
|
|
33
|
+
// ships, keyed by platform, and the run-time resolver
|
|
34
|
+
// (`shims/addon-resolve.ts`) picks the entry for the host the bundle finds
|
|
35
|
+
// itself on and resolves it through the bundle's own location. Per-tuple tag
|
|
36
|
+
// selection stays node-gyp-build's (`selectPrebuildFile`), so the entry for the
|
|
37
|
+
// build host is the binary Node would load; `resolveAddonPath()` remains the
|
|
38
|
+
// single-file probe of that same order, kept as the public, build-host answer.
|
|
34
39
|
//
|
|
35
40
|
// The shims import `@gjsify/napi` by BARE SPECIFIER: it is a `gjs:polyfill`
|
|
36
41
|
// package that bundles normally, and its native typelib is auto-added to
|
|
@@ -42,8 +47,9 @@
|
|
|
42
47
|
// Portability (same as `gjs-gi-node.ts`): the `filter` is a Rolldown fast path;
|
|
43
48
|
// the handler's internal guard is the load-bearing check.
|
|
44
49
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
45
|
-
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
50
|
+
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
46
51
|
import { GJSIFY_VIRTUAL_PREFIX } from '../utils/virtual-module-id.js';
|
|
52
|
+
import { addonPlatformKey, normalizeNapiRsTriple } from '../utils/addon-platform.js';
|
|
47
53
|
const NAPI_ADDON_VIRTUAL_PREFIX = `${GJSIFY_VIRTUAL_PREFIX}napi-addon:`;
|
|
48
54
|
/** Bare specifier the shims import — resolved + bundled from the consumer graph. */
|
|
49
55
|
const NAPI_BARE_SPECIFIER = '@gjsify/napi';
|
|
@@ -99,10 +105,28 @@ function isMusl(platform) {
|
|
|
99
105
|
function hostTarget() {
|
|
100
106
|
const platform = process.env.npm_config_platform || process.platform;
|
|
101
107
|
const arch = process.env.npm_config_arch || process.arch;
|
|
102
|
-
|
|
108
|
+
return makeTarget(platform, arch, hostLibc(platform), process.versions ? process.versions.modules : undefined);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The host WITHOUT the `npm_config_*` override — the machine that is running
|
|
112
|
+
* the build, which is what `build/Release` and `build/Debug` are compiled FOR.
|
|
113
|
+
*
|
|
114
|
+
* Separate from {@link hostTarget} because a cross-build sets the override: a
|
|
115
|
+
* linux-arm64 bundle built on x64 selects its PREBUILDS for arm64 (that is the
|
|
116
|
+
* override's whole purpose, and it is what makes ADR 0084's cross-build clause
|
|
117
|
+
* work) while the binaries under `build/` are still x64. Keying those by the
|
|
118
|
+
* overridden target would have the bundle load an x64 `.node` on an arm64 host
|
|
119
|
+
* — a wrong claim rather than a missing one, which is the harder failure.
|
|
120
|
+
*/
|
|
121
|
+
function buildHostTarget() {
|
|
122
|
+
return makeTarget(process.platform, process.arch, hostLibc(process.platform), process.versions ? process.versions.modules : undefined);
|
|
123
|
+
}
|
|
124
|
+
function hostLibc(platform) {
|
|
125
|
+
return process.env.LIBC === 'musl' || isMusl(platform) ? 'musl' : 'glibc';
|
|
126
|
+
}
|
|
127
|
+
function makeTarget(platform, arch, libc, abi) {
|
|
103
128
|
const uv = ((process.versions && process.versions.uv) || '').split('.')[0] || '';
|
|
104
129
|
const armv = process.env.ARM_VERSION || (arch === 'arm64' ? '8' : '') || '';
|
|
105
|
-
const libc = process.env.LIBC === 'musl' || isMusl(platform) ? 'musl' : 'glibc';
|
|
106
130
|
return { platform, arch, libc, abi, uv, armv, runtime: 'node' };
|
|
107
131
|
}
|
|
108
132
|
function readdirSafe(dir) {
|
|
@@ -168,21 +192,33 @@ function runtimeAgnostic(tags) {
|
|
|
168
192
|
return tags.runtime === 'node' && tags.napi === true;
|
|
169
193
|
}
|
|
170
194
|
/**
|
|
171
|
-
*
|
|
172
|
-
*
|
|
195
|
+
* node-gyp-build's PER-TUPLE selection: the tags that match `host`, best first.
|
|
196
|
+
*
|
|
197
|
+
* ONE selector, and the reason it must be one: it is the whole correctness of
|
|
198
|
+
* the ADDON TABLE. The table is what the bundle loads at run time, so a tuple
|
|
199
|
+
* answered by anything weaker than node-gyp-build's own algorithm points the
|
|
200
|
+
* bundle at a binary Node would never load — and the failure is a `dlopen` at
|
|
201
|
+
* LAUNCH, on a user's machine, with the fix's own test suite green because the
|
|
202
|
+
* test asserts the table's SHAPE rather than its contents. Measured before this
|
|
203
|
+
* was extracted, on a `prebuilds/linux-x64/` holding one file of each kind:
|
|
204
|
+
*
|
|
205
|
+
* electron.node + node.node → table: electron.node (node-gyp-build: node.node)
|
|
206
|
+
* node.abi115.node + node.node → table: node.abi115.node (node-gyp-build: node.node)
|
|
207
|
+
*
|
|
208
|
+
* Both because the previous enumeration sorted by SPECIFICITY alone: `electron`
|
|
209
|
+
* and `abi115` each score 1 and `node` scores 0, so the foreign-runtime binary
|
|
210
|
+
* outranked the right one. The filter below is node-gyp-build's `parseTags` +
|
|
211
|
+
* `matches`, ported verbatim, tolerant of a missing `abi`.
|
|
212
|
+
*
|
|
213
|
+
* The FILE NAME is the final tiebreak and is ours, not node-gyp-build's: its
|
|
214
|
+
* comparator ends in a `0`, so a same-specificity pair is decided by `readdir`
|
|
215
|
+
* order — which is a property of the FILESYSTEM, not of the tree. The table is
|
|
216
|
+
* `JSON.stringify`'d into the bundle, so an unsorted tie made the artifact's
|
|
217
|
+
* bytes vary by where it was built. Compared with `<`/`>`, never
|
|
218
|
+
* `localeCompare`: that would trade one filesystem dependency for a locale one.
|
|
173
219
|
*/
|
|
174
|
-
function
|
|
175
|
-
|
|
176
|
-
const tuple = readdirSafe(prebuildsDir)
|
|
177
|
-
.map(parseTuple)
|
|
178
|
-
.filter((t) => t !== null && t.platform === host.platform && t.architectures.includes(host.arch))
|
|
179
|
-
// Prefer single-arch prebuilds over multi-arch (compareTuples).
|
|
180
|
-
.sort((a, b) => a.architectures.length - b.architectures.length)[0];
|
|
181
|
-
if (!tuple)
|
|
182
|
-
return null;
|
|
183
|
-
const tupleDir = join(prebuildsDir, tuple.name);
|
|
184
|
-
const winner = readdirSafe(tupleDir)
|
|
185
|
-
.map(parseTags)
|
|
220
|
+
function selectPrebuildFile(files, host) {
|
|
221
|
+
return (files
|
|
186
222
|
.filter((t) => {
|
|
187
223
|
if (t === null)
|
|
188
224
|
return false;
|
|
@@ -208,8 +244,24 @@ function resolvePrebuild(pkgRoot, host) {
|
|
|
208
244
|
return a.abi ? -1 : 1;
|
|
209
245
|
if (a.specificity !== b.specificity)
|
|
210
246
|
return a.specificity > b.specificity ? -1 : 1;
|
|
211
|
-
return 0;
|
|
212
|
-
})[0];
|
|
247
|
+
return a.file < b.file ? -1 : a.file > b.file ? 1 : 0;
|
|
248
|
+
})[0] ?? null);
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Resolve the best `prebuilds/<platform>-<arch>/<file>.node` for `pkgRoot`,
|
|
252
|
+
* ported from node-gyp-build's `resolve(dir)` — tolerant of a missing `abi`.
|
|
253
|
+
*/
|
|
254
|
+
function resolvePrebuild(pkgRoot, host) {
|
|
255
|
+
const prebuildsDir = join(pkgRoot, 'prebuilds');
|
|
256
|
+
const tuple = readdirSafe(prebuildsDir)
|
|
257
|
+
.map(parseTuple)
|
|
258
|
+
.filter((t) => t !== null && t.platform === host.platform && t.architectures.includes(host.arch))
|
|
259
|
+
// Prefer single-arch prebuilds over multi-arch (compareTuples).
|
|
260
|
+
.sort((a, b) => a.architectures.length - b.architectures.length)[0];
|
|
261
|
+
if (!tuple)
|
|
262
|
+
return null;
|
|
263
|
+
const tupleDir = join(prebuildsDir, tuple.name);
|
|
264
|
+
const winner = selectPrebuildFile(readdirSafe(tupleDir).map(parseTags), host);
|
|
213
265
|
return winner ? join(tupleDir, winner.file) : null;
|
|
214
266
|
}
|
|
215
267
|
/** A native addon package root has no resolvable compiled `.node`. */
|
|
@@ -220,6 +272,74 @@ export class AddonNotBuiltError extends Error {
|
|
|
220
272
|
this.name = 'AddonNotBuiltError';
|
|
221
273
|
}
|
|
222
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* Enumerate every `.node` an addon package ships, keyed by platform, as a
|
|
277
|
+
* `<package>/<subpath>` spec per entry. ADR 0084: the build ENUMERATES, it
|
|
278
|
+
* does not select — the bundle picks the right entry at RUN time from the
|
|
279
|
+
* host it finds itself on.
|
|
280
|
+
*
|
|
281
|
+
* The table has:
|
|
282
|
+
* - One entry per `prebuilds/<tuple>/` directory, keyed by the tuple's
|
|
283
|
+
* platform and EVERY architecture it declares, and per libc variant. Each
|
|
284
|
+
* entry is chosen by {@link selectPrebuildFile} — node-gyp-build's own
|
|
285
|
+
* algorithm, run for a synthetic host of that tuple — so the entry for the
|
|
286
|
+
* build host is exactly the binary Node would load, and a foreign platform's
|
|
287
|
+
* entry is the best one that host could load rather than the best-looking
|
|
288
|
+
* file in the directory.
|
|
289
|
+
* - One entry per `build/Release` and `build/Debug`, keyed by the BUILD host
|
|
290
|
+
* ({@link buildHostTarget}, deliberately not the `npm_config_*` override
|
|
291
|
+
* those files are not built for), overriding the prebuilds entry for the
|
|
292
|
+
* same key: node-gyp-build's order has build/Release winning.
|
|
293
|
+
*
|
|
294
|
+
* Returns an empty record when no `.node` exists anywhere — the caller then
|
|
295
|
+
* throws {@link AddonNotBuiltError} as a build-time gate.
|
|
296
|
+
*/
|
|
297
|
+
export function enumerateAddonTargets(pkgRoot, pkg) {
|
|
298
|
+
const targets = {};
|
|
299
|
+
const pkgName = typeof pkg.name === 'string' && pkg.name ? pkg.name : null;
|
|
300
|
+
if (!pkgName)
|
|
301
|
+
return targets;
|
|
302
|
+
// 1. prebuilds/<tuple>/<best tag>, per tuple, per architecture, per libc.
|
|
303
|
+
// `readdirSafe` is sorted: the table is `JSON.stringify`'d into the
|
|
304
|
+
// bundle, so its key ORDER is part of the artifact's bytes and must come
|
|
305
|
+
// from the tree rather than from the filesystem's directory order.
|
|
306
|
+
const prebuildsDir = join(pkgRoot, 'prebuilds');
|
|
307
|
+
const tuples = readdirSafe(prebuildsDir)
|
|
308
|
+
.sort()
|
|
309
|
+
.map(parseTuple)
|
|
310
|
+
.filter((t) => t !== null);
|
|
311
|
+
for (const tuple of tuples) {
|
|
312
|
+
const files = readdirSafe(join(prebuildsDir, tuple.name)).map(parseTags);
|
|
313
|
+
if (!files.some((t) => t !== null))
|
|
314
|
+
continue;
|
|
315
|
+
for (const arch of tuple.architectures) {
|
|
316
|
+
for (const libc of ['glibc', 'musl']) {
|
|
317
|
+
// `abi: undefined` on purpose: the only prebuilds a FOREIGN
|
|
318
|
+
// platform can be served are the runtime-agnostic ones. An
|
|
319
|
+
// `abi<N>.node` matching this build's Node would be selected
|
|
320
|
+
// otherwise, and it is not loadable by that host's runtime at
|
|
321
|
+
// all — the wrong binary beats a missing one.
|
|
322
|
+
const best = selectPrebuildFile(files, makeTarget(tuple.platform, arch, libc, undefined));
|
|
323
|
+
if (best === null)
|
|
324
|
+
continue;
|
|
325
|
+
targets[addonPlatformKey(tuple.platform, arch, libc)] =
|
|
326
|
+
`${pkgName}/${subpathOf(pkgRoot, join(prebuildsDir, tuple.name, best.file))}`;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// 2. build/Release + build/Debug for the BUILD host (overrides prebuilds).
|
|
331
|
+
const host = buildHostTarget();
|
|
332
|
+
const hostKey = addonPlatformKey(host.platform, host.arch, host.libc);
|
|
333
|
+
for (const flavor of ['Release', 'Debug']) {
|
|
334
|
+
const dir = join(pkgRoot, 'build', flavor);
|
|
335
|
+
const hit = firstNodeFile(dir);
|
|
336
|
+
if (hit) {
|
|
337
|
+
targets[hostKey] = `${pkgName}/${subpathOf(pkgRoot, join(dir, hit.file))}`;
|
|
338
|
+
break; // Release wins
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return targets;
|
|
342
|
+
}
|
|
223
343
|
/**
|
|
224
344
|
* Locate the compiled `.node` for an addon package root, matching node-gyp-build's
|
|
225
345
|
* probe order: `build/Release` → `build/Debug` →
|
|
@@ -259,47 +379,53 @@ export function nearestPackageRoot(importerFile) {
|
|
|
259
379
|
}
|
|
260
380
|
return null;
|
|
261
381
|
}
|
|
382
|
+
/** The addon-resolve shim specifier — resolved by the consumer's build. */
|
|
383
|
+
const ADDON_RESOLVE_SHIM = '@gjsify/rolldown-plugin-gjsify/shims/addon-resolve';
|
|
262
384
|
/** Direct `.node` import → the addon's exports (ESM default). */
|
|
263
|
-
export function directNodeShim(
|
|
385
|
+
export function directNodeShim(addonTable) {
|
|
264
386
|
return (`import { loadAddon } from ${JSON.stringify(NAPI_BARE_SPECIFIER)};\n` +
|
|
265
|
-
`
|
|
387
|
+
`import { __gjsifyAddonResolve } from ${JSON.stringify(ADDON_RESOLVE_SHIM)};\n` +
|
|
388
|
+
`export default loadAddon(__gjsifyAddonResolve(${addonTable}));\n`);
|
|
266
389
|
}
|
|
267
390
|
/** `node-gyp-build` replacement — a callable `load(dir)` carrying `.path()`. */
|
|
268
|
-
export function nodeGypBuildShim(
|
|
391
|
+
export function nodeGypBuildShim(addonTable) {
|
|
269
392
|
return (`const { loadAddon } = require(${JSON.stringify(NAPI_BARE_SPECIFIER)});\n` +
|
|
270
|
-
`
|
|
271
|
-
`
|
|
393
|
+
`const { __gjsifyAddonResolve } = require(${JSON.stringify(ADDON_RESOLVE_SHIM)});\n` +
|
|
394
|
+
`function load() { return loadAddon(__gjsifyAddonResolve(${addonTable})); }\n` +
|
|
395
|
+
`load.path = function () { return __gjsifyAddonResolve(${addonTable}); };\n` +
|
|
272
396
|
`load.resolve = load.path;\n` +
|
|
273
397
|
`module.exports = load;\n`);
|
|
274
398
|
}
|
|
275
399
|
/** `bindings` replacement — a callable `bindings(name)` returning the addon. */
|
|
276
|
-
export function bindingsShim(
|
|
400
|
+
export function bindingsShim(addonTable) {
|
|
277
401
|
return (`const { loadAddon } = require(${JSON.stringify(NAPI_BARE_SPECIFIER)});\n` +
|
|
278
|
-
`
|
|
402
|
+
`const { __gjsifyAddonResolve } = require(${JSON.stringify(ADDON_RESOLVE_SHIM)});\n` +
|
|
403
|
+
`function bindings() { return loadAddon(__gjsifyAddonResolve(${addonTable})); }\n` +
|
|
279
404
|
`module.exports = bindings;\n`);
|
|
280
405
|
}
|
|
281
406
|
/** napi-rs sibling → the raw native exports as the module value. */
|
|
282
|
-
export function napiRsShim(
|
|
407
|
+
export function napiRsShim(addonTable) {
|
|
283
408
|
return (`const { loadAddon } = require(${JSON.stringify(NAPI_BARE_SPECIFIER)});\n` +
|
|
284
|
-
`
|
|
409
|
+
`const { __gjsifyAddonResolve } = require(${JSON.stringify(ADDON_RESOLVE_SHIM)});\n` +
|
|
410
|
+
`module.exports = loadAddon(__gjsifyAddonResolve(${addonTable}));\n`);
|
|
285
411
|
}
|
|
286
|
-
function shimFor(kind,
|
|
412
|
+
function shimFor(kind, addonTable) {
|
|
287
413
|
switch (kind) {
|
|
288
414
|
case 'direct':
|
|
289
|
-
return directNodeShim(
|
|
415
|
+
return directNodeShim(addonTable);
|
|
290
416
|
case 'node-gyp-build':
|
|
291
|
-
return nodeGypBuildShim(
|
|
417
|
+
return nodeGypBuildShim(addonTable);
|
|
292
418
|
case 'bindings':
|
|
293
|
-
return bindingsShim(
|
|
419
|
+
return bindingsShim(addonTable);
|
|
294
420
|
case 'napi-rs':
|
|
295
421
|
case 'napi-rs-entry':
|
|
296
422
|
// Same body: `napi-rs-entry` replaces the whole GENERATED loader,
|
|
297
423
|
// `napi-rs` a directly-imported platform sibling.
|
|
298
|
-
return napiRsShim(
|
|
424
|
+
return napiRsShim(addonTable);
|
|
299
425
|
}
|
|
300
426
|
}
|
|
301
|
-
function encodeVirtual(kind,
|
|
302
|
-
return `${NAPI_ADDON_VIRTUAL_PREFIX}${kind}:${
|
|
427
|
+
function encodeVirtual(kind, addonTable) {
|
|
428
|
+
return `${NAPI_ADDON_VIRTUAL_PREFIX}${kind}:${addonTable}`;
|
|
303
429
|
}
|
|
304
430
|
function decodeVirtual(id) {
|
|
305
431
|
if (!id.startsWith(NAPI_ADDON_VIRTUAL_PREFIX))
|
|
@@ -309,8 +435,8 @@ function decodeVirtual(id) {
|
|
|
309
435
|
if (sep === -1)
|
|
310
436
|
return null;
|
|
311
437
|
const kind = rest.slice(0, sep);
|
|
312
|
-
const
|
|
313
|
-
return { kind,
|
|
438
|
+
const addonTable = rest.slice(sep + 1);
|
|
439
|
+
return { kind, addonTable };
|
|
314
440
|
}
|
|
315
441
|
/** Classify a specifier for interception — pure decision logic, no filesystem. */
|
|
316
442
|
export function classifySpecifier(source) {
|
|
@@ -568,59 +694,84 @@ export function hostNapiRsTriple() {
|
|
|
568
694
|
return null;
|
|
569
695
|
}
|
|
570
696
|
}
|
|
571
|
-
/**
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
697
|
+
/**
|
|
698
|
+
* The path of `abs` inside `pkgRoot`, as a `/`-separated MODULE SUBPATH.
|
|
699
|
+
*
|
|
700
|
+
* `relative()` answers in the HOST's separator, so on win32 the table carried
|
|
701
|
+
* `pkg/prebuilds\\win32-x64\\node.napi.node` while every other value in it — and
|
|
702
|
+
* the resolver's own `splitPackageSpec`, which splits on `/` — is `/`-separated.
|
|
703
|
+
* `join` happened to absorb the difference, so the bundle still loaded; what did
|
|
704
|
+
* not survive is the table as BYTES: the same tree then serialised differently
|
|
705
|
+
* per platform, which is the reproducibility `verify-committed-bundles` reads.
|
|
706
|
+
*/
|
|
707
|
+
function subpathOf(pkgRoot, abs) {
|
|
708
|
+
return relative(pkgRoot, abs).split('\\').join('/');
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* The `<package>/<subpath>` spec for an absolute `.node` file path — the part
|
|
712
|
+
* after the LAST `node_modules/` segment, which is what the runtime resolver
|
|
713
|
+
* feeds to `createRequire(...).resolve`. Always a module SPECIFIER, so always
|
|
714
|
+
* `/`-separated.
|
|
715
|
+
*
|
|
716
|
+
* A path under no `node_modules` (a direct import of a locally built `.node`)
|
|
717
|
+
* has no package identity to record, so the path itself is the spec and the
|
|
718
|
+
* resolver returns it unchanged. That is the one case ADR 0084's premise does
|
|
719
|
+
* not reach, and it is not a regression: before this ADR the absolute path was
|
|
720
|
+
* baked in and the bundle loaded the file. It only stops being RELOCATABLE, so
|
|
721
|
+
* the build says so once rather than shipping it silently.
|
|
722
|
+
*/
|
|
723
|
+
function packageSpecFor(absPath, warn) {
|
|
724
|
+
const normalized = process.platform === 'win32' ? absPath.replaceAll('\\', '/') : absPath;
|
|
725
|
+
const marker = 'node_modules/';
|
|
726
|
+
const idx = normalized.lastIndexOf(marker);
|
|
727
|
+
if (idx >= 0)
|
|
728
|
+
return normalized.slice(idx + marker.length);
|
|
729
|
+
warn?.(`[gjsify-napi-addon] '${absPath}' is not inside a node_modules, so the bundle carries its ` +
|
|
730
|
+
'ABSOLUTE path and only loads where it was built. Move the addon into a package (or ' +
|
|
731
|
+
'install one that ships it) for a bundle that travels.');
|
|
732
|
+
return normalized;
|
|
575
733
|
}
|
|
576
734
|
/**
|
|
577
|
-
*
|
|
578
|
-
*
|
|
579
|
-
*
|
|
580
|
-
*
|
|
581
|
-
*
|
|
735
|
+
* Enumerate every napi-rs platform sibling of `pkg` that resolves to a `.node`,
|
|
736
|
+
* keyed by the sibling's platform triple. ADR 0084: the build ENUMERATES every
|
|
737
|
+
* installed sibling, so the bundle picks the right one at RUN time.
|
|
738
|
+
*
|
|
739
|
+
* Returns null when no sibling resolves — the caller then falls through to
|
|
740
|
+
* normal resolution (never a shim over nothing).
|
|
582
741
|
*/
|
|
583
|
-
async function
|
|
742
|
+
async function enumerateNapiRsEntryTargets(ctx, pkgRoot, pkg, importer) {
|
|
584
743
|
const siblings = Object.keys(pkg.optionalDependencies ?? {}).filter((dep) => isNapiRsSibling(pkg, dep));
|
|
585
|
-
const
|
|
586
|
-
|
|
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.
|
|
592
|
-
const ordered = triple === null ? siblings : siblings.filter((dep) => dep.endsWith(`-${triple}`));
|
|
593
|
-
for (const dep of ordered) {
|
|
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
|
|
596
|
-
// `unresolved-workspace-import` guard still runs at `order:'post'` and
|
|
597
|
-
// (correctly, for a real import) makes an unresolvable bare `@gjsify/*`
|
|
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.
|
|
744
|
+
const targets = {};
|
|
745
|
+
for (const dep of siblings) {
|
|
601
746
|
let resolved = null;
|
|
602
747
|
try {
|
|
603
748
|
resolved = await ctx.resolve(dep, importer, { skipSelf: true });
|
|
604
749
|
}
|
|
605
|
-
catch
|
|
606
|
-
warnSafe(ctx, `[gjsify-napi] probing the platform sibling "${dep}" of "${pkg.name ?? pkgRoot}" failed; not rewriting its entry (${err instanceof Error ? err.message.split('\n')[0] : String(err)})`);
|
|
750
|
+
catch {
|
|
607
751
|
continue;
|
|
608
752
|
}
|
|
609
|
-
if (!resolved)
|
|
753
|
+
if (!resolved || !resolved.id.endsWith('.node') || !existsSync(resolved.id))
|
|
610
754
|
continue;
|
|
611
|
-
const
|
|
612
|
-
|
|
613
|
-
|
|
755
|
+
const spec = packageSpecFor(resolved.id, (m) => warnSafe(ctx, m));
|
|
756
|
+
// Extract the triple from the sibling name (`<prefix>-<triple>`).
|
|
757
|
+
const match = dep.match(NAPI_RS_TRIPLE_RE);
|
|
758
|
+
const triple = match ? match[0].slice(1) : null;
|
|
759
|
+
if (!triple)
|
|
760
|
+
continue;
|
|
761
|
+
const key = normalizeNapiRsTriple(triple);
|
|
762
|
+
targets[key] = spec;
|
|
614
763
|
}
|
|
615
|
-
// Local in-package binary (`<binaryName>.<
|
|
616
|
-
|
|
764
|
+
// Local in-package binary (`<binaryName>.<triple>.node`) — host triple only.
|
|
765
|
+
const triple = hostNapiRsTriple();
|
|
617
766
|
const binaryName = napiBinaryName(pkg);
|
|
618
767
|
if (binaryName && triple) {
|
|
619
768
|
const local = join(pkgRoot, `${binaryName}.${triple}.node`);
|
|
620
|
-
if (existsSync(local))
|
|
621
|
-
|
|
769
|
+
if (existsSync(local)) {
|
|
770
|
+
const spec = packageSpecFor(local, (m) => warnSafe(ctx, m));
|
|
771
|
+
targets[normalizeNapiRsTriple(triple)] = spec;
|
|
772
|
+
}
|
|
622
773
|
}
|
|
623
|
-
return null;
|
|
774
|
+
return Object.keys(targets).length > 0 ? targets : null;
|
|
624
775
|
}
|
|
625
776
|
/** Emit a non-fatal warning through the context, if it supports it. */
|
|
626
777
|
function warnSafe(ctx, msg) {
|
|
@@ -658,7 +809,9 @@ async function resolveNodeFile(ctx, source, importer) {
|
|
|
658
809
|
*/
|
|
659
810
|
export function napiNodeAddonPlugin(options = {}) {
|
|
660
811
|
const warnOnMissingNapi = options.warnOnMissingNapi !== false;
|
|
812
|
+
const runtimeResolve = options.runtimeResolve !== false;
|
|
661
813
|
let missingNapiChecked = false;
|
|
814
|
+
let unanchoredChecked = false;
|
|
662
815
|
// Memoized per resolved file: the `index.*` filter fires the handler for every
|
|
663
816
|
// package's index entry on every build pass, so this bounds the package.json
|
|
664
817
|
// reads to one per unique entry file.
|
|
@@ -762,6 +915,21 @@ export function napiNodeAddonPlugin(options = {}) {
|
|
|
762
915
|
// `dirname(null)` then took the whole GJS build down as an
|
|
763
916
|
// UNHANDLEABLE_ERROR. Normalise once, at the boundary.
|
|
764
917
|
const importer = typeof rawImporter === 'string' ? rawImporter : undefined;
|
|
918
|
+
// One gate ahead of the `@gjsify/napi` gate: without the
|
|
919
|
+
// bundle-URL banner the run-time resolver has nothing to anchor
|
|
920
|
+
// on and would throw at LOAD. Declining leaves the module to
|
|
921
|
+
// normal resolution, which is the same shape as the missing-napi
|
|
922
|
+
// decline below and for the same reason — a knowingly
|
|
923
|
+
// unloadable artifact is worse than an unrewritten one.
|
|
924
|
+
if (!runtimeResolve) {
|
|
925
|
+
if (warnOnMissingNapi && !unanchoredChecked) {
|
|
926
|
+
unanchoredChecked = true;
|
|
927
|
+
warnSafe(ctx, `[gjsify-napi-addon] leaving native addons to normal resolution — the run-time ` +
|
|
928
|
+
`addon resolver needs an ESM single-file build (gjsify build --app gjs), and ` +
|
|
929
|
+
'this output carries no bundle-URL anchor.');
|
|
930
|
+
}
|
|
931
|
+
return null;
|
|
932
|
+
}
|
|
765
933
|
const cls = classifySpecifier(source);
|
|
766
934
|
if (cls !== null) {
|
|
767
935
|
// Direct `.node` — resolve the file path itself.
|
|
@@ -771,7 +939,8 @@ export function napiNodeAddonPlugin(options = {}) {
|
|
|
771
939
|
return null; // unresolvable — let the default chain error
|
|
772
940
|
if (!(await ensureNapiAvailable(ctx, importer)))
|
|
773
941
|
return null;
|
|
774
|
-
|
|
942
|
+
const spec = packageSpecFor(abs, (m) => warnSafe(ctx, m));
|
|
943
|
+
return { id: encodeVirtual('direct', JSON.stringify({ '*': spec })) };
|
|
775
944
|
}
|
|
776
945
|
// napi-rs platform sibling — confirm it resolves to a `.node`.
|
|
777
946
|
if (cls.kind === 'napi-rs-candidate') {
|
|
@@ -780,7 +949,8 @@ export function napiNodeAddonPlugin(options = {}) {
|
|
|
780
949
|
return null; // not a native sibling
|
|
781
950
|
if (!(await ensureNapiAvailable(ctx, importer)))
|
|
782
951
|
return null;
|
|
783
|
-
|
|
952
|
+
const spec = packageSpecFor(resolved.id, (m) => warnSafe(ctx, m));
|
|
953
|
+
return { id: encodeVirtual('napi-rs', JSON.stringify({ '*': spec })) };
|
|
784
954
|
}
|
|
785
955
|
// node-gyp-build / bindings — probe the importer's package root.
|
|
786
956
|
if (importer === undefined)
|
|
@@ -790,8 +960,14 @@ export function napiNodeAddonPlugin(options = {}) {
|
|
|
790
960
|
return null;
|
|
791
961
|
if (!(await ensureNapiAvailable(ctx, importer)))
|
|
792
962
|
return null;
|
|
793
|
-
const
|
|
794
|
-
|
|
963
|
+
const pkg = readPackageJsonSafe(pkgRoot);
|
|
964
|
+
if (pkg === null)
|
|
965
|
+
return null;
|
|
966
|
+
const table = enumerateAddonTargets(pkgRoot, pkg);
|
|
967
|
+
if (Object.keys(table).length === 0) {
|
|
968
|
+
throw new AddonNotBuiltError(pkgRoot);
|
|
969
|
+
}
|
|
970
|
+
return { id: encodeVirtual(cls.kind, JSON.stringify(table)) };
|
|
795
971
|
}
|
|
796
972
|
// napi-rs GENERATED-LOADER ENTRY. The specifier may be the entry PATH
|
|
797
973
|
// (an internal or already-aliased import) or the package's BARE name,
|
|
@@ -803,11 +979,11 @@ export function napiNodeAddonPlugin(options = {}) {
|
|
|
803
979
|
return null;
|
|
804
980
|
const entry = detectNapiRsEntryCached(entryFile);
|
|
805
981
|
if (entry !== null) {
|
|
806
|
-
const
|
|
807
|
-
if (
|
|
982
|
+
const table = await enumerateNapiRsEntryTargets(ctx, entry.pkgRoot, entry.pkg, entryFile);
|
|
983
|
+
if (table !== null) {
|
|
808
984
|
if (!(await ensureNapiAvailable(ctx, importer)))
|
|
809
985
|
return null;
|
|
810
|
-
return { id: encodeVirtual('napi-rs-entry',
|
|
986
|
+
return { id: encodeVirtual('napi-rs-entry', JSON.stringify(table)) };
|
|
811
987
|
}
|
|
812
988
|
}
|
|
813
989
|
return null;
|
|
@@ -817,7 +993,7 @@ export function napiNodeAddonPlugin(options = {}) {
|
|
|
817
993
|
const decoded = decodeVirtual(id);
|
|
818
994
|
if (decoded === null)
|
|
819
995
|
return null;
|
|
820
|
-
return { code: shimFor(decoded.kind, decoded.
|
|
996
|
+
return { code: shimFor(decoded.kind, decoded.addonTable), moduleSideEffects: false };
|
|
821
997
|
},
|
|
822
998
|
};
|
|
823
999
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Plugin } from 'rolldown';
|
|
2
|
+
import { type AddonPackageJson } from './napi-node-addon.js';
|
|
3
|
+
interface NativePackageJson extends AddonPackageJson {
|
|
4
|
+
gypfile?: boolean;
|
|
5
|
+
binary?: unknown;
|
|
6
|
+
dependencies?: Record<string, string>;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Is the package at `pkgRoot` a Node-API addon — one whose code loads a `.node`
|
|
10
|
+
* relative to its own directory? True on any of: `gypfile`, a `binding.gyp`, a
|
|
11
|
+
* node-pre-gyp `binary` block, a dependency on an addon loader, the napi-rs manifest
|
|
12
|
+
* signals, or a `.node` under `prebuilds/` or `build/`. A gjsify native bridge is
|
|
13
|
+
* never one: it ships a GI typelib loaded through `gi://`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function isNodeAddonPackage(pkgRoot: string, pkg: NativePackageJson): boolean;
|
|
16
|
+
/** `@scope/name/sub` → `@scope/name`, `name/sub` → `name`; null for anything not bare. */
|
|
17
|
+
export declare function packageNameOf(specifier: string): string | null;
|
|
18
|
+
/** Keeps every import of a Node-API addon package external on `--app node`. */
|
|
19
|
+
export declare function nodeNativeExternalPlugin(): Plugin;
|
|
20
|
+
export {};
|