@gjsify/rolldown-plugin-gjsify 0.51.1 → 0.53.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.
Files changed (44) hide show
  1. package/README.md +11 -0
  2. package/lib/app/browser.js +28 -0
  3. package/lib/app/gjs.js +14 -2
  4. package/lib/app/nativescript.js +15 -3
  5. package/lib/app/node.d.ts +1 -1
  6. package/lib/app/node.js +26 -10
  7. package/lib/index.d.ts +4 -2
  8. package/lib/index.js +4 -2
  9. package/lib/plugins/console-assign.d.ts +13 -0
  10. package/lib/plugins/console-assign.js +125 -0
  11. package/lib/plugins/css-as-string.js +126 -44
  12. package/lib/plugins/gi-renderer.d.ts +1 -1
  13. package/lib/plugins/gi-renderer.js +1 -1
  14. package/lib/plugins/gi-runtime-paths.js +5 -2
  15. package/lib/plugins/napi-node-addon.d.ts +48 -4
  16. package/lib/plugins/napi-node-addon.js +261 -85
  17. package/lib/plugins/node-native-external.d.ts +20 -0
  18. package/lib/plugins/node-native-external.js +145 -0
  19. package/lib/plugins/platform-resolve.d.ts +95 -5
  20. package/lib/plugins/platform-resolve.js +132 -11
  21. package/lib/plugins/rewrite-node-modules-paths.d.ts +18 -1
  22. package/lib/plugins/rewrite-node-modules-paths.js +181 -25
  23. package/lib/plugins/unresolved-workspace-import.js +89 -68
  24. package/lib/shims/addon-resolve.d.ts +19 -0
  25. package/lib/shims/addon-resolve.js +193 -0
  26. package/lib/utils/addon-platform.d.ts +23 -0
  27. package/lib/utils/addon-platform.js +50 -0
  28. package/lib/utils/auto-globals.d.ts +6 -0
  29. package/lib/utils/auto-globals.js +92 -15
  30. package/lib/utils/declare-build-input.d.ts +13 -0
  31. package/lib/utils/declare-build-input.js +37 -0
  32. package/lib/utils/detect-free-globals.d.ts +3 -0
  33. package/lib/utils/detect-free-globals.js +1 -1
  34. package/lib/utils/entry-wrapper.js +6 -1
  35. package/lib/utils/index.d.ts +1 -0
  36. package/lib/utils/index.js +1 -0
  37. package/lib/utils/inline-static-reads.d.ts +15 -1
  38. package/lib/utils/inline-static-reads.js +16 -8
  39. package/lib/utils/runtime.js +1 -1
  40. package/lib/utils/scan-globals.d.ts +2 -2
  41. package/lib/utils/scan-globals.js +20 -14
  42. package/lib/utils/zip-path.d.ts +9 -0
  43. package/lib/utils/zip-path.js +12 -0
  44. package/package.json +13 -9
@@ -26,7 +26,9 @@
26
26
  // until 2026-08-22 — see `shouldInline` for the incident that measured the cost.
27
27
  import { createRequire } from 'node:module';
28
28
  import { dirname, join, relative, resolve } from 'node:path';
29
- import { inlineStaticReads } from '../utils/inline-static-reads.js';
29
+ import { inlineStaticReads, parseSource } from '../utils/inline-static-reads.js';
30
+ import { declareBuildInput } from '../utils/declare-build-input.js';
31
+ import { hasZipSegment } from '../utils/zip-path.js';
30
32
  export const REWRITE_FILTER = /\.(m?js|cjs|[cm]?tsx?)$/;
31
33
  const DIRNAME_DECL_RE = /(?:var|let|const)\s+__dirname\b|export\s+(?:var|let|const)\s+__dirname\b/;
32
34
  const FILENAME_DECL_RE = /(?:var|let|const)\s+__filename\b|export\s+(?:var|let|const)\s+__filename\b/;
@@ -162,6 +164,132 @@ function needsDirnameDecl(src, flags) {
162
164
  function needsFilenameDecl(src, flags) {
163
165
  return flags.hasFilename && !FILENAME_DECL_RE.test(src);
164
166
  }
167
+ // The local name the file-URL → path conversion is imported under. Prefixed so it
168
+ // cannot collide with a `fileURLToPath` the dep declares itself.
169
+ const URL_TO_PATH = '__gjsifyFileURLToPath';
170
+ /**
171
+ * A path expression for the file URL `url`, with `fileURLToPath` semantics.
172
+ *
173
+ * It used to be `new URL(…).pathname`, which is a URL PATH, not a filesystem path, and
174
+ * the two part ways twice: percent-escapes stay encoded (`/my%20app/x.js` on any OS),
175
+ * and on win32 the drive comes out as `/C:/…`, which no Windows API accepts. Every
176
+ * target has a `fileURLToPath` behind `node:url` (Node's own; `@gjsify/url` on GJS and
177
+ * in the browser), and the case-1 shim already relies on it.
178
+ *
179
+ * `dir` strips the trailing separator the directory URL leaves behind — either one,
180
+ * since win32 answers `\`.
181
+ */
182
+ function urlToPathExpr(url, dir = false) {
183
+ const path = `${URL_TO_PATH}(${url})`;
184
+ return dir ? `${path}.replace(/[\\\\/]$/, "")` : path;
185
+ }
186
+ /**
187
+ * The import the {@link urlToPathExpr} expressions need, when any of them survive.
188
+ *
189
+ * A non-empty `preamble` is NOT the condition. When a file declares its OWN `__dirname`,
190
+ * {@link routeMetaPaths} declines the declaration and hands back the raw expression, so
191
+ * `import.meta.dirname` is still rewritten to a `__gjsifyFileURLToPath(…)` call while the
192
+ * preamble stays EMPTY — and a header keyed on `preamble.length` leaves that call bound to
193
+ * nothing. Measured: a node_modules dep with `var __dirname = "MINE"` beside
194
+ * `import.meta.dirname` emitted a bare `__gjsifyFileURLToPath(…)` and died on the
195
+ * ReferenceError at first use. `substitutes` is the same `import.meta` test the callers
196
+ * already branch on, so the header tracks what the output actually references.
197
+ */
198
+ function urlToPathHeader(preamble, substitutes) {
199
+ return preamble.length > 0 || substitutes
200
+ ? `import { fileURLToPath as ${URL_TO_PATH} } from "node:url";`
201
+ : undefined;
202
+ }
203
+ /**
204
+ * Route `import.meta.dirname`/`filename` the way the caller routes a bare
205
+ * `__dirname`/`__filename`: ONE `var` in the preamble, every occurrence referencing it.
206
+ * Answering them at all is the point — a bundled dep's location tokens must never
207
+ * resolve to the BUNDLE: @signalapp/libsignal-client's
208
+ * `node-gyp-build(import.meta.dirname + '/..')` looked beside the bundle and threw
209
+ * "No native build was found". (The addon package itself stays external now; a
210
+ * NON-addon dep spelling `import.meta.dirname` still lands here.) Case 1 answers inline
211
+ * through the runtime shim instead, and a file that declares its OWN `__dirname` keeps
212
+ * the inline expression, so a generated declaration can never collide with it.
213
+ */
214
+ function routeMetaPaths(src, flags, decl) {
215
+ const declareDirname = (flags.hasDirname || /\bimport\.meta\.dirname\b/.test(src)) && !DIRNAME_DECL_RE.test(src);
216
+ const declareFilename = (flags.hasFilename || /\bimport\.meta\.filename\b/.test(src)) && !FILENAME_DECL_RE.test(src);
217
+ return {
218
+ preamble: [
219
+ ...(declareDirname ? [`var __dirname = ${decl.dirname};`] : []),
220
+ ...(declareFilename ? [`var __filename = ${decl.filename};`] : []),
221
+ ],
222
+ dirname: declareDirname ? '__dirname' : decl.dirname,
223
+ filename: declareFilename ? '__filename' : decl.filename,
224
+ };
225
+ }
226
+ const IMPORT_META_PROPS = ['url', 'dirname', 'filename'];
227
+ const IMPORT_META_RE = /\bimport\.meta\.(url|dirname|filename)\b/g;
228
+ /** Every `import.meta.<url|dirname|filename>` EXPRESSION in `src`, or null when it does not parse. */
229
+ function importMetaRanges(src, path) {
230
+ let ast;
231
+ try {
232
+ ast = parseSource(src, path);
233
+ }
234
+ catch {
235
+ // acorn trails the bundler on new syntax; the caller keeps the token rewrite.
236
+ return null;
237
+ }
238
+ const ranges = [];
239
+ const stack = [ast];
240
+ while (stack.length > 0) {
241
+ const node = stack.pop();
242
+ if (node === null || typeof node !== 'object')
243
+ continue;
244
+ if (Array.isArray(node)) {
245
+ stack.push(...node);
246
+ continue;
247
+ }
248
+ const object = node.object;
249
+ const property = node.property;
250
+ if (node.type === 'MemberExpression' &&
251
+ !node.computed &&
252
+ object?.type === 'MetaProperty' &&
253
+ object.meta?.name === 'import' &&
254
+ object.property?.name === 'meta' &&
255
+ property?.type === 'Identifier' &&
256
+ IMPORT_META_PROPS.includes(property.name ?? '')) {
257
+ ranges.push({
258
+ start: node.start,
259
+ end: node.end,
260
+ prop: property.name,
261
+ });
262
+ continue;
263
+ }
264
+ for (const value of Object.values(node))
265
+ if (value !== null && typeof value === 'object')
266
+ stack.push(value);
267
+ }
268
+ return ranges;
269
+ }
270
+ /**
271
+ * Replace every `import.meta.url` / `.dirname` / `.filename` expression in `src`.
272
+ *
273
+ * On the AST, not the text: the token also occurs inside STRINGS, and a text rewrite
274
+ * put a quoted replacement inside a quoted key — vite's `define: { "import.meta.url":
275
+ * … }` became `"__gjsifyModuleUrl("vite/…")"`, a PARSE_ERROR ("Expected `:` but found
276
+ * `Identifier`") that failed every `--app gjs` build reaching vite or wxt. A source
277
+ * acorn cannot parse keeps the token rewrite it always had.
278
+ *
279
+ * `dirname` and `filename` (Node ≥ 20.11) are answered too: GJS defines neither, so
280
+ * unplugin's `path.resolve(import.meta.dirname, …)` threw `The "path" argument must be
281
+ * of type string. Received type undefined` at load — where wxt's rebuild stopped next.
282
+ */
283
+ export function replaceImportMeta(src, path, replacements) {
284
+ const ranges = importMetaRanges(src, path);
285
+ if (ranges === null)
286
+ return src.replace(IMPORT_META_RE, (_m, prop) => replacements[prop]);
287
+ let out = src;
288
+ for (const { start, end, prop } of ranges.sort((a, b) => b.start - a.start)) {
289
+ out = out.slice(0, start) + replacements[prop] + out.slice(end);
290
+ }
291
+ return out;
292
+ }
165
293
  /** Prepend preamble + (optional) shim import to the source. */
166
294
  function withPreamble(src, lines, importHeader) {
167
295
  const parts = importHeader ? [importHeader, ...lines, src] : [...lines, src];
@@ -185,7 +313,15 @@ function rewriteOnDiskEsm(src, path, flags) {
185
313
  preamble.push(`var __filename = __gjsifyModuleFile(${spec});`);
186
314
  used.push('__gjsifyModuleFile');
187
315
  }
188
- const code = src.replace(/\bimport\.meta\.url\b/g, `__gjsifyModuleUrl(${spec})`);
316
+ if (src.includes('import.meta.dirname') && !used.includes('__gjsifyModuleDir'))
317
+ used.push('__gjsifyModuleDir');
318
+ if (src.includes('import.meta.filename') && !used.includes('__gjsifyModuleFile'))
319
+ used.push('__gjsifyModuleFile');
320
+ const code = replaceImportMeta(src, path, {
321
+ url: `__gjsifyModuleUrl(${spec})`,
322
+ dirname: `__gjsifyModuleDir(${spec})`,
323
+ filename: `__gjsifyModuleFile(${spec})`,
324
+ });
189
325
  const header = `import { ${used.join(', ')} } from ${JSON.stringify(MODULE_RESOLVE_SHIM)};`;
190
326
  return { code: withPreamble(code, preamble, header), moduleType: moduleTypeForPath(path) };
191
327
  }
@@ -197,29 +333,40 @@ function rewriteOnDiskEsm(src, path, flags) {
197
333
  function rewriteOnDiskEsmLegacy(src, path, bundleDir, flags) {
198
334
  const relPath = relative(bundleDir, path);
199
335
  const relDirWithSlash = (relative(bundleDir, dirname(path)) || '.') + '/';
200
- const preamble = [];
201
- if (needsDirnameDecl(src, flags)) {
202
- preamble.push(`var __dirname = new URL(${JSON.stringify(relDirWithSlash)}, import.meta.url).pathname.replace(/\\/$/, "");`);
203
- }
204
- if (needsFilenameDecl(src, flags)) {
205
- preamble.push(`var __filename = new URL(${JSON.stringify(relPath)}, import.meta.url).pathname;`);
206
- }
207
- const code = src.replace(/\bimport\.meta\.url\b/g, `new URL(${JSON.stringify(relPath)}, import.meta.url).href`);
208
- return { code: withPreamble(code, preamble), moduleType: moduleTypeForPath(path) };
336
+ const route = routeMetaPaths(src, flags, {
337
+ dirname: urlToPathExpr(`new URL(${JSON.stringify(relDirWithSlash)}, import.meta.url)`, true),
338
+ filename: urlToPathExpr(`new URL(${JSON.stringify(relPath)}, import.meta.url)`),
339
+ });
340
+ const code = replaceImportMeta(src, path, {
341
+ url: `new URL(${JSON.stringify(relPath)}, import.meta.url).href`,
342
+ dirname: route.dirname,
343
+ filename: route.filename,
344
+ });
345
+ const substitutes = /\bimport\.meta\.(?:dirname|filename)\b/.test(src);
346
+ return {
347
+ code: withPreamble(code, route.preamble, urlToPathHeader(route.preamble, substitutes)),
348
+ moduleType: moduleTypeForPath(path),
349
+ };
209
350
  }
210
351
  /**
211
352
  * Case 3 — PnP zip-resident. Keep `import.meta.url` as the bundle's own URL and
212
353
  * derive `__dirname`/`__filename` from it.
213
354
  */
214
355
  function rewriteZipResident(src, path, flags) {
215
- const preamble = [];
216
- if (needsDirnameDecl(src, flags)) {
217
- preamble.push(`var __dirname = new URL(".", import.meta.url).pathname.replace(/\\/$/, "");`);
218
- }
219
- if (needsFilenameDecl(src, flags)) {
220
- preamble.push(`var __filename = new URL(import.meta.url).pathname;`);
221
- }
222
- return { code: withPreamble(src, preamble), moduleType: moduleTypeForPath(path) };
356
+ const route = routeMetaPaths(src, flags, {
357
+ dirname: urlToPathExpr('new URL(".", import.meta.url)', true),
358
+ filename: urlToPathExpr('import.meta.url'),
359
+ });
360
+ // `import.meta.url` keeps case 3's rule (the bundle's own URL); only the two
361
+ // members GJS does not define are routed through the declarations.
362
+ const substitutes = /\bimport\.meta\.(?:dirname|filename)\b/.test(src);
363
+ const code = substitutes
364
+ ? replaceImportMeta(src, path, { url: 'import.meta.url', dirname: route.dirname, filename: route.filename })
365
+ : src;
366
+ return {
367
+ code: withPreamble(code, route.preamble, urlToPathHeader(route.preamble, substitutes)),
368
+ moduleType: moduleTypeForPath(path),
369
+ };
223
370
  }
224
371
  /**
225
372
  * Case 4a — CJS, runtime-resolve. `__dirname`/`__filename` come from the module-resolve
@@ -269,13 +416,16 @@ function rewriteCjsAbsolute(src, path, flags) {
269
416
  * Pure rewriter: the rewritten code plus the module type to re-parse it with, or `null`
270
417
  * when the file references none of the tokens.
271
418
  */
272
- export function rewriteContents(args, srcInput, bundleDir, runtimeResolve) {
419
+ export function rewriteContents(args, srcInput, bundleDir, runtimeResolve, declare) {
273
420
  if (!shouldRewrite(args.path))
274
421
  return null;
275
- const inlined = inlineStaticReads(srcInput, args.path);
422
+ const inlined = inlineStaticReads(srcInput, args.path, declare);
276
423
  const src = inlined.contents;
277
424
  const flags = {
278
- hasMetaUrl: src.includes('import.meta.url'),
425
+ // A file that spelled ANY `import.meta.*` location is ESM, so it keeps the ESM
426
+ // strategies even when `import.meta.url` itself never appeared. The strategies
427
+ // answer all three on the AST (see `replaceImportMeta`), never as text.
428
+ hasMetaUrl: /\bimport\.meta\.(?:url|dirname|filename)\b/.test(src),
279
429
  hasDirname: src.includes('__dirname'),
280
430
  hasFilename: src.includes('__filename'),
281
431
  };
@@ -285,7 +435,7 @@ export function rewriteContents(args, srcInput, bundleDir, runtimeResolve) {
285
435
  }
286
436
  // Step 2: dispatch by case (see file header).
287
437
  if (flags.hasMetaUrl) {
288
- if (relative(bundleDir, args.path).includes('.zip/')) {
438
+ if (hasZipSegment(relative(bundleDir, args.path))) {
289
439
  return rewriteZipResident(src, args.path, flags);
290
440
  }
291
441
  return runtimeResolve
@@ -319,8 +469,14 @@ export function nodeModulesPathRewritePlugin(options) {
319
469
  order: 'post',
320
470
  filter: { id: REWRITE_FILTER },
321
471
  handler(code, id) {
472
+ // What the inliner FOLDS into the bundle is an input of the
473
+ // output and no module graph names it, so it is declared through
474
+ // the standard contract — the same one `css-as-string` uses.
475
+ // Feature-detected, and the reason is measured: see
476
+ // `utils/declare-build-input.ts`.
477
+ const declare = (abs) => declareBuildInput(this, abs);
322
478
  if (id.includes('node_modules')) {
323
- const result = rewriteContents({ path: id }, code, options.bundleDir, runtimeResolve);
479
+ const result = rewriteContents({ path: id }, code, options.bundleDir, runtimeResolve, declare);
324
480
  if (!result)
325
481
  return null;
326
482
  return { code: result.code, map: null };
@@ -330,7 +486,7 @@ export function nodeModulesPathRewritePlugin(options) {
330
486
  // is the node_modules question this plugin was written for.
331
487
  if (!shouldInline(id))
332
488
  return null;
333
- const inlined = inlineStaticReads(code, id);
489
+ const inlined = inlineStaticReads(code, id, declare);
334
490
  return inlined.inlined > 0 ? { code: inlined.contents, map: null } : null;
335
491
  },
336
492
  },
@@ -20,6 +20,8 @@
20
20
  // on the old fall-through instead of promoting a user's own mapping mistake to a
21
21
  // fatal error in a plugin they did not ask for.
22
22
  import { dirname } from 'node:path';
23
+ /** Marks this plugin's own `this.resolve` probes (`custom` resolve option). */
24
+ const OWN_PROBE = 'gjsify-unresolved-workspace-import/probe';
23
25
  /** Bare `@gjsify/*` package specifier — the workspace edge this guard protects. */
24
26
  const WORKSPACE_SCOPE = '@gjsify/';
25
27
  /**
@@ -169,10 +171,8 @@ function describeResolverFailure(err) {
169
171
  */
170
172
  export function unresolvedWorkspaceImportPlugin(options) {
171
173
  const { target, aliases, isExternal, toolchainAnchor } = options;
172
- // Re-entrancy belt: `skipSelf: true` should keep our own `this.resolve` out of
173
- // this hook, but a hang is a far worse failure than a missed check. Keyed by
174
- // `candidate\0importer` — the same package legitimately resolves from many.
175
- const inFlight = new Set();
174
+ // Resolutions in flight, by the cache key — concurrent askers await the same one.
175
+ const pending = new Map();
176
176
  // Read only on the failure path, to recover the `node:fs` behind a failed
177
177
  // `@gjsify/fs`.
178
178
  const reverseAliases = buildReverseAliasIndex(aliases);
@@ -200,6 +200,12 @@ export function unresolvedWorkspaceImportPlugin(options) {
200
200
  const importer = typeof rawImporter === 'string' ? rawImporter : undefined;
201
201
  if (extraOptions?.isEntry)
202
202
  return null;
203
+ // Re-entrancy belt: `skipSelf: true` should keep our own probes out of
204
+ // this hook, but one that got back in would now await ITSELF below — a
205
+ // hang, far worse than a missed check. The probes carry a marker, so the
206
+ // belt tells re-entrancy from a concurrent ask instead of guessing.
207
+ if (extraOptions?.custom?.[OWN_PROBE])
208
+ return null;
203
209
  // The toolchain fallback below re-enters this hook with the anchor as
204
210
  // the importer. Declining there keeps the probe from rescuing itself:
205
211
  // the anchor cannot answer for the anchor, and the recursion has no
@@ -216,75 +222,90 @@ export function unresolvedWorkspaceImportPlugin(options) {
216
222
  const cached = resolvedCache.get(key);
217
223
  if (cached)
218
224
  return cached;
219
- if (inFlight.has(key))
220
- return null;
221
- inFlight.add(key);
222
- let resolved = null;
223
- // KEPT DELIBERATELY, and NOT as a miss. `this.resolve` re-runs the whole
224
- // `pre`-order chain, so it can fail two very different ways. "Nothing is
225
- // there" is `null` on both engines — npm `rolldown` always answered that,
226
- // and `@gjsify/rolldown-native` now does too (`isResolveMiss`, which is
227
- // where the engine mismatch that hid this plugin's diagnostic under GJS
228
- // is fixed). What reaches this catch is the OTHER kind: a hook in that
229
- // chain throwing on purpose (`napi-node-addon`'s `resolveAddonPath`), an
230
- // EACCES, a corrupt `package.json`, a bridge fault. Letting it escape
231
- // surfaces it as a bare "plugin `gjsify-alias` threw an error" naming
232
- // neither specifier nor importer; swallowing it would report a real fault
233
- // as the user's missing dependency. So it is caught, kept, and thrown
234
- // BELOW with this plugin's context around it and the original as `cause`.
235
- let resolverFailure;
225
+ // A CONCURRENT ask for the same key shares the one in flight. The belt
226
+ // used to answer it with `null`, as if it were re-entrancy, and Rolldown
227
+ // externalised the specifier: two modules of one directory, or two
228
+ // `import … from 'node:path'` statements of one module, resolving at the
229
+ // same moment. Measured twice — web-ext's `lib/cmd/*.js` (loaded in
230
+ // parallel through dynamic imports) lost the module-resolve shim, and
231
+ // vite's `chunks/node.js` kept a bare `node:path` — each only when the
232
+ // scheduler interleaved them, so the same build could pass or fail.
233
+ const shared = pending.get(key);
234
+ if (shared)
235
+ return shared;
236
+ const work = resolveCandidate.call(this, source, importer, verdict, extraOptions?.kind, key);
237
+ pending.set(key, work);
236
238
  try {
237
- resolved = await this.resolve(verdict.candidate, importer, {
238
- skipSelf: true,
239
- kind: extraOptions?.kind,
240
- });
241
- }
242
- catch (err) {
243
- resolverFailure = err;
239
+ return await work;
244
240
  }
245
241
  finally {
246
- inFlight.delete(key);
242
+ pending.delete(key);
247
243
  }
248
- if (resolved) {
249
- resolvedCache.set(key, resolved);
250
- return resolved;
251
- }
252
- // Toolchain fallback — see `toolchainAnchor`. Only after a real `null`:
253
- // a resolver that ERRORED has not established that the project lacks the
254
- // package, and rescuing there would turn "project first, always" into
255
- // "project first unless the project errors".
256
- if (resolverFailure === undefined && toolchainAnchor !== undefined) {
257
- const fromToolchain = await this.resolve(verdict.candidate, toolchainAnchor, {
258
- skipSelf: true,
259
- kind: extraOptions?.kind,
260
- });
261
- if (fromToolchain) {
262
- // The one visible trace that this artifact is MIXED — some of its
263
- // `@gjsify/*` from the project, this one from the CLI's own
264
- // install, possibly a different major. Without it a rescued build
265
- // and a healthy one have byte-identical logs, and a wrong answer
266
- // that leaves no trace is the expensive kind. Deduplicated by
267
- // `resolvedCache`: one line per (candidate, importer dir, kind).
268
- this.warn(`gjsify: \`${verdict.candidate}\` did not resolve from the project ` +
269
- `(imported by ${importer ?? '<entry>'}) — using the copy installed beside the ` +
270
- `running CLI (${toolchainAnchor}). Build the workspace package to use the ` +
271
- `project's own copy.`);
272
- resolvedCache.set(key, fromToolchain);
273
- return fromToolchain;
274
- }
275
- }
276
- throw new UnresolvedWorkspaceImportError({
277
- target,
278
- source,
279
- candidate: verdict.candidate,
280
- aliasTarget: verdict.aliasTarget,
281
- aliasedFrom: reverseAliases.get(verdict.candidate)?.filter((s) => s !== source),
282
- importer: importer ?? '<entry>',
283
- ...(resolverFailure === undefined
284
- ? {}
285
- : { resolverFailure: describeResolverFailure(resolverFailure) }),
286
- }, resolverFailure);
287
244
  },
288
245
  },
289
246
  };
247
+ async function resolveCandidate(source, importer, verdict, kind, key) {
248
+ let resolved = null;
249
+ // KEPT DELIBERATELY, and NOT as a miss. `this.resolve` re-runs the whole
250
+ // `pre`-order chain, so it can fail two very different ways. "Nothing is
251
+ // there" is `null` on both engines — npm `rolldown` always answered that,
252
+ // and `@gjsify/rolldown-native` now does too (`isResolveMiss`, which is
253
+ // where the engine mismatch that hid this plugin's diagnostic under GJS
254
+ // is fixed). What reaches this catch is the OTHER kind: a hook in that
255
+ // chain throwing on purpose (`napi-node-addon`'s `resolveAddonPath`), an
256
+ // EACCES, a corrupt `package.json`, a bridge fault. Letting it escape
257
+ // surfaces it as a bare "plugin `gjsify-alias` threw an error" naming
258
+ // neither specifier nor importer; swallowing it would report a real fault
259
+ // as the user's missing dependency. So it is caught, kept, and thrown
260
+ // BELOW with this plugin's context around it and the original as `cause`.
261
+ let resolverFailure;
262
+ try {
263
+ resolved = await this.resolve(verdict.candidate, importer, {
264
+ skipSelf: true,
265
+ kind,
266
+ custom: { [OWN_PROBE]: true },
267
+ });
268
+ }
269
+ catch (err) {
270
+ resolverFailure = err;
271
+ }
272
+ if (resolved) {
273
+ resolvedCache.set(key, resolved);
274
+ return resolved;
275
+ }
276
+ // Toolchain fallback — see `toolchainAnchor`. Only after a real `null`:
277
+ // a resolver that ERRORED has not established that the project lacks the
278
+ // package, and rescuing there would turn "project first, always" into
279
+ // "project first unless the project errors".
280
+ if (resolverFailure === undefined && toolchainAnchor !== undefined) {
281
+ const fromToolchain = await this.resolve(verdict.candidate, toolchainAnchor, {
282
+ skipSelf: true,
283
+ kind,
284
+ custom: { [OWN_PROBE]: true },
285
+ });
286
+ if (fromToolchain) {
287
+ // The one visible trace that this artifact is MIXED — some of its
288
+ // `@gjsify/*` from the project, this one from the CLI's own
289
+ // install, possibly a different major. Without it a rescued build
290
+ // and a healthy one have byte-identical logs, and a wrong answer
291
+ // that leaves no trace is the expensive kind. Deduplicated by
292
+ // `resolvedCache`: one line per (candidate, importer dir, kind).
293
+ this.warn(`gjsify: \`${verdict.candidate}\` did not resolve from the project ` +
294
+ `(imported by ${importer ?? '<entry>'}) — using the copy installed beside the ` +
295
+ `running CLI (${toolchainAnchor}). Build the workspace package to use the ` +
296
+ `project's own copy.`);
297
+ resolvedCache.set(key, fromToolchain);
298
+ return fromToolchain;
299
+ }
300
+ }
301
+ throw new UnresolvedWorkspaceImportError({
302
+ target,
303
+ source,
304
+ candidate: verdict.candidate,
305
+ aliasTarget: verdict.aliasTarget,
306
+ aliasedFrom: reverseAliases.get(verdict.candidate)?.filter((s) => s !== source),
307
+ importer: importer ?? '<entry>',
308
+ ...(resolverFailure === undefined ? {} : { resolverFailure: describeResolverFailure(resolverFailure) }),
309
+ }, resolverFailure);
310
+ }
290
311
  }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Resolve the addon's `.node` for the RUNNING host from the build-time table.
3
+ *
4
+ * `targets` maps platform keys (`linux-x64`, `linux-x64-musl`, …) to
5
+ * `<package>/<subpath>` specs. The host keys are tried most-specific first
6
+ * (exact, then libc-agnostic, then `*`). The package root is resolved at run
7
+ * time through the bundle-URL anchor, which is what makes the bundle relocatable
8
+ * (ADR 0084).
9
+ *
10
+ * Throws when no entry matches the running host, when the table is empty, and
11
+ * when the addon package is not installed next to the bundle. The last one is
12
+ * a LIMIT, not an oversight: identity-based resolution needs the package to be
13
+ * somewhere on disk, so a bundle shipped with no `node_modules` around it cannot
14
+ * load a third-party addon. It throws rather than returning
15
+ * `<bundle dir>/addons/…` because that directory is a declared layout nothing
16
+ * fills yet, and a path that cannot exist reaches `loadAddon` as a bare ENOENT
17
+ * that names neither the package nor the remedy. Both facts are in the message.
18
+ */
19
+ export declare function __gjsifyAddonResolve(targets: Record<string, string>): string;