@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.
- package/lib/app/nativescript.js +1 -1
- package/lib/app/node.d.ts +13 -6
- package/lib/app/node.js +27 -17
- package/lib/globals.d.ts +1 -1
- package/lib/globals.js +1 -1
- package/lib/utils/auto-globals.d.ts +17 -6
- package/lib/utils/auto-globals.js +25 -8
- package/lib/utils/detect-free-globals.d.ts +27 -0
- package/lib/utils/detect-free-globals.js +75 -0
- package/lib/utils/entry-points.js +45 -5
- package/lib/utils/inline-static-reads.js +7 -12
- package/package.json +7 -7
package/lib/app/nativescript.js
CHANGED
|
@@ -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
|
|
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`/…)
|
|
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
|
|
50
|
-
* `nodeGiGlobalsInject` detector keys on
|
|
51
|
-
*
|
|
52
|
-
* is
|
|
53
|
-
*
|
|
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`/…)
|
|
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
|
|
112
|
-
* `nodeGiGlobalsInject` detector keys on
|
|
113
|
-
*
|
|
114
|
-
* is
|
|
115
|
-
*
|
|
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`)
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
//
|
|
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`)
|
|
196
|
-
*
|
|
197
|
-
*
|
|
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-
|
|
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
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
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
|
-
|
|
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-
|
|
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
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
*
|
|
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
|
-
|
|
24
|
-
return expanded;
|
|
50
|
+
return await expandPattern(_entryPoints, fullIgnore);
|
|
25
51
|
}
|
|
26
52
|
if (Array.isArray(_entryPoints)) {
|
|
27
|
-
|
|
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
|
|
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 &&
|
|
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:
|
|
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 || !
|
|
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 (!
|
|
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
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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.
|
|
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.
|
|
59
|
-
"@gjsify/resolve-npm": "^0.
|
|
60
|
-
"@gjsify/rolldown-plugin-deepkit": "^0.
|
|
61
|
-
"@gjsify/rolldown-plugin-pnp": "^0.
|
|
62
|
-
"@gjsify/vite-plugin-blueprint": "^0.
|
|
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.
|
|
71
|
+
"@gjsify/lightningcss-native": "^0.26.1",
|
|
72
72
|
"rolldown": "^1.1.4"
|
|
73
73
|
},
|
|
74
74
|
"peerDependenciesMeta": {
|