@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,107 +1,73 @@
1
1
  // Detect free (unbound) global identifiers in bundled JS output.
2
2
  //
3
- // Used by the `--globals auto` two-pass build: the first esbuild pass
4
- // produces a minified bundle without globals injection, this module
5
- // parses it with acorn and finds references to known GJS globals that
6
- // are not locally declared. The result feeds the second pass's inject
7
- // stub so only actually-needed globals are registered.
3
+ // Drives the `--globals auto` iterative build (`auto-globals.ts`): each analysis pass
4
+ // bundles UNMINIFIED and without injection, this module parses that output with acorn
5
+ // and finds references to known GJS globals that are not locally declared, and the
6
+ // result feeds the next pass's inject stub so only actually-needed globals are
7
+ // registered. Minification is forbidden in those passes because it aliases
8
+ // `globalThis` to a short variable and defeats the MemberExpression patterns below.
8
9
  import * as acorn from 'acorn';
9
10
  import * as walk from 'acorn-walk';
10
11
  import { GJS_GLOBALS_MAP } from '@gjsify/resolve-npm/globals-map';
11
12
  const KNOWN_GLOBALS = new Set(Object.keys(GJS_GLOBALS_MAP));
12
13
  /**
13
- * Method markers — `<host>.<method>(…)` patterns that imply a global
14
- * identifier should be injected even though the identifier itself never
15
- * appears in the bundle.
14
+ * `<host>.<method>` patterns that must inject a global even though the identifier
15
+ * itself never appears in the bundle: a project calling `navigator.getGamepads()`
16
+ * references no gamepad identifier, yet still needs `@gjsify/gamepad/register` to
17
+ * patch the method onto `navigator`.
16
18
  *
17
- * Example: a project that calls `navigator.getGamepads()` doesn't reference
18
- * any of the gamepad-related identifiers in the globals map, but it still
19
- * needs `@gjsify/gamepad/register` to patch `navigator` with the method.
20
- * This marker maps `navigator.getGamepads` → inject the `GamepadEvent`
21
- * register path (which is the gamepad package's register entry).
22
- *
23
- * Keyed by `host.method` (lowercase host, exact method name). Values are
24
- * KNOWN_GLOBALS identifiers — the detector adds them as free globals if
25
- * the corresponding member expression is found in the bundle.
19
+ * Keyed `host.method` (exact names); values are KNOWN_GLOBALS identifiers the detector
20
+ * adds as free globals when the member expression is found.
26
21
  */
27
22
  const METHOD_MARKERS = {
28
- // Gamepad API — navigator.getGamepads is patched on by @gjsify/gamepad/register
23
+ // Patched on by @gjsify/gamepad/register
29
24
  'navigator.getGamepads': 'GamepadEvent',
30
- // WebRTC — navigator.mediaDevices is patched on by @gjsify/webrtc/register/media-devices
25
+ // Patched on by @gjsify/webrtc/register/media-devices
31
26
  'navigator.mediaDevices': 'MediaDevices',
32
- // WebAssembly Promise APIs — the runtime stubs throw at first call, so
33
- // any reference to these methods needs the `@gjsify/webassembly` polyfill.
34
- // The register entry replaces the stubs with wrappers around the working
35
- // synchronous `new WebAssembly.{Module,Instance}` constructors.
27
+ // The runtime stubs throw at first call, so any reference needs the
28
+ // `@gjsify/webassembly` polyfill, whose register replaces them with wrappers around
29
+ // the working synchronous `new WebAssembly.{Module,Instance}` constructors.
36
30
  'WebAssembly.compile': 'WebAssembly',
37
31
  'WebAssembly.compileStreaming': 'WebAssembly',
38
32
  'WebAssembly.instantiate': 'WebAssembly',
39
33
  'WebAssembly.instantiateStreaming': 'WebAssembly',
40
34
  'WebAssembly.validate': 'WebAssembly',
41
- // Note: URL.createObjectURL / URL.revokeObjectURL don't need markers —
42
- // they are first-class static methods on @gjsify/url's URL class, so the
43
- // free `URL` identifier (detected directly, maps to
44
- // @gjsify/node-globals/register/url in GJS_GLOBALS_MAP) already pulls in
45
- // the correct register module.
35
+ // `URL.createObjectURL`/`revokeObjectURL` need no marker: they are first-class
36
+ // statics on @gjsify/url's URL class, so the directly-detected free `URL` already
37
+ // pulls the right register module.
46
38
  };
47
39
  /**
48
- * wasm-bindgen-generated function-name patterns that imply a global
49
- * identifier should be injected. wasm-bindgen emits its host-API
50
- * import bindings as top-level functions named
51
- * `__wbg_<jsName>_<hash>` where `<jsName>` is the property name of the
52
- * JS API the WASM module wants to call. The body looks like:
53
- *
54
- * function __wbg_crypto_574e78ad8b13b65f(arg0) {
55
- * const ret = getObject(arg0).crypto;
56
- * return addHeapObject(ret);
57
- * }
58
- *
59
- * `getObject(arg0)` is a runtime heap dereference (the object is one
60
- * of the host bridges registered by wasm-bindgen at init time —
61
- * typically `globalThis`, `window`, or `self`). The MemberExpression
62
- * visitor can't follow that — `node.object` is a CallExpression, not
63
- * an Identifier — so the underlying `.crypto` access is invisible to
64
- * the static scan.
40
+ * wasm-bindgen function-name patterns that must inject a global. wasm-bindgen emits its
41
+ * host-API import bindings as top-level `__wbg_<jsName>_<hash>` functions whose body is
42
+ * `getObject(arg0).<jsName>` — a runtime heap dereference the MemberExpression visitor
43
+ * cannot follow (`node.object` is a CallExpression), so the underlying property access
44
+ * is invisible to the static scan.
65
45
  *
66
- * Matching on the FUNCTION-NAME pattern instead is high precision
67
- * (no false positives — `__wbg_` is wasm-bindgen-reserved) and high
68
- * recall (the names are extremely stable across wasm-bindgen
69
- * versions). Add an entry whenever a new wasm-bindgen-built npm
70
- * package surfaces a needed global that the static scan misses.
71
- *
72
- * Keyed by the `<jsName>` extracted from the `__wbg_<jsName>_<hash>`
73
- * function name; value is the gjsify global to inject.
46
+ * Matching the FUNCTION NAME instead is high precision (`__wbg_` is
47
+ * wasm-bindgen-reserved) and high recall (the names are stable across versions). Add an
48
+ * entry whenever a wasm-bindgen-built package needs a global the scan misses. Keyed by
49
+ * `<jsName>`; value is the gjsify global to inject.
74
50
  */
75
51
  const WASM_BINDGEN_MARKERS = {
76
- // crypto.getRandomValues chain — wasm-bindgen's canonical
77
- // crypto-import binding pattern. Used by ed25519-dalek, ring, rand
78
- // (when targeting wasm), and most Rust crates that touch
79
- // randomness or hashing. Loro is the driving real-world consumer:
80
- // its CRDT operations need crypto.getRandomValues for peer-id
81
- // generation and ChangeID nonces.
52
+ // wasm-bindgen's canonical crypto-import binding chain — ed25519-dalek, ring, rand
53
+ // and most Rust crates touching randomness or hashing. Loro drives it: its CRDT ops
54
+ // need crypto.getRandomValues for peer ids and ChangeID nonces.
82
55
  crypto: 'crypto',
83
56
  getRandomValues: 'crypto',
84
- // Legacy IE prefix path — wasm-bindgen probes for `msCrypto` as a
85
- // fallback when `crypto` isn't available. Doesn't apply to GJS
86
- // (we ship a real `crypto`), but flagging the marker keeps the
87
- // detector self-documenting.
57
+ // Legacy IE fallback wasm-bindgen probes when `crypto` is absent. Irrelevant to GJS
58
+ // (we ship a real `crypto`); mapped so the marker set is self-documenting.
88
59
  msCrypto: 'crypto',
89
60
  };
90
61
  /**
91
- * Match the wasm-bindgen function-name shape — return the `<jsName>`
92
- * part of `__wbg_<jsName>_<hash>` or `null`. wasm-bindgen hashes are
93
- * 8–16 hex chars in practice, but we accept any alphanumeric trailer
94
- * so the pattern survives future format tweaks.
62
+ * The `<jsName>` part of `__wbg_<jsName>_<hash>`, or null. Hashes are 8–16 hex chars in
63
+ * practice; any alphanumeric trailer is accepted so the pattern survives format tweaks.
95
64
  */
96
65
  const WBG_NAME_RE = /^__wbg_([A-Za-z][A-Za-z0-9]*)_[A-Za-z0-9]+$/;
97
66
  function wbgJsNameFor(fnName) {
98
67
  const match = fnName.match(WBG_NAME_RE);
99
68
  return match?.[1] ?? null;
100
69
  }
101
- /**
102
- * Extract all bound names from a binding pattern
103
- * (Identifier, ObjectPattern, ArrayPattern, AssignmentPattern, RestElement).
104
- */
70
+ /** Every name bound by a binding pattern. */
105
71
  function extractBindingNames(node) {
106
72
  if (!node)
107
73
  return [];
@@ -123,10 +89,20 @@ function extractBindingNames(node) {
123
89
  }
124
90
  }
125
91
  /**
126
- * Core scanner shared by `detectFreeGlobals` (web/Node globals map) and
127
- * `detectGjsAmbientGlobals` (the `--app node` GJS-ambient set). Parametrised so
128
- * the GJS path stays byte-identical while the node path can use a different
129
- * known-globals set + the stricter host-member typeof handling.
92
+ * Core scanner behind `detectFreeGlobals` (web/Node globals map) and
93
+ * `detectGjsAmbientGlobals` (the `--app node` GJS-ambient set), parametrised so the node
94
+ * path can use a different known-globals set and the stricter host-member handling.
95
+ *
96
+ * "Free" = referenced but never declared in the module
97
+ * (var/let/const/function/class/import/param/catch).
98
+ *
99
+ * A `typeof X` presence-check guard (`typeof document !== 'undefined'`) that is the SOLE
100
+ * occurrence of `X` does NOT count as a use. Such guards are pervasive in isomorphic npm
101
+ * packages; on GJS the guarded branch is dead, but a bundler cannot DCE a `typeof`
102
+ * expression since its value depends on the runtime. Injecting for one of those can hard-
103
+ * error the analysis bundle when the polyfill package is not installed. If `X` also
104
+ * appears in a genuine reference position the injection still fires; the resolvability
105
+ * gate in `auto-globals.ts` covers the remaining case.
130
106
  */
131
107
  function scanFreeGlobals(code, opts) {
132
108
  const knownGlobals = opts.knownGlobals;
@@ -135,14 +111,11 @@ function scanFreeGlobals(code, opts) {
135
111
  const ast = acorn.parse(code, {
136
112
  ecmaVersion: 'latest',
137
113
  sourceType: 'module',
138
- // Some bundled chunks carry an embedded `#!shebang` line —
139
- // notably any project bundling its own CLI gets the
140
- // `#!/usr/bin/env -S gjs -m` shebang hoisted to byte 0.
141
- // Acorn rejects shebangs by default; allow them so the
142
- // free-globals analyzer doesn't choke on its own input.
114
+ // Acorn rejects shebangs by default, and any project bundling its own CLI gets
115
+ // one hoisted to byte 0.
143
116
  allowHashBang: true,
144
117
  });
145
- // --- Pass 1: collect all declared names across the entire module ---
118
+ // Pass 1: every declared name in the module.
146
119
  const declaredNames = new Set();
147
120
  walk.simple(ast, {
148
121
  VariableDeclarator(node) {
@@ -196,24 +169,17 @@ function scanFreeGlobals(code, opts) {
196
169
  }
197
170
  },
198
171
  });
199
- // --- Pass 2: find Identifier nodes in reference position ---
200
- // Also detects MemberExpressions like `globalThis.X` / `global.X` /
201
- // `window.X` / `self.X` where X is a known global. esbuild's `define`
202
- // config replaces `global`/`window` with `globalThis`, but we accept
203
- // all four host-object names for safety (esbuild also never renames
204
- // these because they are language keywords / pre-defined globals).
172
+ // Pass 2: Identifier nodes in reference position, plus `host.X` MemberExpressions
173
+ // where X is a known global. `transform.define` already maps `global`/`window` to
174
+ // `globalThis`, but every host-object name is accepted for safety.
205
175
  const freeGlobals = new Set();
206
- // Globals seen only inside `typeof X` expressions so far. Moved into
207
- // freeGlobals the moment the same name is detected in a real-use
208
- // position. Names that remain here at the end were only ever
209
- // presence-checked — not genuinely used — and are excluded from the
210
- // injection set (see JSDoc above).
176
+ // Globals seen ONLY inside `typeof X` so far; promoted to freeGlobals the moment the
177
+ // name appears in a real-use position, and excluded from injection if it never does.
211
178
  const typeofGuardGlobals = new Set();
212
179
  const HOST_OBJECTS = new Set(['globalThis', 'global', 'window', 'self', 'globalObject']);
213
180
  walk.ancestor(ast, {
214
181
  MemberExpression(node) {
215
- // Only dot-access — skip computed (bracket) access since the
216
- // property is then a dynamic Expression, not a known name.
182
+ // Dot-access only: a computed property is a dynamic Expression, not a name.
217
183
  if (node.computed)
218
184
  return;
219
185
  if (node.object.type !== 'Identifier')
@@ -222,12 +188,8 @@ function scanFreeGlobals(code, opts) {
222
188
  return;
223
189
  const objName = node.object.name;
224
190
  const propName = node.property.name;
225
- // Pattern A: globalThis.X / global.X / window.X / self.X
226
- // The property is a known global identifier itself.
191
+ // Pattern A: `<host>.X` where the property is itself a known global.
227
192
  if (HOST_OBJECTS.has(objName)) {
228
- // The node GJS-ambient detector ignores host-member forms — they
229
- // are the isomorphic-guard shape, not genuine GJS source (see
230
- // `ignoreHostMembers`). Bare-identifier detection still applies.
231
193
  if (opts.ignoreHostMembers)
232
194
  return;
233
195
  if (knownGlobals.has(propName)) {
@@ -235,10 +197,7 @@ function scanFreeGlobals(code, opts) {
235
197
  }
236
198
  return;
237
199
  }
238
- // Pattern B: known-instance method markers like
239
- // `navigator.getGamepads` → marker map forwards to a global
240
- // identifier that triggers the right register path even though
241
- // the identifier itself never appears in the bundle.
200
+ // Pattern B: METHOD_MARKERS.
242
201
  const markerKey = `${objName}.${propName}`;
243
202
  const markerTarget = methodMarkers[markerKey];
244
203
  if (markerTarget && knownGlobals.has(markerTarget)) {
@@ -246,13 +205,8 @@ function scanFreeGlobals(code, opts) {
246
205
  }
247
206
  },
248
207
  FunctionDeclaration(node) {
249
- // Pattern C: wasm-bindgen marker hook. The function name
250
- // matches `__wbg_<jsName>_<hash>` and `<jsName>` is a known
251
- // host API. The body would be `getObject(arg0).<jsName>` —
252
- // an unfollowable runtime heap dereference — but the
253
- // function NAME tells us exactly which global is needed.
254
- // See WASM_BINDGEN_MARKERS for the why + which jsNames are
255
- // mapped.
208
+ // Pattern C: WASM_BINDGEN_MARKERS, keyed on the function NAME because the
209
+ // body's property access is an unfollowable heap dereference.
256
210
  if (!node.id)
257
211
  return;
258
212
  const jsName = wbgJsNameFor(node.id.name);
@@ -265,14 +219,11 @@ function scanFreeGlobals(code, opts) {
265
219
  },
266
220
  Identifier(node, ancestors) {
267
221
  const name = node.name;
268
- // Quick filter: only check known globals
269
222
  if (!knownGlobals.has(name))
270
223
  return;
271
- // Skip if locally declared
272
224
  if (declaredNames.has(name))
273
225
  return;
274
- // Determine if this Identifier is in a reference position
275
- // by checking the parent node.
226
+ // Reference position is decided from the parent node.
276
227
  const parent = ancestors[ancestors.length - 2];
277
228
  if (!parent) {
278
229
  freeGlobals.add(name);
@@ -293,7 +244,6 @@ function scanFreeGlobals(code, opts) {
293
244
  return;
294
245
  break;
295
246
  }
296
- // Method/property definitions in classes
297
247
  case 'MethodDefinition':
298
248
  case 'PropertyDefinition': {
299
249
  const def = parent;
@@ -315,8 +265,7 @@ function scanFreeGlobals(code, opts) {
315
265
  return;
316
266
  break;
317
267
  }
318
- // Declaration ids (function name, class name, variable id)
319
- // are already in declaredNames, but guard anyway
268
+ // Declaration ids are already in declaredNames; guarded anyway.
320
269
  case 'FunctionDeclaration':
321
270
  case 'FunctionExpression':
322
271
  case 'ClassDeclaration':
@@ -332,15 +281,11 @@ function scanFreeGlobals(code, opts) {
332
281
  return;
333
282
  break;
334
283
  }
335
- // import { X } / import X — already in declaredNames
336
284
  case 'ImportSpecifier':
337
285
  case 'ImportDefaultSpecifier':
338
286
  case 'ImportNamespaceSpecifier':
339
287
  return;
340
- // `typeof X` — presence-check guard. Defer: record in the
341
- // typeof-guard set and only promote to freeGlobals if the
342
- // same name also appears in a real-use position elsewhere
343
- // in the bundle. See JSDoc on detectFreeGlobals.
288
+ // `typeof X` — presence-check guard, deferred until a real use shows up.
344
289
  case 'UnaryExpression': {
345
290
  const unary = parent;
346
291
  if (unary.operator === 'typeof') {
@@ -352,8 +297,7 @@ function scanFreeGlobals(code, opts) {
352
297
  break;
353
298
  }
354
299
  }
355
- // Real-use position. Promote from typeofGuardGlobals if
356
- // this name was previously seen only as a typeof argument.
300
+ // Real use — promote out of the typeof-guard set.
357
301
  typeofGuardGlobals.delete(name);
358
302
  freeGlobals.add(name);
359
303
  },
@@ -361,9 +305,8 @@ function scanFreeGlobals(code, opts) {
361
305
  return freeGlobals;
362
306
  }
363
307
  /**
364
- * Parse bundled JS output and return the set of free (unbound) identifiers that
365
- * match the known web/Node GJS globals from `GJS_GLOBALS_MAP`. Drives the
366
- * `--app gjs --globals auto` injection. See `scanFreeGlobals` for the rules.
308
+ * Free identifiers in bundled output that match the known web/Node globals in
309
+ * `GJS_GLOBALS_MAP` — drives `--app gjs --globals auto`. Rules: `scanFreeGlobals`.
367
310
  */
368
311
  export function detectFreeGlobals(code) {
369
312
  return scanFreeGlobals(code, {
@@ -373,14 +316,9 @@ export function detectFreeGlobals(code) {
373
316
  });
374
317
  }
375
318
  /**
376
- * The GJS ambient globals that exist implicitly under gjs but not on Node:
377
- * `print`/`printerr`/`log`/`logError`, the `ARGV` array, and the legacy
378
- * `imports` object (`imports.gi.Gtk`, `imports.system`, `imports.gettext`).
379
- * `@gjsify/node-gi/globals` seeds these on `globalThis` when imported.
380
- *
381
- * This is the REVERSE of `GJS_GLOBALS_MAP` (which maps Node/Web globals onto
382
- * GJS): it's the set the `--app node` target injects the node-gi globals shim
383
- * for. Kept here next to the detector so the two never drift.
319
+ * The globals that exist implicitly under gjs but not on Node, seeded on `globalThis` by
320
+ * `@gjsify/node-gi/globals`. The REVERSE of `GJS_GLOBALS_MAP`: the set whose presence
321
+ * makes `--app node` inject that shim. Kept beside the detector so the two never drift.
384
322
  */
385
323
  export const GJS_AMBIENT_GLOBALS = new Set([
386
324
  'print',
@@ -391,21 +329,12 @@ export const GJS_AMBIENT_GLOBALS = new Set([
391
329
  'imports',
392
330
  ]);
393
331
  /**
394
- * Detect references to the GJS ambient globals (`GJS_AMBIENT_GLOBALS`) in a
395
- * bundled `--app node` chunk — the signal that the bundle needs
396
- * `@gjsify/node-gi/globals` injected.
332
+ * References to `GJS_AMBIENT_GLOBALS` in a bundled `--app node` chunk — the signal that
333
+ * the bundle needs `@gjsify/node-gi/globals`.
397
334
  *
398
- * Runs on the bundled output AFTER tree-shaking (same property the GJS path
399
- * relies on), so a `print`/`imports` reference behind a statically-dead branch
400
- * does not trigger injection. Detects only BARE free identifiers — the genuine
401
- * GJS-source shape (`print(...)`, `imports.gi.Gtk`). The `globalThis.imports` /
402
- * `globalThis.print` host-member form is the cross-platform isomorphic-GUARD
403
- * shape (`if (typeof globalThis.imports !== 'undefined') { … }`) and is
404
- * deliberately ignored: injecting `@gjsify/node-gi/globals` for it would break
405
- * plain-Node loadability of a package that merely gates GJS usage (the bare
406
- * external import is unresolvable without node-gi; the shim eagerly loads the
407
- * native addon). Pure `typeof imports` probes are deferred too. See
408
- * `ignoreHostMembers`.
335
+ * Runs on the output AFTER tree-shaking, so a reference behind a statically-dead branch
336
+ * does not trigger injection, and matches only BARE identifiers (`print(...)`,
337
+ * `imports.gi.Gtk`) — see `ignoreHostMembers` for why the host-member form is ignored.
409
338
  */
410
339
  export function detectGjsAmbientGlobals(code) {
411
340
  return scanFreeGlobals(code, {
@@ -413,25 +342,21 @@ export function detectGjsAmbientGlobals(code) {
413
342
  ignoreHostMembers: true,
414
343
  });
415
344
  }
416
- /** The node-gi reverse-bridge package every `--app node` GJS shim resolves to. */
417
345
  import { ALIASES_GJS_FOR_NODE } from '@gjsify/resolve-npm';
346
+ /** The node-gi reverse-bridge package every `--app node` GJS shim resolves to. */
418
347
  const NODE_GI_PACKAGE = '@gjsify/node-gi';
419
348
  /**
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.
349
+ * The BARE GJS built-ins — the PRE-ALIAS spelling of the same fact, derived from the
350
+ * alias table so the two cannot drift.
429
351
  *
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`.
352
+ * Both spellings must count because the analysis pass that calls this runs with
353
+ * `nodeGiGlobalsInject` still FALSE: the routing that rewrites `system` →
354
+ * `@gjsify/node-gi/system` has not happened yet, so looking only for the aliased form
355
+ * makes the signal appear AFTER the decision it is supposed to trigger. Measured on
356
+ * adwaita-storybook `--app node`: the final bundle carried `import e from
357
+ * "@gjsify/node-gi/system"` and zero `gi://` — inject never fired, `@girs/*` were
358
+ * emptied to `{}`, nothing was left to rewrite. It passes `node --check` and dies at
359
+ * runtime with `class extends undefined`.
435
360
  */
436
361
  const GJS_BARE_BUILTINS = new Set(Object.keys(ALIASES_GJS_FOR_NODE));
437
362
  /**
@@ -446,30 +371,26 @@ function isNodeGiSpecifier(source) {
446
371
  return GJS_BARE_BUILTINS.has(source);
447
372
  }
448
373
  /**
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`.
374
+ * A STATIC import of `@gjsify/node-gi` (root or subpath) in a bundled `--app node`
375
+ * chunk — the second genuine-GJS-source signal beside `detectGjsAmbientGlobals`.
452
376
  *
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.
377
+ * It is a signal because the bare GJS built-ins are rewritten to
378
+ * `@gjsify/node-gi/<name>` and kept EXTERNAL as top-level imports: a tree-shaken graph
379
+ * that retains one is ALREADY bound to the bridge at load (Node resolves the external
380
+ * at link time and fails without node-gi), so treating it as a GJS-source build cannot
381
+ * cost plain-Node loadability. NOT treating it as one is how a portable GJS app
382
+ * spelling `ARGV` as `system.programArgs` lost its `@girs/*` bodies and its globals
383
+ * shim in one go — `gjsify storybook --runtime node` emitted a bundle with no
384
+ * `requireGi` at all.
463
385
  *
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.
386
+ * A surviving `gi://` import is deliberately NOT a signal: its shim loads node-gi LAZILY
387
+ * through a `require('@gjsify/node-gi/gi')` CALL, so this scan is structurally blind to
388
+ * it by design. A cross-platform package with a gjs-gated `gi://` import stays loadable
389
+ * on plain Node (#641, pinned by `tests/e2e/node-gi-globals-inject`), and flipping
390
+ * injection for it would break exactly that.
470
391
  *
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.
392
+ * Scans import/re-export STATEMENTS via the AST, never text, so a quoted specifier in a
393
+ * test name or template cannot trigger.
473
394
  */
474
395
  export function detectNodeGiModuleImports(code) {
475
396
  const ast = acorn.parse(code, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/rolldown-plugin-gjsify",
3
- "version": "0.36.0",
3
+ "version": "0.38.0",
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.36.0",
59
- "@gjsify/resolve-npm": "^0.36.0",
60
- "@gjsify/rolldown-plugin-deepkit": "^0.36.0",
61
- "@gjsify/rolldown-plugin-pnp": "^0.36.0",
62
- "@gjsify/vite-plugin-blueprint": "^0.36.0",
58
+ "@gjsify/console": "^0.38.0",
59
+ "@gjsify/resolve-npm": "^0.38.0",
60
+ "@gjsify/rolldown-plugin-deepkit": "^0.38.0",
61
+ "@gjsify/rolldown-plugin-pnp": "^0.38.0",
62
+ "@gjsify/vite-plugin-blueprint": "^0.38.0",
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.36.0",
71
+ "@gjsify/lightningcss-native": "^0.38.0",
72
72
  "rolldown": "^1.1.4"
73
73
  },
74
74
  "peerDependenciesMeta": {