@gjsify/rolldown-plugin-gjsify 0.25.1 → 0.26.1

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.
@@ -24,7 +24,7 @@
24
24
  // "this package IS the native implementation" sense and therefore routes to a
25
25
  // `globals.mjs` it does not ship → `@gjsify/empty`. The slot vocabulary's
26
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
27
+ // packages are mis-declared; that is tracked in status/open-todos.md and must be settled
28
28
  // before this table is composed. A direct `import … from
29
29
  // '@gjsify/native-fs-bridge'` already hits the derived map (plain key hit) and
30
30
  // is emptied TODAY — same root cause, listed in the same TODO.
package/lib/app/node.d.ts CHANGED
@@ -30,7 +30,9 @@ export declare function enableGjsRegistersForNode(baseAliases: Record<string, st
30
30
  *
31
31
  * Two signals qualify, and BOTH must gate the same things:
32
32
  * - `nodeGiGlobalsInject` — the CLI's post-tree-shake detection found bare GJS
33
- * ambient globals (`print`/`imports`/`ARGV`/…) in the bundled output;
33
+ * ambient globals (`print`/`imports`/`ARGV`/…) OR a static
34
+ * `@gjsify/node-gi/*` import (the externalised spelling of the bare
35
+ * `system`/`gettext`/`cairo` built-ins) in the bundled output;
34
36
  * - `registerInject` (the `--globals` auto/explicit inject stub) — the user
35
37
  * asked for GJS registers on the node target explicitly.
36
38
  *
@@ -46,11 +48,16 @@ export declare function enableGjsRegistersForNode(baseAliases: Record<string, st
46
48
  * routing and the `@girs/*` emptying both stay, so a cross-platform package's
47
49
  * node bundle keeps loading on plain Node without node-gi installed.
48
50
  *
49
- * KNOWN NARROWNESS (pre-dates this predicate, tracked in STATUS.md): the
50
- * `nodeGiGlobalsInject` detector keys on BARE ambient globals, so a genuine GJS
51
- * source that uses `gi://` but logs via `console.log` and passes no `--globals`
52
- * is not recognised — its `@girs/*` AND its registers are both emptied. The two
53
- * are now at parity; widening the signal itself is the separate fix.
51
+ * KNOWN NARROWNESS (pre-dates this predicate, tracked in status/open-todos.md): the
52
+ * `nodeGiGlobalsInject` detector keys on bare ambient globals + static
53
+ * `@gjsify/node-gi/*` imports, so a genuine GJS source whose ONLY platform
54
+ * reach is `gi://` (no ambient global, no bare `system`/`gettext`/`cairo`) is
55
+ * still not recognised — its `@girs/*` AND its registers are both emptied. A
56
+ * surviving `gi://` import CANNOT simply become a third signal: its shim loads
57
+ * node-gi lazily precisely so a gjs-gated `gi://` import in a cross-platform
58
+ * package keeps that package's node bundle loadable on plain Node (#641,
59
+ * pinned by `tests/e2e/node-gi-globals-inject`), and injecting the eager
60
+ * globals shim for it would break exactly that.
54
61
  */
55
62
  export declare function isGjsSourceBuild(options: {
56
63
  nodeGiGlobalsInject?: boolean;
package/lib/app/node.js CHANGED
@@ -92,7 +92,9 @@ export function enableGjsRegistersForNode(baseAliases) {
92
92
  *
93
93
  * Two signals qualify, and BOTH must gate the same things:
94
94
  * - `nodeGiGlobalsInject` — the CLI's post-tree-shake detection found bare GJS
95
- * ambient globals (`print`/`imports`/`ARGV`/…) in the bundled output;
95
+ * ambient globals (`print`/`imports`/`ARGV`/…) OR a static
96
+ * `@gjsify/node-gi/*` import (the externalised spelling of the bare
97
+ * `system`/`gettext`/`cairo` built-ins) in the bundled output;
96
98
  * - `registerInject` (the `--globals` auto/explicit inject stub) — the user
97
99
  * asked for GJS registers on the node target explicitly.
98
100
  *
@@ -108,11 +110,16 @@ export function enableGjsRegistersForNode(baseAliases) {
108
110
  * routing and the `@girs/*` emptying both stay, so a cross-platform package's
109
111
  * node bundle keeps loading on plain Node without node-gi installed.
110
112
  *
111
- * KNOWN NARROWNESS (pre-dates this predicate, tracked in STATUS.md): the
112
- * `nodeGiGlobalsInject` detector keys on BARE ambient globals, so a genuine GJS
113
- * source that uses `gi://` but logs via `console.log` and passes no `--globals`
114
- * is not recognised — its `@girs/*` AND its registers are both emptied. The two
115
- * are now at parity; widening the signal itself is the separate fix.
113
+ * KNOWN NARROWNESS (pre-dates this predicate, tracked in status/open-todos.md): the
114
+ * `nodeGiGlobalsInject` detector keys on bare ambient globals + static
115
+ * `@gjsify/node-gi/*` imports, so a genuine GJS source whose ONLY platform
116
+ * reach is `gi://` (no ambient global, no bare `system`/`gettext`/`cairo`) is
117
+ * still not recognised — its `@girs/*` AND its registers are both emptied. A
118
+ * surviving `gi://` import CANNOT simply become a third signal: its shim loads
119
+ * node-gi lazily precisely so a gjs-gated `gi://` import in a cross-platform
120
+ * package keeps that package's node bundle loadable on plain Node (#641,
121
+ * pinned by `tests/e2e/node-gi-globals-inject`), and injecting the eager
122
+ * globals shim for it would break exactly that.
116
123
  */
117
124
  export function isGjsSourceBuild(options) {
118
125
  return Boolean(options.nodeGiGlobalsInject || options.registerInject);
@@ -166,14 +173,16 @@ export const setupForNode = async (input) => {
166
173
  const format = input.pluginOptions.format ?? 'esm';
167
174
  const exclude = input.pluginOptions.exclude ?? [];
168
175
  const entryPoints = await globToEntryPoints(input.input, exclude);
169
- // When the CLI's pre-build detection (`detectNodeGiGlobals`) found GJS
170
- // ambient globals in the bundled output, wrap the entry so it side-effect
171
- // imports the node-gi globals shim BEFORE the user code runs. The import is
172
- // injected ONLY when detected — a bundle that never touches `print`/`imports`
173
- // stays free of `@gjsify/node-gi/globals`, so it loads on plain Node without
174
- // node-gi installed (the eager-native-load regression guard, same lesson as
175
- // the lazy `gi://` shim in #641). Reuses the SAME virtual-entry wrapper the
176
- // GJS `--globals auto` machinery uses.
176
+ // When the CLI's pre-build detection (`detectNodeGiGlobals`) recognised a
177
+ // genuine GJS source (bare ambient globals OR a static `@gjsify/node-gi/*`
178
+ // import in the bundled output), wrap the entry so it side-effect imports
179
+ // the node-gi globals shim BEFORE the user code runs. The import is
180
+ // injected ONLY when detected — a bundle that neither touches
181
+ // `print`/`imports` nor retains a bare `system`/`gettext`/`cairo` import
182
+ // stays free of `@gjsify/node-gi/globals`, so it loads on plain Node
183
+ // without node-gi installed (the eager-native-load regression guard, same
184
+ // lesson as the lazy `gi://` shim in #641). Reuses the SAME virtual-entry
185
+ // wrapper the GJS `--globals auto` machinery uses.
177
186
  const sideEffectImports = [];
178
187
  if (input.pluginOptions.nodeGiGlobalsInject)
179
188
  sideEffectImports.push(NODE_GI_GLOBALS_SPECIFIER);
@@ -192,9 +201,10 @@ export const setupForNode = async (input) => {
192
201
  * TWO signals answer that ONE question and both must count:
193
202
  *
194
203
  * - `nodeGiGlobalsInject` — the CLI detected BARE GJS ambient globals
195
- * (`print`/`imports`/`ARGV`) in the tree-shaken output. Only genuine GJS
196
- * source has those (the detector deliberately ignores the
197
- * `globalThis.imports` isomorphic-guard shape).
204
+ * (`print`/`imports`/`ARGV`) or a static `@gjsify/node-gi/*` import
205
+ * (a retained bare `system`/`gettext`/`cairo` built-in) in the
206
+ * tree-shaken output. Only genuine GJS source has those (the detector
207
+ * deliberately ignores the `globalThis.imports` isomorphic-guard shape).
198
208
  * - `autoGlobalsInject` — the user named EXPLICIT `--globals` (e.g.
199
209
  * `--globals auto,dom`), i.e. asked for the DOM surface on Node.
200
210
  *
package/lib/globals.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export { resolveGlobalsList, writeRegisterInjectFile, isRegisterPathResolvable, filterResolvableRegisterPaths } from './utils/scan-globals.js';
2
- export { detectFreeGlobals, detectGjsAmbientGlobals, GJS_AMBIENT_GLOBALS } from './utils/detect-free-globals.js';
2
+ export { detectFreeGlobals, detectGjsAmbientGlobals, detectNodeGiModuleImports, GJS_AMBIENT_GLOBALS, } from './utils/detect-free-globals.js';
3
3
  export { detectAutoGlobals, detectNodeGiGlobals, describeGiBackedInjection } from './utils/auto-globals.js';
4
4
  export type { AutoGlobalsResult, DetectAutoGlobalsOptions, AnalysisOptions } from './utils/auto-globals.js';
package/lib/globals.js CHANGED
@@ -5,5 +5,5 @@
5
5
  // stub that the plugin picks up via its `autoGlobalsInject` option. See the
6
6
  // "Tree-shakeable Globals" section in AGENTS.md for the architecture.
7
7
  export { resolveGlobalsList, writeRegisterInjectFile, isRegisterPathResolvable, filterResolvableRegisterPaths } from './utils/scan-globals.js';
8
- export { detectFreeGlobals, detectGjsAmbientGlobals, GJS_AMBIENT_GLOBALS } from './utils/detect-free-globals.js';
8
+ export { detectFreeGlobals, detectGjsAmbientGlobals, detectNodeGiModuleImports, GJS_AMBIENT_GLOBALS, } from './utils/detect-free-globals.js';
9
9
  export { detectAutoGlobals, detectNodeGiGlobals, describeGiBackedInjection } from './utils/auto-globals.js';
@@ -136,13 +136,24 @@ type GjsifyPluginFactory = (options: PluginOptions) => GjsifyFactoryReturn | Pro
136
136
  */
137
137
  export declare function detectAutoGlobals(analysisOptions: AnalysisOptions, pluginOptions: Omit<PluginOptions, 'autoGlobalsInject'>, gjsifyPluginFactory: GjsifyPluginFactory, verbose?: boolean, options?: DetectAutoGlobalsOptions, bundler?: AnalysisBundler): Promise<AutoGlobalsResult>;
138
138
  /**
139
- * `--app node` GJS-ambient-globals detection (the reverse of `--globals auto`).
139
+ * `--app node` genuine-GJS-source detection (the reverse of `--globals auto`).
140
140
  *
141
- * Runs ONE in-memory node bundle (no injection) and scans each chunk for the
142
- * GJS ambient globals (`print`/`printerr`/`log`/`logError`/`ARGV`/`imports`)
143
- * that `@gjsify/node-gi/globals` seeds. Returns `true` iff any is referenced in
144
- * the bundled, tree-shaken output — the signal the CLI uses to flip
145
- * `pluginOptions.nodeGiGlobalsInject` for the final build.
141
+ * Runs ONE in-memory node bundle (no injection) and scans each chunk for TWO
142
+ * signals, either of which flips `pluginOptions.nodeGiGlobalsInject` for the
143
+ * final build:
144
+ *
145
+ * 1. a GJS ambient global (`print`/`printerr`/`log`/`logError`/`ARGV`/
146
+ * `imports`) referenced as a BARE identifier — the legacy GJS-source shape
147
+ * `@gjsify/node-gi/globals` seeds (`detectGjsAmbientGlobals`);
148
+ * 2. a STATIC import of `@gjsify/node-gi` (root or subpath) — the shape the
149
+ * bare GJS built-ins (`system`/`gettext`/`cairo`) are externalised to, i.e.
150
+ * the PORTABLE GJS-source spelling (`detectNodeGiModuleImports`). A bundle
151
+ * retaining one is already bound to the bridge at load, so flipping
152
+ * injection cannot cost plain-Node loadability — while a portable GJS app
153
+ * with no ambient global at all (e.g. `system.programArgs` instead of
154
+ * `ARGV`) would otherwise get its `@girs/*` bodies emptied and its globals
155
+ * shim dropped (the `gjsify storybook --runtime node` no-`requireGi`
156
+ * regression).
146
157
  *
147
158
  * A SINGLE pass suffices (unlike the GJS register-injection loop): the shim is
148
159
  * EXTERNAL, so injecting it adds no bundled code that could reference more
@@ -36,7 +36,7 @@ const defaultBundler = async ({ rolldownInput, format }) => {
36
36
  await build.close();
37
37
  }
38
38
  };
39
- import { detectFreeGlobals, detectGjsAmbientGlobals } from './detect-free-globals.js';
39
+ import { detectFreeGlobals, detectGjsAmbientGlobals, detectNodeGiModuleImports } from './detect-free-globals.js';
40
40
  import { resolveGlobalsList, writeRegisterInjectFile, filterResolvableRegisterPaths, isRegisterPathResolvable, } from './scan-globals.js';
41
41
  import { GJS_GLOBALS_MAP, GJS_GI_BACKED_REGISTERS } from '@gjsify/resolve-npm/globals-map';
42
42
  import { REGISTER_GLOBALS_CLOSURE } from '@gjsify/resolve-npm/register-globals-closure';
@@ -330,7 +330,11 @@ export async function detectAutoGlobals(analysisOptions, pluginOptions, gjsifyPl
330
330
  console.error(`[gjsify-auto-globals] parse failed on chunk #${i} — wrote ${path} for inspection`);
331
331
  }
332
332
  catch {
333
- /* ignore */
333
+ // Deliberately swallowed, and the ONLY reason this
334
+ // catch is legitimate: writing the debug dump is
335
+ // best-effort (permissions, disk), and letting its
336
+ // failure escape would replace the real parse error
337
+ // `e` — rethrown below — with a misleading one.
334
338
  }
335
339
  }
336
340
  throw e;
@@ -405,13 +409,24 @@ export async function detectAutoGlobals(analysisOptions, pluginOptions, gjsifyPl
405
409
  return applyExcludeGlobals(detected, currentInject, extraRegisterPaths, options.excludeGlobals, cwd);
406
410
  }
407
411
  /**
408
- * `--app node` GJS-ambient-globals detection (the reverse of `--globals auto`).
412
+ * `--app node` genuine-GJS-source detection (the reverse of `--globals auto`).
409
413
  *
410
- * Runs ONE in-memory node bundle (no injection) and scans each chunk for the
411
- * GJS ambient globals (`print`/`printerr`/`log`/`logError`/`ARGV`/`imports`)
412
- * that `@gjsify/node-gi/globals` seeds. Returns `true` iff any is referenced in
413
- * the bundled, tree-shaken output — the signal the CLI uses to flip
414
- * `pluginOptions.nodeGiGlobalsInject` for the final build.
414
+ * Runs ONE in-memory node bundle (no injection) and scans each chunk for TWO
415
+ * signals, either of which flips `pluginOptions.nodeGiGlobalsInject` for the
416
+ * final build:
417
+ *
418
+ * 1. a GJS ambient global (`print`/`printerr`/`log`/`logError`/`ARGV`/
419
+ * `imports`) referenced as a BARE identifier — the legacy GJS-source shape
420
+ * `@gjsify/node-gi/globals` seeds (`detectGjsAmbientGlobals`);
421
+ * 2. a STATIC import of `@gjsify/node-gi` (root or subpath) — the shape the
422
+ * bare GJS built-ins (`system`/`gettext`/`cairo`) are externalised to, i.e.
423
+ * the PORTABLE GJS-source spelling (`detectNodeGiModuleImports`). A bundle
424
+ * retaining one is already bound to the bridge at load, so flipping
425
+ * injection cannot cost plain-Node loadability — while a portable GJS app
426
+ * with no ambient global at all (e.g. `system.programArgs` instead of
427
+ * `ARGV`) would otherwise get its `@girs/*` bodies emptied and its globals
428
+ * shim dropped (the `gjsify storybook --runtime node` no-`requireGi`
429
+ * regression).
415
430
  *
416
431
  * A SINGLE pass suffices (unlike the GJS register-injection loop): the shim is
417
432
  * EXTERNAL, so injecting it adds no bundled code that could reference more
@@ -481,6 +496,8 @@ export async function detectNodeGiGlobals(analysisOptions, pluginOptions, gjsify
481
496
  for (const code of chunkCodes) {
482
497
  if (detectGjsAmbientGlobals(code).size > 0)
483
498
  return true;
499
+ if (detectNodeGiModuleImports(code))
500
+ return true;
484
501
  }
485
502
  return false;
486
503
  }
@@ -33,3 +33,30 @@ export declare const GJS_AMBIENT_GLOBALS: ReadonlySet<string>;
33
33
  * `ignoreHostMembers`.
34
34
  */
35
35
  export declare function detectGjsAmbientGlobals(code: string): Set<string>;
36
+ /**
37
+ * Detect a STATIC import of `@gjsify/node-gi` (root or any subpath) in a
38
+ * bundled `--app node` chunk — the second genuine-GJS-source signal next to
39
+ * `detectGjsAmbientGlobals`.
40
+ *
41
+ * Why this is a signal: the bare GJS built-in modules (`system`/`gettext`/
42
+ * `cairo`) are rewritten to `@gjsify/node-gi/<name>` and kept EXTERNAL as
43
+ * top-level import statements, so any bundle whose tree-shaken graph retains
44
+ * one is ALREADY bound to the bridge at load — Node resolves the external at
45
+ * link time and fails without node-gi installed. Treating that bundle as a
46
+ * GJS-source build therefore cannot cost plain-Node loadability; NOT treating
47
+ * it as one is how a portable GJS app that spells `ARGV` as
48
+ * `system.programArgs` (the AGENTS.md-mandated spelling) lost its `@girs/*`
49
+ * bodies and its globals shim in one go — `gjsify storybook --runtime node`
50
+ * emitted a bundle with no `requireGi` at all.
51
+ *
52
+ * Why a surviving `gi://` import is deliberately NOT a signal: its shim loads
53
+ * node-gi LAZILY through a `require('@gjsify/node-gi/gi')` CALL — not an import
54
+ * statement, so this scan is structurally blind to it, by design. A
55
+ * cross-platform package with a gjs-gated `gi://` import stays loadable on
56
+ * plain Node (the #641 lesson, pinned by `tests/e2e/node-gi-globals-inject`),
57
+ * and flipping injection for it would break exactly that.
58
+ *
59
+ * Scans import/re-export STATEMENTS via the AST (never text), so a quoted
60
+ * `'@gjsify/node-gi/system'` inside a test name or template cannot trigger.
61
+ */
62
+ export declare function detectNodeGiModuleImports(code: string): boolean;
@@ -413,3 +413,78 @@ export function detectGjsAmbientGlobals(code) {
413
413
  ignoreHostMembers: true,
414
414
  });
415
415
  }
416
+ /** The node-gi reverse-bridge package every `--app node` GJS shim resolves to. */
417
+ import { ALIASES_GJS_FOR_NODE } from '@gjsify/resolve-npm';
418
+ const NODE_GI_PACKAGE = '@gjsify/node-gi';
419
+ /**
420
+ * The BARE GJS built-ins, i.e. the PRE-ALIAS spelling of the same fact.
421
+ * Derived from the alias table so the two cannot drift.
422
+ *
423
+ * Why both spellings must count: the analysis pass that calls this runs with
424
+ * `nodeGiGlobalsInject` still FALSE, so the GJS-source routing that rewrites
425
+ * `system` → `@gjsify/node-gi/system` has not happened yet. Looking only for
426
+ * the aliased form makes the signal appear only AFTER the decision it is
427
+ * supposed to trigger — chicken-and-egg, and the same shape as the bug this
428
+ * detector was added to fix.
429
+ *
430
+ * Measured (CI, adwaita-storybook `--app node`): the FINAL bundle carries
431
+ * `import e from "@gjsify/node-gi/system"` and zero `gi://`, because the
432
+ * inject never fired, `@girs/*` were emptied to `{}`, and nothing was left to
433
+ * rewrite. The bundle then passes `node --check` and dies at runtime with
434
+ * `class extends undefined`.
435
+ */
436
+ const GJS_BARE_BUILTINS = new Set(Object.keys(ALIASES_GJS_FOR_NODE));
437
+ /**
438
+ * Is `source` the `@gjsify/node-gi` package root, one of its subpaths, or the
439
+ * bare GJS built-in that resolves to one on the node target?
440
+ */
441
+ function isNodeGiSpecifier(source) {
442
+ if (typeof source !== 'string')
443
+ return false;
444
+ if (source === NODE_GI_PACKAGE || source.startsWith(`${NODE_GI_PACKAGE}/`))
445
+ return true;
446
+ return GJS_BARE_BUILTINS.has(source);
447
+ }
448
+ /**
449
+ * Detect a STATIC import of `@gjsify/node-gi` (root or any subpath) in a
450
+ * bundled `--app node` chunk — the second genuine-GJS-source signal next to
451
+ * `detectGjsAmbientGlobals`.
452
+ *
453
+ * Why this is a signal: the bare GJS built-in modules (`system`/`gettext`/
454
+ * `cairo`) are rewritten to `@gjsify/node-gi/<name>` and kept EXTERNAL as
455
+ * top-level import statements, so any bundle whose tree-shaken graph retains
456
+ * one is ALREADY bound to the bridge at load — Node resolves the external at
457
+ * link time and fails without node-gi installed. Treating that bundle as a
458
+ * GJS-source build therefore cannot cost plain-Node loadability; NOT treating
459
+ * it as one is how a portable GJS app that spells `ARGV` as
460
+ * `system.programArgs` (the AGENTS.md-mandated spelling) lost its `@girs/*`
461
+ * bodies and its globals shim in one go — `gjsify storybook --runtime node`
462
+ * emitted a bundle with no `requireGi` at all.
463
+ *
464
+ * Why a surviving `gi://` import is deliberately NOT a signal: its shim loads
465
+ * node-gi LAZILY through a `require('@gjsify/node-gi/gi')` CALL — not an import
466
+ * statement, so this scan is structurally blind to it, by design. A
467
+ * cross-platform package with a gjs-gated `gi://` import stays loadable on
468
+ * plain Node (the #641 lesson, pinned by `tests/e2e/node-gi-globals-inject`),
469
+ * and flipping injection for it would break exactly that.
470
+ *
471
+ * Scans import/re-export STATEMENTS via the AST (never text), so a quoted
472
+ * `'@gjsify/node-gi/system'` inside a test name or template cannot trigger.
473
+ */
474
+ export function detectNodeGiModuleImports(code) {
475
+ const ast = acorn.parse(code, {
476
+ ecmaVersion: 'latest',
477
+ sourceType: 'module',
478
+ allowHashBang: true,
479
+ });
480
+ for (const node of ast.body) {
481
+ if (node.type === 'ImportDeclaration' && isNodeGiSpecifier(node.source.value))
482
+ return true;
483
+ if ((node.type === 'ExportNamedDeclaration' || node.type === 'ExportAllDeclaration') &&
484
+ node.source &&
485
+ isNodeGiSpecifier(node.source.value)) {
486
+ return true;
487
+ }
488
+ }
489
+ return false;
490
+ }
@@ -13,24 +13,64 @@
13
13
  // not parseable as runtime modules. esbuild handled this implicitly via
14
14
  // its loader table; Rolldown's Oxc parser errors on declaration-only
15
15
  // shapes (`get foo(): T;`).
16
+ //
17
+ // DETERMINISM — entry ORDER is an input to the emitted BYTES, so each
18
+ // pattern's expansion is sorted. Rolldown derives every module's and
19
+ // external's exec order from a DFS that starts at the entries IN ORDER
20
+ // (`sort_modules.rs`), and each emitted chunk renders its import statements
21
+ // sorted by that exec order (`compute_cross_chunk_links.rs`) — so a
22
+ // multi-entry `--library` build emits differently-ordered `lib/esm/*.js`
23
+ // import statements when the entry order changes, and every app bundle that
24
+ // inlines those libs (the committed `dist/*.gjs.mjs`) inherits the drift.
25
+ // fast-glob does NOT pin that order: its concurrent directory walker yields
26
+ // matches in COMPLETION order, so sibling directories race (measured: 9
27
+ // distinct orderings in 60 runs over one 6-subdir tree). That race is what
28
+ // made the committed GJS bundles reproduce on CI but not on developer
29
+ // machines (`verify-committed-bundles.mjs`'s `system`/`gi://GioUnix`
30
+ // hoisted-import swap; the `makeCallable`/`mapSysname` module-order
31
+ // divergence noted in release-cut.yml). The sort is `Array.prototype.sort`'s
32
+ // UTF-16 code-unit order — locale-independent, unlike the `strcoll`-based
33
+ // ordering some readdir paths apply. The pattern-LIST order is the user's
34
+ // explicit input (entry execution order) and is preserved; only each
35
+ // pattern's own expansion is canonicalized. Guarded by
36
+ // `packages/infra/cli/src/entry-points.spec.ts` and
37
+ // `tests/e2e/deterministic-library-build/`.
16
38
  import fastGlob from 'fast-glob';
17
39
  const DEFAULT_IGNORE = ['**/*.d.ts'];
40
+ /** Expand ONE pattern to its lexicographically sorted match list. */
41
+ async function expandPattern(pattern, ignore) {
42
+ const matched = await fastGlob([pattern], { ignore });
43
+ return matched.sort();
44
+ }
18
45
  export const globToEntryPoints = async (_entryPoints, ignore = []) => {
19
46
  if (_entryPoints === undefined)
20
47
  return undefined;
21
48
  const fullIgnore = [...DEFAULT_IGNORE, ...ignore];
22
49
  if (typeof _entryPoints === 'string') {
23
- const expanded = await fastGlob([_entryPoints], { ignore: fullIgnore });
24
- return expanded;
50
+ return await expandPattern(_entryPoints, fullIgnore);
25
51
  }
26
52
  if (Array.isArray(_entryPoints)) {
27
- return await fastGlob(_entryPoints, { ignore: fullIgnore });
53
+ // Per-pattern expansion (not one combined fast-glob call): the
54
+ // combined call would interleave matches across patterns in walker
55
+ // order, losing both the sort and the user's pattern order. Overlapping
56
+ // patterns are deduped like fast-glob's `unique` did — first
57
+ // occurrence wins.
58
+ const out = [];
59
+ const seen = new Set();
60
+ for (const pattern of _entryPoints) {
61
+ for (const matched of await expandPattern(pattern, fullIgnore)) {
62
+ if (!seen.has(matched)) {
63
+ seen.add(matched);
64
+ out.push(matched);
65
+ }
66
+ }
67
+ }
68
+ return out;
28
69
  }
29
70
  const entryPoints = {};
30
71
  for (const input in _entryPoints) {
31
72
  const output = _entryPoints[input];
32
- const inputs = await fastGlob(input, { ignore: fullIgnore });
33
- for (const matched of inputs) {
73
+ for (const matched of await expandPattern(input, fullIgnore)) {
34
74
  entryPoints[matched] = output;
35
75
  }
36
76
  }
@@ -158,7 +158,7 @@ function tryInlineCall(node, ctx, _src) {
158
158
  if (hasWithFileTypes(node.arguments[1]))
159
159
  return undefined;
160
160
  const path = evalPathExpr(node.arguments[0], ctx);
161
- if (path && existsSyncSafe(path) && isDirectorySafe(path)) {
161
+ if (path && existsSync(path) && isDirectorySafe(path)) {
162
162
  try {
163
163
  const names = readdirSync(path);
164
164
  return {
@@ -178,7 +178,7 @@ function tryInlineCall(node, ctx, _src) {
178
178
  return {
179
179
  start: node.start,
180
180
  end: node.end,
181
- replacement: existsSyncSafe(path) ? 'true' : 'false',
181
+ replacement: existsSync(path) ? 'true' : 'false',
182
182
  };
183
183
  }
184
184
  }
@@ -204,7 +204,7 @@ function tryInlineCall(node, ctx, _src) {
204
204
  // where Node's PnP hooks make `existsSync` return true at build
205
205
  // time but the path doesn't exist under GJS at runtime).
206
206
  const isZip = path !== undefined && path.includes('.zip/');
207
- if (path !== undefined && (isZip || !existsSyncSafe(path))) {
207
+ if (path !== undefined && (isZip || !existsSync(path))) {
208
208
  return {
209
209
  start: node.start,
210
210
  end: node.end,
@@ -231,7 +231,7 @@ function tryInlineReadFile(node, ctx, forceTextEncoding) {
231
231
  const path = evalPathExpr(node.arguments[0], ctx);
232
232
  if (!path)
233
233
  return undefined;
234
- if (!existsSyncSafe(path) || isDirectorySafe(path))
234
+ if (!existsSync(path) || isDirectorySafe(path))
235
235
  return undefined;
236
236
  let encoding;
237
237
  if (forceTextEncoding) {
@@ -580,14 +580,9 @@ function jsLiteral(v) {
580
580
  function jsStringLiteral(s) {
581
581
  return JSON.stringify(s);
582
582
  }
583
- function existsSyncSafe(path) {
584
- try {
585
- return existsSync(path);
586
- }
587
- catch {
588
- return false;
589
- }
590
- }
583
+ // NOTE: `existsSync` needs no wrapper — it never throws by contract (Node
584
+ // returns false on any error, and the GJS/Bun/Deno node:fs shims match).
585
+ // `statSync` below genuinely throws (ENOENT etc.), hence its guarded helper.
591
586
  function isDirectorySafe(path) {
592
587
  try {
593
588
  return statSync(path).isDirectory();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/rolldown-plugin-gjsify",
3
- "version": "0.25.1",
3
+ "version": "0.26.1",
4
4
  "description": "Rolldown / Rollup / Vite plugin orchestrator for GJS, Node, and Browser targets",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -55,11 +55,11 @@
55
55
  ],
56
56
  "license": "MIT",
57
57
  "dependencies": {
58
- "@gjsify/console": "^0.25.1",
59
- "@gjsify/resolve-npm": "^0.25.1",
60
- "@gjsify/rolldown-plugin-deepkit": "^0.25.1",
61
- "@gjsify/rolldown-plugin-pnp": "^0.25.1",
62
- "@gjsify/vite-plugin-blueprint": "^0.25.1",
58
+ "@gjsify/console": "^0.26.1",
59
+ "@gjsify/resolve-npm": "^0.26.1",
60
+ "@gjsify/rolldown-plugin-deepkit": "^0.26.1",
61
+ "@gjsify/rolldown-plugin-pnp": "^0.26.1",
62
+ "@gjsify/vite-plugin-blueprint": "^0.26.1",
63
63
  "@rollup/pluginutils": "^5.4.0",
64
64
  "acorn": "^8.17.0",
65
65
  "acorn-walk": "^8.3.5",
@@ -68,7 +68,7 @@
68
68
  "sass": "^1.101.0"
69
69
  },
70
70
  "peerDependencies": {
71
- "@gjsify/lightningcss-native": "^0.25.1",
71
+ "@gjsify/lightningcss-native": "^0.26.1",
72
72
  "rolldown": "^1.1.4"
73
73
  },
74
74
  "peerDependenciesMeta": {