@gjsify/rolldown-plugin-gjsify 0.23.0 → 0.24.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,10 +4,14 @@
4
4
  // (they appear transitively via `@gjsify/unit` and similar packages with
5
5
  // GJS-specific code paths). Bare Node specifiers + their `node:*` prefix
6
6
  // variants are routed to `@gjsify/<X>` via the curated
7
- // `ALIASES_NODE_FOR_BROWSER` table — the dynamic per-runtimes-triplet
8
- // resolver (`getDerivedAliasesSync`) finishes the routing in a second pass
9
- // (`@gjsify/<X>` → `@gjsify/<X>/globals` / `@gjsify/empty` depending on the
10
- // slot declaration).
7
+ // `ALIASES_NODE_FOR_BROWSER` table, whose VALUES already carry the
8
+ // per-runtimes-triplet slot routing (`@gjsify/<X>` → `@gjsify/<X>/browser` /
9
+ // `/globals` / `@gjsify/empty`): that table is exported through
10
+ // `withDerivedSlotRouting` in `@gjsify/resolve-npm`. Composition happens THERE,
11
+ // not as a resolver second pass here — `aliasPlugin` resolves an alias target
12
+ // in exactly one hop, because the map it receives is merged flat across tiers
13
+ // and a chain would re-route user `--alias` targets too (full argument in
14
+ // `withDerivedSlotRouting`'s doc comment and `plugins/alias.ts`).
11
15
  import { aliasPlugin } from '../plugins/alias.js';
12
16
  import { deepkitPlugin } from '@gjsify/rolldown-plugin-deepkit';
13
17
  import blueprintPlugin from '@gjsify/vite-plugin-blueprint';
@@ -15,6 +19,7 @@ import { ALIASES_NODE_FOR_BROWSER, getDerivedAliasesSync } from '@gjsify/resolve
15
19
  import { globToEntryPoints } from '../utils/entry-points.js';
16
20
  import { gjsImportsEmptyPlugin } from '../plugins/gjs-imports-empty.js';
17
21
  import { cssAsStringPlugin } from '../plugins/css-as-string.js';
22
+ import { unresolvedWorkspaceImportPlugin } from '../plugins/unresolved-workspace-import.js';
18
23
  export const setupForBrowser = async (input) => {
19
24
  const userExternal = input.userExternal ?? [];
20
25
  const external = [...userExternal];
@@ -77,12 +82,26 @@ export const setupForBrowser = async (input) => {
77
82
  },
78
83
  treeshake: true,
79
84
  };
85
+ // The substitution table exactly as `aliasPlugin` receives it — shared with
86
+ // the unresolved-workspace-import guard below so the guard reports the same
87
+ // promise the alias layer made.
88
+ const aliasEntries = flattenAliases(aliasMap);
80
89
  const plugins = [
81
90
  gjsImportsEmptyPlugin(),
82
- aliasPlugin({ entries: flattenAliases(aliasMap) }),
91
+ aliasPlugin({ entries: aliasEntries }),
83
92
  blueprintPlugin(),
84
93
  deepkitPlugin({ reflection: input.pluginOptions.reflection }),
85
94
  cssAsStringPlugin(),
95
+ // `order: 'post'` — see app/gjs.ts. The browser target's whole job is to
96
+ // replace Node builtins with their `@gjsify/*` browser entries; when one
97
+ // of those cannot be resolved, Rolldown externalises the ORIGINAL bare
98
+ // specifier and a browser cannot resolve it at all. There is no
99
+ // `node:`-shaped emit-time symptom here, so this guard is the only signal.
100
+ unresolvedWorkspaceImportPlugin({
101
+ target: 'browser',
102
+ aliases: aliasEntries,
103
+ isExternal: (id) => external.includes(id),
104
+ }),
86
105
  ];
87
106
  return { options, plugins };
88
107
  };
package/lib/app/gjs.d.ts CHANGED
@@ -55,6 +55,30 @@ export declare const setupForGjs: (input: GjsFactoryInput) => Promise<GjsBuildCo
55
55
  * in `auto-globals.spec.ts` — canonical contract, change-detector status.
56
56
  */
57
57
  export declare function createGjsExternalsPredicate(userExternal?: string[]): (id: string) => boolean;
58
+ /**
59
+ * Recognize the shims this plugin INJECTS into a `--app gjs` bundle
60
+ * (`shims/module-resolve`, `shims/console-gjs`, `shims/unicorn-magic`, …), in
61
+ * both bare-specifier and resolved-disk-path form.
62
+ *
63
+ * They must be force-inlined for exactly the reason `/register` subpaths are,
64
+ * and the failure is the same one: GJS has no `require` and its ESM loader has
65
+ * neither a node_modules walker nor `exports`-map support, so an EXTERNALIZED
66
+ * shim aborts the program at load:
67
+ *
68
+ * Error: Calling `require` for
69
+ * "@gjsify/rolldown-plugin-gjsify/shims/module-resolve" in an environment
70
+ * that doesn't expose the `require` function
71
+ *
72
+ * The shim is not optional — the bundler injected it because the bundle needs
73
+ * it — so externalizing it can only ever produce an unloadable artifact. The
74
+ * carve-out short-circuits BEFORE the user-external check so `bundler.external`
75
+ * cannot override it, and it matches by SHAPE rather than an explicit list, so
76
+ * a shim added later is covered without touching this predicate.
77
+ *
78
+ * Scope is deliberately narrow: only OUR shim directory. A consumer package
79
+ * that happens to have a `shims/` folder is unaffected.
80
+ */
81
+ export declare function isGjsifyShim(id: string): boolean;
58
82
  /**
59
83
  * Recognize the `/register` and `/register/<feature>` subpath shapes that
60
84
  * `--globals auto` injects into the bundle as side-effect imports.
package/lib/app/gjs.js CHANGED
@@ -10,9 +10,12 @@
10
10
  // handled separately by `setupLib`.
11
11
  import { fileURLToPath } from 'node:url';
12
12
  import { dirname, resolve } from 'node:path';
13
+ import { existsSync } from 'node:fs';
14
+ import { createRequire } from 'node:module';
13
15
  import { aliasPlugin } from '../plugins/alias.js';
14
16
  import { externalsPlugin } from '../plugins/externals.js';
15
17
  import { napiNodeAddonPlugin } from '../plugins/napi-node-addon.js';
18
+ import { unresolvedWorkspaceImportPlugin } from '../plugins/unresolved-workspace-import.js';
16
19
  import { deepkitPlugin } from '@gjsify/rolldown-plugin-deepkit';
17
20
  import blueprintPlugin from '@gjsify/vite-plugin-blueprint';
18
21
  import { getAliasesForGjs } from '../utils/alias.js';
@@ -23,35 +26,38 @@ import { cssAsStringPlugin } from '../plugins/css-as-string.js';
23
26
  import { shebangPlugin, resolveShebangLine, inputShebangStripPlugin } from '../plugins/shebang.js';
24
27
  import { wrapInputWithSideEffects } from '../utils/entry-wrapper.js';
25
28
  const _shimDir = dirname(fileURLToPath(import.meta.url));
29
+ /**
30
+ * Resolve one of this package's bundled shims to an absolute path.
31
+ *
32
+ * `createRequire` is a NAMED IMPORT from `node:module`, never a bare
33
+ * `require(...)` — this source is ESM (see AGENTS.md "Our source is ESM —
34
+ * no bare `require`"). It is used here for its RESOLVER, which is
35
+ * `exports`-map aware, and only to answer "where does this subpath live";
36
+ * nothing is loaded through it.
37
+ */
26
38
  function resolveShim(shimName) {
27
39
  // Preferred: relative to this module's directory. Works under the
28
40
  // normal Node consumer flow where `_shimDir` = `<pkg>/lib/app/`.
29
41
  const relative = resolve(_shimDir, `../shims/${shimName}.js`);
30
- let fs = null;
31
- try {
32
- // eslint-disable-next-line @typescript-eslint/no-require-imports
33
- fs = require('node:fs');
34
- }
35
- catch {
36
- return relative;
37
- }
38
- if (fs.existsSync(relative))
42
+ if (existsSync(relative))
39
43
  return relative;
40
44
  // Fallback: when the orchestrator is bundled into a single .mjs
41
45
  // (GJS-CLI self-host loop) `_shimDir` collapses to the bundle's
42
- // own directory and the relative lookup misses. createRequire's
43
- // resolver is `exports`-map-aware (Phase C), so the published
44
- // subpath export `./shims/<name>` works under both Node and GJS
45
- // without further walking. Each shim MUST be a `./shims/<name>`
46
- // subpath export in package.json for this fallback to resolve.
46
+ // own directory and the relative lookup misses. The published subpath
47
+ // export `./shims/<name>` resolves under both Node and GJS without
48
+ // further walking. Each shim MUST be a `./shims/<name>` subpath export
49
+ // in package.json for this fallback to resolve.
47
50
  try {
48
- // eslint-disable-next-line @typescript-eslint/no-require-imports
49
- const Module = require('node:module');
50
- const require_ = Module.createRequire(import.meta.url);
51
- return require_.resolve(`@gjsify/rolldown-plugin-gjsify/shims/${shimName}`);
51
+ return createRequire(import.meta.url).resolve(`@gjsify/rolldown-plugin-gjsify/shims/${shimName}`);
52
52
  }
53
- catch {
54
- return relative;
53
+ catch (cause) {
54
+ // Both lookups failed — the shim is genuinely absent. Returning the
55
+ // unverified `relative` here would hand a non-existent path to the
56
+ // alias map, and the failure would resurface much later as an
57
+ // unrelated "could not resolve" naming a path nobody wrote. Name the
58
+ // actual problem, at the site that knows what it was looking for.
59
+ throw new Error(`@gjsify/rolldown-plugin-gjsify: cannot locate the "${shimName}" shim. ` +
60
+ `Tried ${relative} and the "./shims/${shimName}" subpath export.`, { cause });
55
61
  }
56
62
  }
57
63
  export const setupForGjs = async (input) => {
@@ -72,7 +78,12 @@ export const setupForGjs = async (input) => {
72
78
  //
73
79
  // `exactExternal` filters out register subpaths so a user external
74
80
  // entry can never override the force-inline carve-out below.
75
- const exactExternal = ['cairo', 'gettext', 'system', ...userExternal.filter((id) => !isRegisterSubpath(id))];
81
+ const exactExternal = [
82
+ 'cairo',
83
+ 'gettext',
84
+ 'system',
85
+ ...userExternal.filter((id) => !isRegisterSubpath(id) && !isGjsifyShim(id)),
86
+ ];
76
87
  const external = createGjsExternalsPredicate(userExternal);
77
88
  const format = input.pluginOptions.format ?? 'esm';
78
89
  const exclude = input.pluginOptions.exclude ?? [];
@@ -178,6 +189,13 @@ export const setupForGjs = async (input) => {
178
189
  treeshake: true,
179
190
  };
180
191
  const bundleDir = getBundleDirFromOutput(input.output);
192
+ // The substitution table exactly as `aliasPlugin` receives it — shared with
193
+ // the unresolved-workspace-import guard below so the guard reports the same
194
+ // promise the alias layer made.
195
+ const aliasEntries = {
196
+ 'random-access-file': 'random-access-file/index.js',
197
+ ...flattenAliases(aliasMap),
198
+ };
181
199
  const plugins = [
182
200
  // Virtual-entry plugin runs FIRST so its resolveId/load match the
183
201
  // synthetic input ids that `wrapInputWithSideEffects` produces.
@@ -191,12 +209,7 @@ export const setupForGjs = async (input) => {
191
209
  // random-access-file's 'browser' field maps to a throwing stub; force
192
210
  // the fs-backed Node entry. Implemented via the gjsify alias plugin
193
211
  // as a direct entry-table override.
194
- aliasPlugin({
195
- entries: {
196
- 'random-access-file': 'random-access-file/index.js',
197
- ...flattenAliases(aliasMap),
198
- },
199
- }),
212
+ aliasPlugin({ entries: aliasEntries }),
200
213
  // Transparent N-API `.node`-addon loader — the forward mirror of
201
214
  // `gjsGiNodePlugin`. Its `order:'pre'` resolveId claims a native-addon
202
215
  // acquisition (`bindings`/`node-gyp-build`/a direct `.node`/a napi-rs
@@ -229,6 +242,14 @@ export const setupForGjs = async (input) => {
229
242
  const line = resolveShebangLine(input.shebang);
230
243
  return shebangPlugin({ enabled: line !== null, line: line ?? undefined });
231
244
  })(),
245
+ // LAST claim on an id nothing else wanted (`order: 'post'`): a
246
+ // `@gjsify/*` substitution that cannot be resolved is a build ERROR, not
247
+ // a silent external. Without it Rolldown externalises the ORIGINAL
248
+ // specifier, exits 0, and writes a bundle stock GJS aborts on at load
249
+ // (`ImportError: Unsupported URI scheme for importing: node`) — the
250
+ // failure `utils/gjs-bundle-guard.ts` catches at emit time, caught here
251
+ // at its source instead, with the importer and the cause named.
252
+ unresolvedWorkspaceImportPlugin({ target: 'gjs', aliases: aliasEntries, isExternal: external }),
232
253
  ];
233
254
  return { options, plugins };
234
255
  };
@@ -271,7 +292,7 @@ function flattenAliases(map) {
271
292
  export function createGjsExternalsPredicate(userExternal = []) {
272
293
  const exact = ['cairo', 'gettext', 'system', ...userExternal];
273
294
  return (id) => {
274
- if (isRegisterSubpath(id))
295
+ if (isRegisterSubpath(id) || isGjsifyShim(id))
275
296
  return false;
276
297
  if (id.startsWith('gi://'))
277
298
  return true;
@@ -280,6 +301,36 @@ export function createGjsExternalsPredicate(userExternal = []) {
280
301
  return false;
281
302
  };
282
303
  }
304
+ /**
305
+ * Recognize the shims this plugin INJECTS into a `--app gjs` bundle
306
+ * (`shims/module-resolve`, `shims/console-gjs`, `shims/unicorn-magic`, …), in
307
+ * both bare-specifier and resolved-disk-path form.
308
+ *
309
+ * They must be force-inlined for exactly the reason `/register` subpaths are,
310
+ * and the failure is the same one: GJS has no `require` and its ESM loader has
311
+ * neither a node_modules walker nor `exports`-map support, so an EXTERNALIZED
312
+ * shim aborts the program at load:
313
+ *
314
+ * Error: Calling `require` for
315
+ * "@gjsify/rolldown-plugin-gjsify/shims/module-resolve" in an environment
316
+ * that doesn't expose the `require` function
317
+ *
318
+ * The shim is not optional — the bundler injected it because the bundle needs
319
+ * it — so externalizing it can only ever produce an unloadable artifact. The
320
+ * carve-out short-circuits BEFORE the user-external check so `bundler.external`
321
+ * cannot override it, and it matches by SHAPE rather than an explicit list, so
322
+ * a shim added later is covered without touching this predicate.
323
+ *
324
+ * Scope is deliberately narrow: only OUR shim directory. A consumer package
325
+ * that happens to have a `shims/` folder is unaffected.
326
+ */
327
+ export function isGjsifyShim(id) {
328
+ // Bare/fully-qualified: `@gjsify/rolldown-plugin-gjsify/shims/<name>`.
329
+ if (/@gjsify\/rolldown-plugin-gjsify\/shims\/[^?]+$/.test(id))
330
+ return true;
331
+ // Resolved disk path: `…/rolldown-plugin-gjsify/{lib,src}/shims/<name>.js`.
332
+ return /[\\/]rolldown-plugin-gjsify[\\/](?:lib|src)[\\/]shims[\\/]/.test(id);
333
+ }
283
334
  /**
284
335
  * Recognize the `/register` and `/register/<feature>` subpath shapes that
285
336
  * `--globals auto` injects into the bundle as side-effect imports.
@@ -1,4 +1,4 @@
1
- export { setupForGjs, isRegisterSubpath, createGjsExternalsPredicate } from './gjs.js';
1
+ export { setupForGjs, isRegisterSubpath, isGjsifyShim, createGjsExternalsPredicate } from './gjs.js';
2
2
  export type { GjsBuildConfig, GjsFactoryInput } from './gjs.js';
3
3
  export { setupForNode, enableGjsRegistersForNode, isGjsSourceBuild } from './node.js';
4
4
  export type { NodeBuildConfig, NodeFactoryInput } from './node.js';
package/lib/app/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { setupForGjs, isRegisterSubpath, createGjsExternalsPredicate } from './gjs.js';
1
+ export { setupForGjs, isRegisterSubpath, isGjsifyShim, createGjsExternalsPredicate } from './gjs.js';
2
2
  export { setupForNode, enableGjsRegistersForNode, isGjsSourceBuild } from './node.js';
3
3
  export { setupForBrowser } from './browser.js';
4
4
  export { setupForNativescript } from './nativescript.js';
@@ -16,10 +16,21 @@
16
16
  // GJS-specific code paths that never execute on NS.
17
17
  //
18
18
  // Bare Node specifiers + their `node:*` prefix variants are routed to
19
- // `@gjsify/<X>` via the curated `ALIASES_NODE_FOR_NATIVESCRIPT` table — the
20
- // dynamic per-runtimes-triplet resolver (`getDerivedAliasesSync`) finishes
21
- // the routing in a second pass (`@gjsify/<X>` → `@gjsify/<X>/globals` /
22
- // `@gjsify/empty` depending on the slot declaration).
19
+ // `@gjsify/<X>` via the curated `ALIASES_NODE_FOR_NATIVESCRIPT` table. Unlike
20
+ // the browser table, this one is NOT passed through `withDerivedSlotRouting`:
21
+ // slot routing would only change 4 of its 122 entries and all four are
22
+ // regressions — `fs`/`fs/promises` (+ their `node:` forms) deliberately point
23
+ // at `@gjsify/native-fs-bridge`, which declares `nativescript: "native"` in the
24
+ // "this package IS the native implementation" sense and therefore routes to a
25
+ // `globals.mjs` it does not ship → `@gjsify/empty`. The slot vocabulary's
26
+ // `native` means "the RUNTIME provides this, use its value", so the bridge
27
+ // packages are mis-declared; that is tracked in STATUS.md and must be settled
28
+ // before this table is composed. A direct `import … from
29
+ // '@gjsify/native-fs-bridge'` already hits the derived map (plain key hit) and
30
+ // is emptied TODAY — same root cause, listed in the same TODO.
31
+ //
32
+ // Derived `@gjsify/<X>` entries are merged into the map below, so an import by
33
+ // PACKAGE NAME still routes per its declared slot in a single hop.
23
34
  //
24
35
  // No `cssAsStringPlugin` (NativeScript ships its own CSS pipeline as part
25
36
  // of `@nativescript/core`) and no `blueprintPlugin` (Blueprint is GTK-only).
@@ -29,6 +40,7 @@ import { ALIASES_NODE_FOR_NATIVESCRIPT, getDerivedAliasesSync } from '@gjsify/re
29
40
  import { globToEntryPoints } from '../utils/entry-points.js';
30
41
  import { gjsImportsEmptyPlugin } from '../plugins/gjs-imports-empty.js';
31
42
  import { platformResolvePlugin, detectNativescriptPlatform, nativescriptPlatformDefines, } from '../plugins/platform-resolve.js';
43
+ import { unresolvedWorkspaceImportPlugin } from '../plugins/unresolved-workspace-import.js';
32
44
  export const setupForNativescript = async (input) => {
33
45
  const userExternal = input.userExternal ?? [];
34
46
  const external = [...userExternal];
@@ -105,18 +117,32 @@ export const setupForNativescript = async (input) => {
105
117
  },
106
118
  treeshake: true,
107
119
  };
120
+ // The substitution table exactly as `aliasPlugin` receives it — shared with
121
+ // the unresolved-workspace-import guard below so the guard reports the same
122
+ // promise the alias layer made.
123
+ const aliasEntries = flattenAliases(aliasMap);
108
124
  const plugins = [
109
125
  gjsImportsEmptyPlugin(),
110
126
  // Platform-specific source variants (`*.android` / `*.ios` /
111
127
  // `*.native`) win over the base file — resolved BEFORE the Node-builtin
112
128
  // alias routing so a platform fork of a portable module is honored.
113
129
  platformResolvePlugin({ platform }),
114
- aliasPlugin({ entries: flattenAliases(aliasMap) }),
130
+ aliasPlugin({ entries: aliasEntries }),
115
131
  // NO blueprintPlugin — Blueprint is a GTK-specific UI DSL
116
132
  // NO cssAsStringPlugin — NS ships its own CSS pipeline via
117
133
  // @nativescript/core; .css imports are handled by the consuming
118
134
  // @nativescript/webpack or @nativescript/vite build
119
135
  deepkitPlugin({ reflection: input.pluginOptions.reflection }),
136
+ // `order: 'post'` — see app/gjs.ts. Native-bridge identifiers
137
+ // (`java.*`, `NS*`, …) are ambient GLOBALS on this target, never
138
+ // imports, so they never reach this hook; only a `@gjsify/*` edge does,
139
+ // and on NS as everywhere else an unresolvable one is a broken bundle
140
+ // rather than something the runtime will provide.
141
+ unresolvedWorkspaceImportPlugin({
142
+ target: 'nativescript',
143
+ aliases: aliasEntries,
144
+ isExternal: (id) => external.includes(id),
145
+ }),
120
146
  ];
121
147
  return { options, plugins };
122
148
  };
package/lib/app/node.js CHANGED
@@ -14,6 +14,7 @@ import { nodeModulesPathRewritePlugin, getBundleDirFromOutput } from '../plugins
14
14
  import { cssAsStringPlugin } from '../plugins/css-as-string.js';
15
15
  import { gjsImportsEmptyPlugin } from '../plugins/gjs-imports-empty.js';
16
16
  import { gjsGiNodePlugin, gjsBuiltinModulesNodePlugin } from '../plugins/gjs-gi-node.js';
17
+ import { unresolvedWorkspaceImportPlugin } from '../plugins/unresolved-workspace-import.js';
17
18
  import { wrapInputWithSideEffects } from '../utils/entry-wrapper.js';
18
19
  /**
19
20
  * Side-effect specifier the node target injects when GJS ambient globals
@@ -219,6 +220,10 @@ export const setupForNode = async (input) => {
219
220
  ...input.pluginOptions.aliases,
220
221
  ...input.userAliases,
221
222
  };
223
+ // The substitution table exactly as `aliasPlugin` receives it — shared with
224
+ // the unresolved-workspace-import guard below so the guard reports the same
225
+ // promise the alias layer made.
226
+ const aliasEntries = flattenAliases(aliasMap);
222
227
  const bundleDir = getBundleDirFromOutput(input.output);
223
228
  // Rolldown's CJS interop wraps bundled CJS via `__commonJSMin` and
224
229
  // routes external Node-builtin `require()` through `__require` —
@@ -317,7 +322,7 @@ export const setupForNode = async (input) => {
317
322
  // `requireGi`, exactly like the ambient-globals case. Same predicate as
318
323
  // the register-alias lift above — one question, one flag.
319
324
  gjsImportsEmptyPlugin({ emptyGirs: !gjsSourceBuild }),
320
- aliasPlugin({ entries: flattenAliases(aliasMap) }),
325
+ aliasPlugin({ entries: aliasEntries }),
321
326
  // Blueprint (.blp → XML string): the reverse bridge runs REAL GTK on
322
327
  // Node, so a GJS app entry (composite-template windows) must build for
323
328
  // `--app node` too. Only claims `.blp` files — inert for plain-Node
@@ -334,6 +339,13 @@ export const setupForNode = async (input) => {
334
339
  // symptom). A plain-Node bundle (no node-gi) keeps the CSS pristine.
335
340
  cssAsStringPlugin(input.pluginOptions.nodeGiGlobalsInject ? { targets: { firefox: 60 << 16 } } : {}),
336
341
  nodeModulesPathRewritePlugin({ bundleDir }),
342
+ // `order: 'post'` — fires only for ids nothing above claimed, i.e. the
343
+ // ones headed for Rolldown's unresolved-import → external fallback. The
344
+ // node target has no `node:`-shaped symptom for the emit-time guard to
345
+ // detect (its `node:` builtins ARE external by policy), so this is the
346
+ // ONLY thing standing between an unbuilt `@gjsify/*` workspace edge and
347
+ // a bundle that re-imports the bare specifier at runtime.
348
+ unresolvedWorkspaceImportPlugin({ target: 'node', aliases: aliasEntries, isExternal: external }),
337
349
  ];
338
350
  return { options, plugins };
339
351
  };
package/lib/index.d.ts CHANGED
@@ -11,10 +11,14 @@ export { textLoaderPlugin } from './plugins/text-loader.js';
11
11
  export type { TextLoaderPluginOptions, LoaderKind } from './plugins/text-loader.js';
12
12
  export { shebangPlugin, GJS_SHEBANG, NODE_SHEBANG, expandEnvTemplate, resolveShebangLine } from './plugins/shebang.js';
13
13
  export type { ShebangPluginOptions } from './plugins/shebang.js';
14
+ export { aliasPlugin } from './plugins/alias.js';
15
+ export type { AliasPluginOptions } from './plugins/alias.js';
14
16
  export { gjsImportsEmptyPlugin } from './plugins/gjs-imports-empty.js';
15
17
  export { externalsPlugin } from './plugins/externals.js';
16
18
  export type { ExternalsPredicate, ExternalsPluginOptions } from './plugins/externals.js';
17
- export { napiNodeAddonPlugin, resolveAddonPath, nearestPackageRoot, classifySpecifier, directNodeShim, nodeGypBuildShim, bindingsShim, napiRsShim, isNapiRsPackageJson, detectNapiRsEntry, AddonNotBuiltError, } from './plugins/napi-node-addon.js';
19
+ export { unresolvedWorkspaceImportPlugin, classifyImport, isWorkspaceSpecifier, formatUnresolvedWorkspaceImport, buildReverseAliasIndex, UnresolvedWorkspaceImportError, } from './plugins/unresolved-workspace-import.js';
20
+ export type { WorkspaceImportGuardOptions, WorkspaceImportGuardTarget, ImportVerdict, ClassifyImportInput, UnresolvedWorkspaceImportDetails, } from './plugins/unresolved-workspace-import.js';
21
+ export { napiNodeAddonPlugin, resolveAddonPath, nearestPackageRoot, classifySpecifier, directNodeShim, nodeGypBuildShim, bindingsShim, napiRsShim, ADDON_FILTER_RE, isNapiRsPackageJson, isNapiRsSibling, detectNapiRsEntry, AddonNotBuiltError, } from './plugins/napi-node-addon.js';
18
22
  export type { NapiNodeAddonPluginOptions, AddonPackageJson } from './plugins/napi-node-addon.js';
19
23
  export { platformResolvePlugin, detectNativescriptPlatform, nativescriptPlatformDefines, } from './plugins/platform-resolve.js';
20
24
  export type { PlatformResolvePluginOptions, NativescriptPlatform } from './plugins/platform-resolve.js';
package/lib/index.js CHANGED
@@ -8,9 +8,11 @@ export { processStubPlugin, GJS_PROCESS_STUB, composeBanner } from './plugins/pr
8
8
  export { cssAsStringPlugin } from './plugins/css-as-string.js';
9
9
  export { textLoaderPlugin } from './plugins/text-loader.js';
10
10
  export { shebangPlugin, GJS_SHEBANG, NODE_SHEBANG, expandEnvTemplate, resolveShebangLine } from './plugins/shebang.js';
11
+ export { aliasPlugin } from './plugins/alias.js';
11
12
  export { gjsImportsEmptyPlugin } from './plugins/gjs-imports-empty.js';
12
13
  export { externalsPlugin } from './plugins/externals.js';
13
- export { napiNodeAddonPlugin, resolveAddonPath, nearestPackageRoot, classifySpecifier, directNodeShim, nodeGypBuildShim, bindingsShim, napiRsShim, isNapiRsPackageJson, detectNapiRsEntry, AddonNotBuiltError, } from './plugins/napi-node-addon.js';
14
+ export { unresolvedWorkspaceImportPlugin, classifyImport, isWorkspaceSpecifier, formatUnresolvedWorkspaceImport, buildReverseAliasIndex, UnresolvedWorkspaceImportError, } from './plugins/unresolved-workspace-import.js';
15
+ export { napiNodeAddonPlugin, resolveAddonPath, nearestPackageRoot, classifySpecifier, directNodeShim, nodeGypBuildShim, bindingsShim, napiRsShim, ADDON_FILTER_RE, isNapiRsPackageJson, isNapiRsSibling, detectNapiRsEntry, AddonNotBuiltError, } from './plugins/napi-node-addon.js';
14
16
  export { platformResolvePlugin, detectNativescriptPlatform, nativescriptPlatformDefines, } from './plugins/platform-resolve.js';
15
17
  export * from './plugin.js';
16
18
  import { gjsifyPlugin } from './plugin.js';
@@ -20,6 +20,38 @@
20
20
  // resolve through the "import" condition (Rolldown's default), bypassing the
21
21
  // `cjs-compat.cjs` shim that unwraps named-export ESM modules to their
22
22
  // constructor — breaking `util.inherits(Child, Stream)` patterns.
23
+ //
24
+ // ── Two invariants this plugin holds ────────────────────────────────────────
25
+ //
26
+ // 1. **Gjsify-generated modules are NOT aliased.** An import whose importer is
27
+ // a `\0gjsify-*` virtual module (`isGjsifyVirtualModuleId`) is left alone.
28
+ // We wrote that module's source; it names the RUNTIME module it needs and
29
+ // must get exactly that. The motivating failure: `gjsGiNodePlugin`'s
30
+ // `import { createRequire } from 'node:module'` was rewritten onto the
31
+ // `@gjsify/module` polyfill by the consumer harness's
32
+ // `--alias node:module=@gjsify/module`, so the bundle called the polyfill's
33
+ // `createRequire` at top level before its GLib proxy existed
34
+ // (`TypeError: … reading 'filename_from_uri'`) — and the polyfill's own
35
+ // Gio-backed loader can only reach GLib THROUGH the module it was being
36
+ // asked to load. The guard is importer-based rather than a per-specifier
37
+ // carve-out so it covers every current and future virtual module (entry
38
+ // wrapper, gi-node, napi-addon, gjs-imports-empty).
39
+ //
40
+ // 2. **An alias target is TERMINAL — exactly one hop, never a chain.** The
41
+ // `entries` map handed in is already MERGED across tiers (derived slot
42
+ // routing → curated `ALIASES_*` tables → per-target overrides → user
43
+ // `--alias`, later wins, see `app/*.ts`), so the tier a value came from is
44
+ // no longer recoverable here. Re-feeding a target through the table would
45
+ // therefore apply slot routing to USER aliases too, which is precisely what
46
+ // must not happen: `scripts/node-gi-consumer-harness.mjs` builds
47
+ // `--alias node:<x>=@gjsify/<x>` to put the POLYFILL under test behind a
48
+ // Node builtin, and 34 of those polyfills declare `runtimes.node: "native"`
49
+ // — a second hop would silently route them back to `@gjsify/<x>/globals`
50
+ // (i.e. Node's own builtin) and the suite would measure nothing. The
51
+ // "second pass" of the slot-routing model is a COMPOSITION concern, and it
52
+ // is resolved where the tiers are still distinguishable — see
53
+ // `withDerivedSlotRouting` in `@gjsify/resolve-npm`.
54
+ import { isGjsifyVirtualModuleId } from '../utils/virtual-module-id.js';
23
55
  export function aliasPlugin(options) {
24
56
  const entries = options.entries;
25
57
  return {
@@ -27,6 +59,10 @@ export function aliasPlugin(options) {
27
59
  resolveId: {
28
60
  order: 'pre',
29
61
  async handler(source, importer, extraOptions) {
62
+ // Invariant 1 — never rewrite an import that WE generated.
63
+ if (isGjsifyVirtualModuleId(importer)) {
64
+ return null;
65
+ }
30
66
  if (!Object.prototype.hasOwnProperty.call(entries, source)) {
31
67
  return null;
32
68
  }
@@ -121,7 +121,32 @@ async function tryLoadNativeBundler() {
121
121
  }
122
122
  }
123
123
  async function loadNpmBundler() {
124
- const { bundleAsync } = await import('lightningcss');
124
+ // INDIRECT specifier, for the same reason `tryLoadNativeBundler` uses one:
125
+ // keep this acquisition out of the BUILD graph so it is resolved at
126
+ // runtime only. npm `lightningcss` is a napi-rs package, and a static
127
+ // `import('lightningcss')` here put its generated loader into the module
128
+ // graph of every `--app gjs` bundle — including the CLI's own committed
129
+ // `dist/cli.gjs.mjs`.
130
+ //
131
+ // That is what made the committed bundle ENVIRONMENT-DEPENDENT. With
132
+ // `@gjsify/napi` resolvable, `napiNodeAddonPlugin` rewrites the loader to a
133
+ // `loadAddon()` shim and inlines it; without it the plugin declines and the
134
+ // import stays external. Two different module graphs, so the minifier
135
+ // assigns different variable names, so the artifact stops reproducing from
136
+ // its own source (`verify-committed-bundles` red, first difference at byte
137
+ // 275: `r=` vs `a=` for `globalThis.imports.gi.GLib`). A toolchain artifact
138
+ // must not depend on whether an optional package happened to be installed
139
+ // when it was built.
140
+ //
141
+ // Nothing is lost under GJS: the documented GJS backend is
142
+ // `@gjsify/lightningcss-native` (above), and this npm fallback was never
143
+ // actually usable there — its `.node` cannot load under GJS without a
144
+ // build-time rewrite, and the only arches where that rewrite is possible
145
+ // (`@gjsify/napi`: linux-x64, darwin-arm64) are arches where the native
146
+ // bridge exists anyway. On Node this is unchanged: `lightningcss` is a real
147
+ // `dependency` of this package, so the runtime resolve finds it.
148
+ const specifier = 'lightningcss';
149
+ const { bundleAsync } = (await import(/* @vite-ignore */ specifier));
125
150
  return async (filename, targets) => {
126
151
  const result = await bundleAsync({
127
152
  filename,
@@ -27,7 +27,11 @@
27
27
  // Portability note (same as `gjs-imports-empty.ts`): the `filter` is a Rolldown
28
28
  // fast-path; the internal guard in the handler is the load-bearing check so the
29
29
  // plugin is correct even when the filter does not pre-filter (e.g. under Vite).
30
- const GI_NODE_VIRTUAL_PREFIX = '\0gjsify-gi-node:';
30
+ import { GJSIFY_VIRTUAL_PREFIX } from '../utils/virtual-module-id.js';
31
+ // Derived from the shared prefix so `isGjsifyVirtualModuleId` recognises these
32
+ // ids — that is what keeps the `node:module` import below bound to the RUNTIME's
33
+ // `createRequire` even when the build carries a user `--alias node:module=…`.
34
+ const GI_NODE_VIRTUAL_PREFIX = `${GJSIFY_VIRTUAL_PREFIX}gi-node:`;
31
35
  /** Parse a `gi://Ns?version=X` specifier into its namespace + optional version. */
32
36
  export function parseGiSpecifier(source) {
33
37
  if (!source.startsWith('gi://'))
@@ -31,7 +31,8 @@
31
31
  // dev server) the handler may receive ALL ids. The internal guard below makes
32
32
  // the plugin correct regardless of whether the filter pre-filtered — it is
33
33
  // the load-bearing check; the `filter` is a defense-in-depth optimization.
34
- const GJSIMPORTS_VIRTUAL_ID = '\0gjsify-empty-gjs-import';
34
+ import { GJSIFY_VIRTUAL_PREFIX } from '../utils/virtual-module-id.js';
35
+ const GJSIMPORTS_VIRTUAL_ID = `${GJSIFY_VIRTUAL_PREFIX}empty-gjs-import`;
35
36
  export function gjsImportsEmptyPlugin(options = {}) {
36
37
  const emptyGirs = options.emptyGirs ?? true;
37
38
  // When `@girs/*` is carved out, only `gi://*` reaches the empty redirect.
@@ -1,4 +1,13 @@
1
1
  import type { Plugin } from 'rolldown';
2
+ /**
3
+ * Fast-path filter (superset of what `claimSpecifier` claims). Handler re-checks.
4
+ *
5
+ * Exported so `napi-node-addon.spec.ts` can assert its Rust-compatibility (see
6
+ * {@link BARE_SPECIFIER_RE}) — a JS-only construct here disables the plugin
7
+ * entirely under `@gjsify/rolldown-native`, and no build-level test would see
8
+ * it, because every addon gate drives the Node CLI entry.
9
+ */
10
+ export declare const ADDON_FILTER_RE: RegExp;
2
11
  /** Options for {@link napiNodeAddonPlugin}. */
3
12
  export interface NapiNodeAddonPluginOptions {
4
13
  /**
@@ -54,6 +63,8 @@ export declare function classifySpecifier(source: string): {
54
63
  export interface AddonPackageJson {
55
64
  name?: string;
56
65
  main?: string;
66
+ module?: string;
67
+ exports?: unknown;
57
68
  browser?: string;
58
69
  napi?: unknown;
59
70
  optionalDependencies?: Record<string, string>;
@@ -65,11 +76,13 @@ export interface AddonPackageJson {
65
76
  * (a) a top-level `napi` config OBJECT (`{ binaryName, targets, … }`) — the
66
77
  * napi-rs CLI's own build block. An ordinary npm package never declares a
67
78
  * `napi` object at the manifest root.
68
- * (b) at least one `optionalDependencies` entry named `<self>-<triple>` — the
69
- * napi-rs prebuilt-binary convention (`@node-rs/argon2` ships
70
- * `@node-rs/argon2-linux-x64-gnu`, …). The key MUST start with the
71
- * package's own name plus `-` AND end in a platform triple, so a normal
72
- * optional dep (`fsevents`) can't match.
79
+ * (b) at least one `optionalDependencies` entry that is a platform SIBLING
80
+ * (see {@link isNapiRsSibling}) — the napi-rs prebuilt-binary convention,
81
+ * under either naming scheme: `<self>-<triple>` (`@node-rs/argon2` ships
82
+ * `@node-rs/argon2-linux-x64-gnu`) or `<napi.packageName>-<triple>`
83
+ * (`rolldown` ships `@rolldown/binding-linux-x64-gnu`). The key MUST
84
+ * carry a known prefix AND end in a platform triple, so a normal optional
85
+ * dep (`fsevents`) can't match.
73
86
  *
74
87
  * Neither fires on a normal package, so pairing this with the "resolved file IS
75
88
  * the package's own native main entry" check (see {@link detectNapiRsEntry})
@@ -88,6 +101,13 @@ export declare function detectNapiRsEntry(entryFile: string): {
88
101
  pkgRoot: string;
89
102
  pkg: AddonPackageJson;
90
103
  } | null;
104
+ /**
105
+ * Is `dep` a platform sibling of `pkg` — `<prefix>-<triple>` for one of the
106
+ * prefixes above? Both halves must hold: a bare prefix match would claim an
107
+ * ordinary dependency, and a bare triple match would claim an unrelated
108
+ * package's binaries. Exported for the unit tests.
109
+ */
110
+ export declare function isNapiRsSibling(pkg: AddonPackageJson, dep: string): boolean;
91
111
  /**
92
112
  * Transparent `.node`-addon loader for `gjsify build --app gjs`.
93
113
  *