@gjsify/rolldown-plugin-gjsify 0.36.0 → 0.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,22 +1,16 @@
1
- // Iterative multi-pass build orchestrator for `--globals auto`.
1
+ // Iterative multi-pass build orchestrator for `--globals auto`. The "analyse the
2
+ // bundled output AFTER tree-shaking" property is bundler-agnostic and load-bearing —
3
+ // AGENTS.md § "Tree-shakeability invariants — permanent", plus the rationale block
4
+ // atop `detect-free-globals.ts`.
2
5
  //
3
- // Architecturally identical to the esbuild predecessor — only the inner
4
- // build call swaps `esbuild.build()` for `rolldown()`. The "after
5
- // tree-shaking" analysis property is bundler-agnostic and load-bearing
6
- // per AGENTS.md "Tree-shakeability invariants — permanent". See the
7
- // rationale block at the top of `detect-free-globals.ts`.
6
+ // Pass 1: bundle with no globals injection → parsed by acorn for free globals.
7
+ // Pass 2: bundle with the detected globals injected → the injected register modules
8
+ // pull in MORE code referencing more globals (a tree-shaking dependency
9
+ // cycle), so the set can still grow.
10
+ // Pass N: repeat until it converges, capped at MAX_ITERATIONS.
8
11
  //
9
- // Pass 1: rolldown() with no globals injection
10
- // → in-memory bundle parsed by acorn for free globals
11
- // Pass 2: rolldown() with detected globals injected
12
- // → some injected register modules pull in MORE code that
13
- // references additional globals (tree-shaking dependency cycle)
14
- // Pass N: repeat until the detected set converges (typically 2–3 iterations,
15
- // capped at MAX_ITERATIONS=5)
16
- //
17
- // We deliberately do NOT minify the analysis builds: minification can
18
- // alias `globalThis` to a short variable and defeat MemberExpression
19
- // detection in detect-free-globals.ts.
12
+ // The analysis builds MUST NOT minify: minification aliases `globalThis` to a short
13
+ // variable and defeats MemberExpression detection in detect-free-globals.ts.
20
14
  const defaultBundler = async ({ rolldownInput, format }) => {
21
15
  // Indirect specifier so the GJS bundle doesn't pull npm rolldown in
22
16
  // statically. Only reached when the caller doesn't override (Node).
@@ -46,19 +40,16 @@ const GI_BACKED = GJS_GI_BACKED_REGISTERS;
46
40
  /** Maximum iterations to prevent runaway loops on pathological inputs. */
47
41
  const MAX_ITERATIONS = 5;
48
42
  /**
49
- * Expand a detected-identifier set with the PRECOMPUTED transitive closure of
50
- * the register modules those identifiers inject (the committed
51
- * `register-globals-closure` map, generated by
43
+ * Expand a detected-identifier set with the PRECOMPUTED transitive closure of the
44
+ * register modules those identifiers inject (`register-globals-closure`, generated by
52
45
  * `packages/infra/cli/scripts/generate-register-closure.mjs`).
53
46
  *
54
- * The iterative loop's passes 2..N exist only to discover which ADDITIONAL
55
- * globals the injected register modules' own code references — static,
56
- * per-register information. Seeding pass 2 with the full closure makes the
57
- * loop converge at the very next pass (a verification bundle), collapsing
58
- * ~3-5 analysis bundles to 2 for every `--globals auto` build. A stale or
59
- * missing closure entry is SAFE: the verification pass detects unexpected
60
- * growth and the loop simply continues iterating (slower + a hint to
61
- * regenerate).
47
+ * Passes 2..N of the plain loop exist only to discover which ADDITIONAL globals the
48
+ * injected registers' own code references — static, per-register information. Seeding
49
+ * pass 2 with the full closure makes the loop converge at the next (verification)
50
+ * pass, collapsing ~3-5 analysis bundles to 2. A stale or missing entry is SAFE: the
51
+ * verification pass sees the unexpected growth and the loop keeps iterating, slower,
52
+ * with a hint to regenerate.
62
53
  */
63
54
  function expandWithClosure(detected, excludeSet) {
64
55
  const out = new Set(detected);
@@ -95,16 +86,14 @@ async function applyExcludeGlobals(detected, currentInject, extraRegisterPaths,
95
86
  for (const id of excludeGlobals)
96
87
  detected.delete(id);
97
88
  }
98
- // Final register set that will be injected (detected → register paths,
99
- // plus any --globals auto,<extras>). Computed for BOTH paths so the
100
- // GI-backed diagnostic below fires whether or not excludeGlobals is set.
89
+ // Computed for BOTH paths so the GI-backed diagnostic fires whether or not
90
+ // excludeGlobals is set.
101
91
  const finalPaths = detectedToRegisterPaths(detected);
102
92
  for (const p of extraRegisterPaths)
103
93
  finalPaths.add(p);
104
94
  emitGiBackedDiagnostic(finalPaths, detected, cwd);
105
- // No exclude: `currentInject` already reflects `finalPaths` (built in the
106
- // loop), so reuse it — re-running filterResolvableRegisterPaths here would
107
- // emit its skip warnings a second time.
95
+ // No exclude: `currentInject` already reflects `finalPaths`, so reuse it —
96
+ // re-running filterResolvableRegisterPaths would repeat its skip warnings.
108
97
  if (!excludeGlobals?.length)
109
98
  return { detected, injectPath: currentInject };
110
99
  const filtered = cwd ? filterResolvableRegisterPaths(finalPaths, cwd) : finalPaths;
@@ -134,18 +123,14 @@ function giNamespacesForRegister(registerPath) {
134
123
  return null;
135
124
  }
136
125
  /**
137
- * Build the build-time note for any GI-backed register modules in a final
138
- * inject set, or null when none are GI-backed. Pure (no I/O) so it is
139
- * unit-testable; `emitGiBackedDiagnostic` is the side-effecting wrapper.
140
- *
141
- * `--globals auto` injecting a GI-backed register makes the bundle
142
- * hard-require a GTK/GNOME runtime at load — under a GTK-less host (headless
143
- * type-check container, SSR generator, plain CI) it throws `Typelib … not
144
- * found`. The note names which globals triggered the injection so that
145
- * otherwise-silent runtime crash is traceable at build time.
126
+ * The build-time note for any GI-backed register modules in a final inject set, or
127
+ * null when none are. Pure so it is unit-testable; `emitGiBackedDiagnostic` is the
128
+ * side-effecting wrapper.
146
129
  *
147
- * @param registerPaths the final register paths to be injected
148
- * @param detected the converged detected-global set (for naming trigger refs)
130
+ * Injecting a GI-backed register makes the bundle hard-require a GTK/GNOME runtime at
131
+ * load, so under a GTK-less host (headless type-check container, SSR generator, plain
132
+ * CI) it throws `Typelib … not found`. Naming the triggering globals makes that
133
+ * otherwise-silent runtime crash traceable at build time.
149
134
  */
150
135
  export function describeGiBackedInjection(registerPaths, detected) {
151
136
  const giRegisters = new Set();
@@ -179,11 +164,10 @@ export function describeGiBackedInjection(registerPaths, detected) {
179
164
  /** Emitted at most once per process so multi-pass / multi-build runs stay quiet. */
180
165
  let giNoteEmitted = false;
181
166
  /**
182
- * console.warn the GI-backed-injection note once per process. Pre-filters
183
- * `registerPaths` to the ones that will ACTUALLY be injected — GI-backed AND
184
- * (quietly) resolvable from the project — so the note never mentions a
185
- * register that gets dropped. Uses `isRegisterPathResolvable` (no warning),
186
- * not `filterResolvableRegisterPaths`, to avoid duplicating its skip warnings.
167
+ * Warn the GI-backed-injection note once per process. Pre-filters `registerPaths` to
168
+ * the ones that will ACTUALLY be injected so the note never mentions a dropped
169
+ * register, via `isRegisterPathResolvable` rather than
170
+ * `filterResolvableRegisterPaths` to avoid duplicating that one's skip warnings.
187
171
  */
188
172
  function emitGiBackedDiagnostic(registerPaths, detected, cwd) {
189
173
  if (giNoteEmitted)
@@ -206,21 +190,10 @@ function isFullConfig(v) {
206
190
  return v !== null && typeof v === 'object' && !Array.isArray(v) && 'plugins' in v && 'options' in v;
207
191
  }
208
192
  /**
209
- * Run an iterative Rolldown build (in-memory) with acorn-based global
210
- * detection. Each pass uses the globals discovered by the previous pass,
211
- * stopping once the detected set is stable (fixpoint reached).
212
- *
213
- * Returns the inject stub path that the caller should pass to the
214
- * final (real) build via `pluginOptions.autoGlobalsInject`.
215
- *
216
- * @param analysisOptions Rolldown input options for the in-memory build.
217
- * @param pluginOptions Gjsify plugin options (without `autoGlobalsInject`,
218
- * which this function computes).
219
- * @param gjsifyPluginFactory Factory returning the gjsify plugin instance
220
- * for a given set of plugin options. Provided by the caller to avoid a
221
- * cyclic import between this module and `../plugin.ts`.
222
- * @param verbose Emit per-iteration debug output to console.
223
- * @param options Optional `extraGlobalsList` / `excludeGlobals`.
193
+ * Run the iterative in-memory build with acorn-based global detection, each pass
194
+ * seeded by the previous pass's globals, until the detected set is stable. Returns the
195
+ * inject stub path the caller passes to the final build via
196
+ * `pluginOptions.autoGlobalsInject`.
224
197
  */
225
198
  export async function detectAutoGlobals(analysisOptions, pluginOptions, gjsifyPluginFactory, verbose, options = {}, bundler = defaultBundler) {
226
199
  const extraRegisterPaths = options.extraGlobalsList
@@ -234,9 +207,8 @@ export async function detectAutoGlobals(analysisOptions, pluginOptions, gjsifyPl
234
207
  const resolvableExtra = cwd ? filterResolvableRegisterPaths(extraRegisterPaths, cwd) : extraRegisterPaths;
235
208
  currentInject = (await writeRegisterInjectFile(resolvableExtra, cwd)) ?? undefined;
236
209
  }
237
- // Caller-provided plugins (e.g. PnP relay) survive every pass; the
238
- // gjsify plugin appended last so its hooks run after any custom
239
- // resolvers / loaders.
210
+ // Caller plugins (e.g. the PnP relay) survive every pass; the gjsify plugin is
211
+ // appended last so its hooks run after any custom resolvers / loaders.
240
212
  const callerPlugins = (analysisOptions.plugins ?? []).filter((p) => {
241
213
  const name = p && typeof p === 'object' && 'name' in p ? p.name : undefined;
242
214
  return name !== 'gjsify' && name !== 'gjsify-orchestrator';
@@ -246,13 +218,6 @@ export async function detectAutoGlobals(analysisOptions, pluginOptions, gjsifyPl
246
218
  ...pluginOptions,
247
219
  autoGlobalsInject: currentInject,
248
220
  });
249
- // Two factory shapes are supported: a plain plugin array (legacy)
250
- // or the orchestrator config `{ options, plugins }`. The latter
251
- // gives auto-globals access to the per-app `resolve.conditionNames`
252
- // / `mainFields` / `external` / `treeshake` settings — without
253
- // these, native-rolldown defaults to a different module-resolution
254
- // condition set than npm-rolldown, packages resolve to different
255
- // entries, and the detected global set diverges between engines.
256
221
  let gjsifyInstance;
257
222
  let orchestratorOptions;
258
223
  if (isFullConfig(factoryResult)) {
@@ -262,28 +227,21 @@ export async function detectAutoGlobals(analysisOptions, pluginOptions, gjsifyPl
262
227
  else {
263
228
  gjsifyInstance = factoryResult;
264
229
  }
265
- // The auto-globals inject stub is a side-effect-only ESM file that
266
- // imports `<pkg>/register/<feature>` paths. Rolldown's `transform.inject`
267
- // is the source-AST per-identifier rewrite we MUST NOT use (see
268
- // AGENTS.md "Tree-shakeability invariants"). Instead, when the
269
- // analyser has produced an inject path, append it as an additional
270
- // entry — Rolldown bundles its side effects into the output and the
271
- // detector sees the resulting identifier references.
230
+ // The inject stub is a side-effect-only ESM file importing
231
+ // `<pkg>/register/<feature>` paths, appended as an ADDITIONAL ENTRY so Rolldown
232
+ // bundles its side effects and the detector sees the resulting references.
233
+ // Rolldown's `transform.inject` is the source-AST per-identifier rewrite the
234
+ // tree-shakeability invariants forbid.
272
235
  const inputWithInject = currentInject
273
236
  ? appendInjectAsEntry(analysisOptions.input, currentInject)
274
237
  : analysisOptions.input;
275
- // Merge resolve + external + transform from the orchestrator's
276
- // options so the analysis bundle goes through the same module
277
- // resolution as the final build. Analysis-side overrides
278
- // (`analysisOptions.*`) win when explicitly set.
238
+ // Take resolve/external/transform from the orchestrator so the analysis bundle
239
+ // goes through the same module resolution as the final build; explicit
240
+ // analysis-side overrides win.
279
241
  const mergedResolve = analysisOptions.resolve ?? orchestratorOptions?.resolve;
280
242
  const mergedExternal = analysisOptions.external ?? orchestratorOptions?.external;
281
243
  const mergedTransform = analysisOptions.transform ?? orchestratorOptions?.transform;
282
244
  const orchTreeshake = orchestratorOptions?.treeshake;
283
- // Inline-collect both halves of the plugin chain in their existing
284
- // shape: caller plugins first (PnP, user text-loaders), then the
285
- // gjsify chain (which may be either a single RolldownPluginOption
286
- // or an array of them depending on the factory shape).
287
245
  const gjsifyPluginsArray = Array.isArray(gjsifyInstance) ? gjsifyInstance : [gjsifyInstance];
288
246
  const chunkCodes = await bundler({
289
247
  rolldownInput: {
@@ -300,11 +258,9 @@ export async function detectAutoGlobals(analysisOptions, pluginOptions, gjsifyPl
300
258
  if (chunkCodes.length === 0) {
301
259
  return { detected: new Set(), injectPath: currentInject };
302
260
  }
303
- // Parse each chunk independently and union the detected sets.
304
- // Rolldown emits one chunk per entry — concatenating them would
305
- // produce a syntactically invalid combined program (duplicate
306
- // top-level declarations: `File`, `Buffer`, …) that acorn can't
307
- // parse. Per-chunk parsing keeps each chunk's lexical scope intact.
261
+ // Per-chunk parse, then union: Rolldown emits one chunk per entry, and
262
+ // concatenating them yields duplicate top-level declarations (`File`,
263
+ // `Buffer`, …) that acorn rejects as an invalid program.
308
264
  const newDetected = new Set();
309
265
  for (let i = 0; i < chunkCodes.length; i++) {
310
266
  const code = chunkCodes[i] ?? '';
@@ -315,49 +271,40 @@ export async function detectAutoGlobals(analysisOptions, pluginOptions, gjsifyPl
315
271
  catch (e) {
316
272
  if (globalThis.process?.env
317
273
  ?.GJSIFY_DEBUG_AUTO_GLOBALS) {
318
- // `tmpdir()`, not a literal `/tmp` — on Windows that wrote
319
- // to `<drive>:\tmp\`, and the `writeFileSync` is inside a
320
- // `catch`, so the dump silently did not appear where the
321
- // log line below says it did.
274
+ // `tmpdir()`, not a literal `/tmp`: on Windows that wrote to
275
+ // `<drive>:\tmp\`, and since the `writeFileSync` sits inside a
276
+ // `catch` the dump silently did not appear where the log line
277
+ // below says it did.
322
278
  const os = await import('node:os');
323
279
  const nodePath = await import('node:path');
324
280
  const path = nodePath.join(os.tmpdir(), `gjsify-auto-globals-failed-chunk-${i}.mjs`);
325
281
  try {
326
- // Dynamic `import()` on purpose: this is a debug-only
327
- // branch and the module graph should not carry
328
- // `node:fs` for it. (It is NOT a `require` — it never
329
- // was; the rule-disable comment that used to sit here
330
- // named a rule that cannot apply to an import
331
- // expression.)
282
+ // Dynamic `import()` so this debug-only branch keeps `node:fs`
283
+ // out of the module graph.
332
284
  const fs = await import('node:fs');
333
285
  fs.writeFileSync(path, code);
334
286
  console.error(`[gjsify-auto-globals] parse failed on chunk #${i} — wrote ${path} for inspection`);
335
287
  }
336
288
  catch {
337
- // Deliberately swallowed, and the ONLY reason this
338
- // catch is legitimate: writing the debug dump is
339
- // best-effort (permissions, disk), and letting its
340
- // failure escape would replace the real parse error
341
- // `e` — rethrown below — with a misleading one.
289
+ // Swallowed on purpose: writing the debug dump is best-effort
290
+ // (permissions, disk), and letting its failure escape would
291
+ // replace the real parse error `e`, rethrown below.
342
292
  }
343
293
  }
344
294
  throw e;
345
295
  }
346
296
  }
347
- // Apply excludeGlobals BEFORE writing the next iteration's inject file.
348
- // Otherwise an excluded identifier would still appear in the inject
349
- // import list and the analysis build itself would fail when the
350
- // corresponding `@gjsify/<pkg>/register/<feature>` is not in the
351
- // project's resolvable dep tree.
297
+ // Before writing the next iteration's inject file: otherwise an excluded
298
+ // identifier still lands in the inject import list and the analysis build fails
299
+ // when its `@gjsify/<pkg>/register/<feature>` is not in the project's dep tree.
352
300
  if (excludeSet.size > 0) {
353
301
  for (const id of excludeSet)
354
302
  newDetected.delete(id);
355
303
  }
356
- // Fixpoint check: detection is monotonic — injecting registers can
357
- // only ADD references. The injected set (`detected`) may be a
358
- // SUPERSET of what the bundle actually references (the closure-map
359
- // expansion below deliberately over-approximates), so convergence is
360
- // "this pass found nothing OUTSIDE the injected set", not equality.
304
+ // Fixpoint check: detection is monotonic (injecting registers can only ADD
305
+ // references) and the closure expansion below deliberately over-approximates, so
306
+ // convergence is "this pass found nothing OUTSIDE the injected set", not
307
+ // equality.
361
308
  if (isSubset(newDetected, detected)) {
362
309
  if (verbose) {
363
310
  const sorted = [...detected].sort();
@@ -366,21 +313,17 @@ export async function detectAutoGlobals(analysisOptions, pluginOptions, gjsifyPl
366
313
  }
367
314
  return applyExcludeGlobals(detected, currentInject, extraRegisterPaths, options.excludeGlobals, cwd);
368
315
  }
369
- // Seed the next pass with the PRECOMPUTED closure of every detected
370
- // identifier's register module — the static answer to what passes
371
- // 2..N of the plain loop would re-discover one bundle at a time.
372
- // Union with the previously-injected set keeps growth monotonic
373
- // (an over-approximated superset must never shrink, or the subset
374
- // convergence check could oscillate).
316
+ // Seed the next pass with the precomputed closure. Unioning with the
317
+ // previously-injected set keeps growth monotonic — an over-approximated superset
318
+ // that shrank could make the subset convergence check oscillate.
375
319
  const expanded = options.disableClosureExpansion ? newDetected : expandWithClosure(newDetected, excludeSet);
376
320
  if (verbose && expanded.size > newDetected.size) {
377
321
  console.debug(`[gjsify] --globals auto: closure map expanded ${newDetected.size} → ${expanded.size} global(s)`);
378
322
  }
379
323
  if (iteration > 1 && verbose) {
380
- // Reaching a growth pass after the closure-seeded pass means the
381
- // committed closure map no longer covers a register module's
382
- // references — correctness is preserved by iterating, but builds
383
- // pay extra passes until the map is regenerated.
324
+ // A growth pass after the closure-seeded pass means the committed map no
325
+ // longer covers a register's references: iterating keeps it correct, but
326
+ // builds pay extra passes until the map is regenerated.
384
327
  console.debug('[gjsify] --globals auto: detection grew past the closure-map expansion — the committed ' +
385
328
  'register-globals-closure map looks stale. Regenerate via ' +
386
329
  '`node packages/infra/cli/scripts/generate-register-closure.mjs`.');
@@ -389,11 +332,9 @@ export async function detectAutoGlobals(analysisOptions, pluginOptions, gjsifyPl
389
332
  let registerPaths = detectedToRegisterPaths(detected);
390
333
  for (const p of extraRegisterPaths)
391
334
  registerPaths.add(p);
392
- // Filter out register paths whose polyfill package is not installed
393
- // in the project. An unresolvable import in the analysis bundle causes
394
- // a hard Rolldown error — emitting it is strictly worse than skipping
395
- // with a warning. The caller can add the dep or use --exclude-globals
396
- // to acknowledge the gap. See AGENTS.md fix (a) rationale.
335
+ // Drop register paths whose polyfill package is not installed: an unresolvable
336
+ // import is a HARD Rolldown error, strictly worse than skipping with a warning.
337
+ // The caller adds the dep or acknowledges the gap via --exclude-globals.
397
338
  if (cwd)
398
339
  registerPaths = filterResolvableRegisterPaths(registerPaths, cwd);
399
340
  if (registerPaths.size === 0) {
@@ -420,24 +361,21 @@ export async function detectAutoGlobals(analysisOptions, pluginOptions, gjsifyPl
420
361
  * 1. a GJS ambient global (`print`/`printerr`/`log`/`logError`/`ARGV`/
421
362
  * `imports`) referenced as a BARE identifier — the legacy GJS-source shape
422
363
  * `@gjsify/node-gi/globals` seeds (`detectGjsAmbientGlobals`);
423
- * 2. a STATIC import of `@gjsify/node-gi` (root or subpath) — the shape the
424
- * bare GJS built-ins (`system`/`gettext`/`cairo`) are externalised to, i.e.
425
- * the PORTABLE GJS-source spelling (`detectNodeGiModuleImports`). A bundle
426
- * retaining one is already bound to the bridge at load, so flipping
427
- * injection cannot cost plain-Node loadability — while a portable GJS app
428
- * with no ambient global at all (e.g. `system.programArgs` instead of
429
- * `ARGV`) would otherwise get its `@girs/*` bodies emptied and its globals
430
- * shim dropped (the `gjsify storybook --runtime node` no-`requireGi`
431
- * regression).
364
+ * 2. a STATIC import of `@gjsify/node-gi` (root or subpath) — the shape the bare GJS
365
+ * built-ins are externalised to, i.e. the PORTABLE GJS-source spelling
366
+ * (`detectNodeGiModuleImports`). A bundle retaining one is already bound to the
367
+ * bridge at load, so flipping injection cannot cost plain-Node loadability, while
368
+ * a portable GJS app with no ambient global at all (`system.programArgs` instead
369
+ * of `ARGV`) would otherwise get its `@girs/*` bodies emptied and its globals shim
370
+ * dropped (the `gjsify storybook --runtime node` no-`requireGi` regression).
432
371
  *
433
- * A SINGLE pass suffices (unlike the GJS register-injection loop): the shim is
434
- * EXTERNAL, so injecting it adds no bundled code that could reference more
435
- * globals — there is no tree-shaking dependency cycle to converge.
372
+ * A SINGLE pass suffices, unlike the GJS register-injection loop: the shim is EXTERNAL,
373
+ * so injecting it adds no bundled code that could reference more globals — no
374
+ * tree-shaking dependency cycle to converge.
436
375
  *
437
- * Critically, detection is on the bundled output AFTER tree-shaking, so a
438
- * `print`/`imports` reference behind statically-dead code does not trigger
439
- * injection — importing the shim eagerly loads the native node-gi addon, which
440
- * crashes on plain Node without node-gi. See `detectGjsAmbientGlobals`.
376
+ * Detection is on the bundled output AFTER tree-shaking, so a `print`/`imports`
377
+ * reference behind statically-dead code does not trigger injection — importing the shim
378
+ * eagerly loads the native node-gi addon, which crashes on plain Node without node-gi.
441
379
  */
442
380
  export async function detectNodeGiGlobals(analysisOptions, pluginOptions, gjsifyPluginFactory, bundler = defaultBundler) {
443
381
  const factoryResult = await gjsifyPluginFactory(pluginOptions);
@@ -455,17 +393,12 @@ export async function detectNodeGiGlobals(analysisOptions, pluginOptions, gjsify
455
393
  return name !== 'gjsify' && name !== 'gjsify-orchestrator';
456
394
  });
457
395
  const gjsifyPluginsArray = Array.isArray(gjsifyInstance) ? gjsifyInstance : [gjsifyInstance];
458
- // Reuse the FULL orchestrator options as the base so the analysis bundle
459
- // goes through the EXACT SAME module resolution as the real `--app node`
460
- // build — most importantly `platform: 'node'`, without which Rolldown does
461
- // not treat `node:*` as builtins and the @gjsify/rolldown-plugin-pnp
462
- // resolver rejects `node:fs` in a non-Node context (`RESOLVE_ERROR`). It
463
- // also carries `external` (the node externals incl. `@gjsify/node-gi/*`),
464
- // `resolve` (conditionNames/mainFields) and `treeshake`. We strip only the
465
- // disk-write `output` (the in-memory `generate({format})` handles output)
466
- // and override `input`/`plugins`. Analysis-side overrides win when set.
467
- // Mirrors how `detectAutoGlobals` reuses the orchestrator config, with the
468
- // platform/external/resolve carried verbatim instead of cherry-picked.
396
+ // The FULL orchestrator options as the base (only `output` stripped, `input`/
397
+ // `plugins` overridden) so the analysis bundle goes through the EXACT SAME module
398
+ // resolution as the real `--app node` build. Most importantly `platform: 'node'`:
399
+ // without it Rolldown does not treat `node:*` as builtins and
400
+ // `@gjsify/rolldown-plugin-pnp` rejects `node:fs` with a `RESOLVE_ERROR`. Carried
401
+ // verbatim rather than cherry-picked, unlike `detectAutoGlobals`.
469
402
  const baseOptions = orchestratorOptions ? { ...orchestratorOptions } : {};
470
403
  delete baseOptions.output;
471
404
  let chunkCodes;
@@ -484,11 +417,9 @@ export async function detectNodeGiGlobals(analysisOptions, pluginOptions, gjsify
484
417
  });
485
418
  }
486
419
  catch (e) {
487
- // Fail SAFE: a detection-pass failure must NEVER break a build that
488
- // would otherwise succeed. Fall back to NOT injecting the shim — worst
489
- // case is a missing global (a clear `ReferenceError: print is not
490
- // defined` at runtime, recoverable via `--globals` or an explicit
491
- // `import '@gjsify/node-gi/globals'`), never a hard build failure.
420
+ // Fail SAFE: a detection-pass failure must never break a build that would
421
+ // otherwise succeed. Not injecting costs at worst a `ReferenceError: print is
422
+ // not defined` at runtime, recoverable via `--globals` or an explicit import.
492
423
  console.warn('[gjsify] --app node: could not analyse the bundle for GJS ambient globals ' +
493
424
  `(${e instanceof Error ? e.message : String(e)}); skipping @gjsify/node-gi/globals ` +
494
425
  'injection. If this build uses print/imports/ARGV, add `import ' +
@@ -504,10 +435,8 @@ export async function detectNodeGiGlobals(analysisOptions, pluginOptions, gjsify
504
435
  return false;
505
436
  }
506
437
  /**
507
- * Append an additional entry path to a Rolldown `input` value while
508
- * preserving its shape (string → array, array → array, record → record).
509
- * The new entry is given the synthetic name `__gjsify_inject` when the
510
- * record form is used so it doesn't collide with user-named outputs.
438
+ * Append an entry path to a Rolldown `input` value, preserving its shape. In the record
439
+ * form the synthetic name `__gjsify_inject` avoids colliding with user-named outputs.
511
440
  */
512
441
  function appendInjectAsEntry(input, injectPath) {
513
442
  if (input === undefined)
@@ -1,62 +1,43 @@
1
1
  /**
2
- * Parse bundled JS output and return the set of free (unbound) identifiers that
3
- * match the known web/Node GJS globals from `GJS_GLOBALS_MAP`. Drives the
4
- * `--app gjs --globals auto` injection. See `scanFreeGlobals` for the rules.
2
+ * Free identifiers in bundled output that match the known web/Node globals in
3
+ * `GJS_GLOBALS_MAP` — drives `--app gjs --globals auto`. Rules: `scanFreeGlobals`.
5
4
  */
6
5
  export declare function detectFreeGlobals(code: string): Set<string>;
7
6
  /**
8
- * The GJS ambient globals that exist implicitly under gjs but not on Node:
9
- * `print`/`printerr`/`log`/`logError`, the `ARGV` array, and the legacy
10
- * `imports` object (`imports.gi.Gtk`, `imports.system`, `imports.gettext`).
11
- * `@gjsify/node-gi/globals` seeds these on `globalThis` when imported.
12
- *
13
- * This is the REVERSE of `GJS_GLOBALS_MAP` (which maps Node/Web globals onto
14
- * GJS): it's the set the `--app node` target injects the node-gi globals shim
15
- * for. Kept here next to the detector so the two never drift.
7
+ * The globals that exist implicitly under gjs but not on Node, seeded on `globalThis` by
8
+ * `@gjsify/node-gi/globals`. The REVERSE of `GJS_GLOBALS_MAP`: the set whose presence
9
+ * makes `--app node` inject that shim. Kept beside the detector so the two never drift.
16
10
  */
17
11
  export declare const GJS_AMBIENT_GLOBALS: ReadonlySet<string>;
18
12
  /**
19
- * Detect references to the GJS ambient globals (`GJS_AMBIENT_GLOBALS`) in a
20
- * bundled `--app node` chunk — the signal that the bundle needs
21
- * `@gjsify/node-gi/globals` injected.
13
+ * References to `GJS_AMBIENT_GLOBALS` in a bundled `--app node` chunk — the signal that
14
+ * the bundle needs `@gjsify/node-gi/globals`.
22
15
  *
23
- * Runs on the bundled output AFTER tree-shaking (same property the GJS path
24
- * relies on), so a `print`/`imports` reference behind a statically-dead branch
25
- * does not trigger injection. Detects only BARE free identifiers — the genuine
26
- * GJS-source shape (`print(...)`, `imports.gi.Gtk`). The `globalThis.imports` /
27
- * `globalThis.print` host-member form is the cross-platform isomorphic-GUARD
28
- * shape (`if (typeof globalThis.imports !== 'undefined') { … }`) and is
29
- * deliberately ignored: injecting `@gjsify/node-gi/globals` for it would break
30
- * plain-Node loadability of a package that merely gates GJS usage (the bare
31
- * external import is unresolvable without node-gi; the shim eagerly loads the
32
- * native addon). Pure `typeof imports` probes are deferred too. See
33
- * `ignoreHostMembers`.
16
+ * Runs on the output AFTER tree-shaking, so a reference behind a statically-dead branch
17
+ * does not trigger injection, and matches only BARE identifiers (`print(...)`,
18
+ * `imports.gi.Gtk`) — see `ignoreHostMembers` for why the host-member form is ignored.
34
19
  */
35
20
  export declare function detectGjsAmbientGlobals(code: string): Set<string>;
36
21
  /**
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`.
22
+ * A STATIC import of `@gjsify/node-gi` (root or subpath) in a bundled `--app node`
23
+ * chunk — the second genuine-GJS-source signal beside `detectGjsAmbientGlobals`.
40
24
  *
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.
25
+ * It is a signal because the bare GJS built-ins are rewritten to
26
+ * `@gjsify/node-gi/<name>` and kept EXTERNAL as top-level imports: a tree-shaken graph
27
+ * that retains one is ALREADY bound to the bridge at load (Node resolves the external
28
+ * at link time and fails without node-gi), so treating it as a GJS-source build cannot
29
+ * cost plain-Node loadability. NOT treating it as one is how a portable GJS app
30
+ * spelling `ARGV` as `system.programArgs` lost its `@girs/*` bodies and its globals
31
+ * shim in one go — `gjsify storybook --runtime node` emitted a bundle with no
32
+ * `requireGi` at all.
51
33
  *
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.
34
+ * A surviving `gi://` import is deliberately NOT a signal: its shim loads node-gi LAZILY
35
+ * through a `require('@gjsify/node-gi/gi')` CALL, so this scan is structurally blind to
36
+ * it by design. A cross-platform package with a gjs-gated `gi://` import stays loadable
37
+ * on plain Node (#641, pinned by `tests/e2e/node-gi-globals-inject`), and flipping
38
+ * injection for it would break exactly that.
58
39
  *
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.
40
+ * Scans import/re-export STATEMENTS via the AST, never text, so a quoted specifier in a
41
+ * test name or template cannot trigger.
61
42
  */
62
43
  export declare function detectNodeGiModuleImports(code: string): boolean;