@gjsify/cli 0.24.2 → 0.25.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.
@@ -60,6 +60,19 @@ export declare class BuildAction {
60
60
  * text scan cannot tell from a statement.
61
61
  */
62
62
  private assertGjsOutputLoadable;
63
+ /**
64
+ * Post-bundle gate for `--app node`: throw when the emitted bundle still
65
+ * references GJS ambient globals (`print`/`imports`/…) although the
66
+ * pre-build detection decided no `@gjsify/node-gi/globals` shim was needed
67
+ * — see `utils/node-bundle-guard.ts` for the full rationale.
68
+ *
69
+ * Unlike the `--app gjs` gate, the oracle here CANNOT be the module graph:
70
+ * an ambient global is a free identifier, not an import, so nothing about it
71
+ * appears in a chunk's `imports` list. It is the same acorn pass the
72
+ * detector runs, re-applied to what was actually written — a post-condition
73
+ * on the prediction, not a second opinion.
74
+ */
75
+ private assertNodeOutputGlobals;
63
76
  /**
64
77
  * Post-bundle step for a `--app browser` HTML entry: write the processed
65
78
  * `index.html` beside the JS bundle, with the entry `<script>`'s `src`
@@ -11,6 +11,7 @@ import { normalizeBundlerOptions, mergeBundlerOptions } from '../utils/normalize
11
11
  import { inputSourceDirs, isOutdirInsideSource, libraryOutputLeakError } from '../utils/library-output.js';
12
12
  import { detectHtmlEntry, parseHtmlEntry, emitBrowserHtml, htmlOutPathFor } from '../utils/html-entry.js';
13
13
  import { assertGjsBundleLoadable } from '../utils/gjs-bundle-guard.js';
14
+ import { assertNodeBundleGlobalsShimmed } from '../utils/node-bundle-guard.js';
14
15
  const DEFAULT_GJS_SHEBANG = '#!/usr/bin/env -S gjs -m';
15
16
  /**
16
17
  * `true` when `path` points at a location that's unsafe to use as a build
@@ -291,6 +292,31 @@ export class BuildAction {
291
292
  const chunks = (result.output ?? []).filter((item) => item.type === 'chunk');
292
293
  assertGjsBundleLoadable(chunks, outfile ?? outdir ?? 'the GJS bundle');
293
294
  }
295
+ /**
296
+ * Post-bundle gate for `--app node`: throw when the emitted bundle still
297
+ * references GJS ambient globals (`print`/`imports`/…) although the
298
+ * pre-build detection decided no `@gjsify/node-gi/globals` shim was needed
299
+ * — see `utils/node-bundle-guard.ts` for the full rationale.
300
+ *
301
+ * Unlike the `--app gjs` gate, the oracle here CANNOT be the module graph:
302
+ * an ambient global is a free identifier, not an import, so nothing about it
303
+ * appears in a chunk's `imports` list. It is the same acorn pass the
304
+ * detector runs, re-applied to what was actually written — a post-condition
305
+ * on the prediction, not a second opinion.
306
+ */
307
+ assertNodeOutputGlobals(result, outfile, outdir) {
308
+ const label = outfile ?? outdir ?? 'the node bundle';
309
+ for (const item of result.output ?? []) {
310
+ if (item.type !== 'chunk')
311
+ continue;
312
+ // `code` is present on a written chunk for both engines; skip
313
+ // defensively rather than fail a build over a missing field.
314
+ const code = item.code;
315
+ if (typeof code !== 'string' || code.length === 0)
316
+ continue;
317
+ assertNodeBundleGlobalsShimmed(code, item.fileName ? `${label} (${item.fileName})` : label);
318
+ }
319
+ }
294
320
  /**
295
321
  * Post-bundle step for a `--app browser` HTML entry: write the processed
296
322
  * `index.html` beside the JS bundle, with the entry `<script>`'s `src`
@@ -530,6 +556,30 @@ export class BuildAction {
530
556
  // detection must run BEFORE the final build so the entry can be
531
557
  // wrapped; nothing is injected when no globals are referenced (the
532
558
  // eager-native-load regression guard).
559
+ //
560
+ // Explicit extras on a node build (`--globals auto,dom,…`) are the
561
+ // reverse bridge's DOM-surface request: inject the SAME register
562
+ // modules the gjs target would for those identifiers, so a genuine
563
+ // GJS source (an Excalibur/WebGLBridge app) gets document /
564
+ // HTMLCanvasElement / matchMedia / … on Node via node-gi. The
565
+ // orchestrator lifts the `@gjsify/empty` register routing only when
566
+ // this inject stub is present (see app/node.ts) — plain `auto`
567
+ // node builds are byte-unchanged.
568
+ //
569
+ // The stub is resolved BEFORE the detection pass, and that ordering
570
+ // is load-bearing: it is one of the two signals `isGjsSourceBuild`
571
+ // reads, so it decides whether `/register` subpaths resolve for real
572
+ // or route to `@gjsify/empty` and whether `@girs/*` bodies survive.
573
+ // Resolving it afterwards made the analysis bundle a DIFFERENT
574
+ // module graph from the one actually emitted — every GJS ambient
575
+ // global reachable only THROUGH a register module was invisible, so
576
+ // the shim was not injected and the emitted bundle threw
577
+ // `ReferenceError: imports is not defined` at runtime (the
578
+ // excalibur-jelly-jumper showcase on node/bun, via
579
+ // `@gjsify/canvas2d-core`'s `_toDataURL` and `@gjsify/utils`).
580
+ if (extras) {
581
+ pluginOpts.autoGlobalsInject = await this.resolveGlobalsInject(app, extras, verbose);
582
+ }
533
583
  const gjsifyPluginFactory = async (opts) => {
534
584
  const cfg = await gjsifyPlugin({
535
585
  input: userBundler.input,
@@ -550,17 +600,6 @@ export class BuildAction {
550
600
  }, pluginOpts, gjsifyPluginFactory, bundleToChunks);
551
601
  if (needsGiGlobals)
552
602
  pluginOpts.nodeGiGlobalsInject = true;
553
- // Explicit extras on a node build (`--globals auto,dom,…`) are the
554
- // reverse bridge's DOM-surface request: inject the SAME register
555
- // modules the gjs target would for those identifiers, so a genuine
556
- // GJS source (an Excalibur/WebGLBridge app) gets document /
557
- // HTMLCanvasElement / matchMedia / … on Node via node-gi. The
558
- // orchestrator lifts the `@gjsify/empty` register routing only when
559
- // this inject stub is present (see app/node.ts) — plain `auto`
560
- // node builds are byte-unchanged.
561
- if (extras) {
562
- pluginOpts.autoGlobalsInject = await this.resolveGlobalsInject(app, extras, verbose);
563
- }
564
603
  }
565
604
  else if (extras) {
566
605
  pluginOpts.autoGlobalsInject = await this.resolveGlobalsInject(app, extras, verbose);
@@ -596,6 +635,13 @@ export class BuildAction {
596
635
  if (app === 'gjs') {
597
636
  this.assertGjsOutputLoadable(writeResult, outfile, outdir);
598
637
  }
638
+ // Refuse to hand back a `--app node` bundle that reaches for GJS ambient
639
+ // globals nothing seeds. Only meaningful when the shim was NOT injected:
640
+ // with it, the globals are installed and the references are correct.
641
+ // `--globals none` opts out of the mechanism, so it opts out of the gate.
642
+ if (app === 'node' && autoMode && !pluginOpts.nodeGiGlobalsInject) {
643
+ this.assertNodeOutputGlobals(writeResult, outfile, outdir);
644
+ }
599
645
  // Browser HTML entry: emit the processed `index.html` beside the bundle
600
646
  // (engine-portable post-step — the native rolldown engine has no
601
647
  // `emitFile`, so this can't live in a plugin). `outfile` is guaranteed
@@ -266,6 +266,17 @@ async function runShowcaseOnRuntime(showcase, runtime, cliVersion) {
266
266
  return process.exit(1);
267
267
  }
268
268
  console.log(`Running showcase: ${showcase.name} ${runningAs(cliVersion)}on ${runtime} (${entry})\n`);
269
- // Terminal call — exit on success.
270
- await runRuntimeBundle(runtime, entry, [], { exitOnSuccess: true });
269
+ // Terminal call — exit on success. Failures are reported here rather than
270
+ // thrown: an exception escaping the handler makes yargs dump the command's
271
+ // full `--help` above the message, which buries it.
272
+ try {
273
+ await runRuntimeBundle(runtime, entry, [], { exitOnSuccess: true });
274
+ }
275
+ catch (err) {
276
+ console.error(`${err.message}\n\n` +
277
+ ` The showcase and the \`@gjsify/node-gi\` bridge it needs are both installed by THIS CLI\n` +
278
+ ` (gjsify ${cliVersion ?? '(unknown)'}) into its own cache, so a missing bridge points at the CLI\n` +
279
+ ` rather than at anything in your project.\n\n ${PIN_HINT}`);
280
+ return process.exit(1);
281
+ }
271
282
  }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The GJS ambient globals referenced as BARE identifiers in `code`, sorted.
3
+ *
4
+ * Empty for a bundle that only uses the `globalThis.imports` isomorphic-guard
5
+ * shape — that form is a runtime probe, not a dependency, and the detector
6
+ * ignores it for exactly that reason.
7
+ */
8
+ export declare function findUnshimmedGjsGlobals(code: string): string[];
9
+ /**
10
+ * Throw when a `--app node` bundle references GJS ambient globals but was built
11
+ * without the `@gjsify/node-gi/globals` shim.
12
+ *
13
+ * @param code the emitted bundle source
14
+ * @param outfile path of the bundle, for the error message
15
+ */
16
+ export declare function assertNodeBundleGlobalsShimmed(code: string, outfile: string): void;
@@ -0,0 +1,69 @@
1
+ // Build-time guard: a `--app node` bundle that USES the GJS ambient globals
2
+ // must also have been given `@gjsify/node-gi/globals`.
3
+ //
4
+ // `print`/`printerr`/`log`/`logError`/`ARGV`/`imports` exist implicitly under
5
+ // gjs and nowhere else. On the node target they are seeded by the side-effect
6
+ // shim `@gjsify/node-gi/globals`, which the CLI injects when — and only when —
7
+ // `detectNodeGiGlobals` finds one of them in an analysis bundle. Injecting it
8
+ // unconditionally is not an option: it eagerly loads the native node-gi addon,
9
+ // so a cross-platform package's node bundle would stop loading on plain Node.
10
+ //
11
+ // That makes the decision a PREDICTION about an artifact that does not exist
12
+ // yet, and a prediction can be wrong. It was: the analysis pass used to run
13
+ // before the explicit-`--globals` register stub was resolved, and that stub is
14
+ // one of the two signals `isGjsSourceBuild()` reads — so the analysis bundle
15
+ // routed `/register` subpaths to `@gjsify/empty` and emptied `@girs/*` while the
16
+ // REAL build resolved them for real. Every ambient global reachable only through
17
+ // a register module was therefore invisible to the detector. The build exited 0
18
+ // and the emitted bundle died at its first canvas op with
19
+ //
20
+ // ReferenceError: imports is not defined
21
+ // at CanvasRenderingContext2D._toDataURL
22
+ //
23
+ // on node AND bun (the excalibur-jelly-jumper showcase), while gjs — where the
24
+ // globals are ambient — ran fine. Nothing between `gjsify build --app node` and
25
+ // a user running the bundle ever looked at the artifact.
26
+ //
27
+ // So this is a POST-condition on what was actually emitted: whatever the
28
+ // detector predicted, check it against the bundle. It is deliberately NOT a
29
+ // second detector — it runs the SAME `detectGjsAmbientGlobals` over the SAME
30
+ // kind of input (bundled, tree-shaken output, parsed by acorn, bare identifiers
31
+ // only). Two passes over one oracle that must agree; when they don't, the graph
32
+ // they saw differed, which is a bug in the build pipeline, not in user code.
33
+ //
34
+ // Scope: only when the CLI decided NOT to inject (`--globals auto`, no shim).
35
+ // `--globals none` opts out of the whole mechanism explicitly and is skipped.
36
+ import { detectGjsAmbientGlobals } from '@gjsify/rolldown-plugin-gjsify/globals';
37
+ /**
38
+ * The GJS ambient globals referenced as BARE identifiers in `code`, sorted.
39
+ *
40
+ * Empty for a bundle that only uses the `globalThis.imports` isomorphic-guard
41
+ * shape — that form is a runtime probe, not a dependency, and the detector
42
+ * ignores it for exactly that reason.
43
+ */
44
+ export function findUnshimmedGjsGlobals(code) {
45
+ return [...detectGjsAmbientGlobals(code)].sort();
46
+ }
47
+ /**
48
+ * Throw when a `--app node` bundle references GJS ambient globals but was built
49
+ * without the `@gjsify/node-gi/globals` shim.
50
+ *
51
+ * @param code the emitted bundle source
52
+ * @param outfile path of the bundle, for the error message
53
+ */
54
+ export function assertNodeBundleGlobalsShimmed(code, outfile) {
55
+ const offenders = findUnshimmedGjsGlobals(code);
56
+ if (offenders.length === 0)
57
+ return;
58
+ throw new Error(`gjsify build --app node: ${outfile} references ${offenders.length} GJS ambient global(s) that ` +
59
+ 'nothing installs on node/bun/deno, so the bundle throws `ReferenceError: <name> is not defined` ' +
60
+ 'the first time that code runs:\n' +
61
+ offenders.map((s) => ` - ${s}`).join('\n') +
62
+ '\n\nThe `@gjsify/node-gi/globals` shim seeds them, but it is injected only when the pre-build ' +
63
+ 'detection pass finds them — and that pass did not. The two passes therefore disagreed about the ' +
64
+ 'module graph. Either the detection ran against a different graph than the one emitted (check that ' +
65
+ 'anything feeding `isGjsSourceBuild()` — the `--globals` register stub, `nodeGiGlobalsInject` — is ' +
66
+ 'settled BEFORE `detectNodeGiGlobals`), or the offending package should stop reaching for the ' +
67
+ 'legacy `imports` object: `gi://<Ns>` and the bare `system`/`gettext`/`cairo` built-ins resolve on ' +
68
+ 'both runtimes, `imports` does not.');
69
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/cli",
3
- "version": "0.24.2",
3
+ "version": "0.25.1",
4
4
  "description": "CLI for Gjsify",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -130,19 +130,19 @@
130
130
  "cli"
131
131
  ],
132
132
  "dependencies": {
133
- "@gjsify/buffer": "^0.24.2",
134
- "@gjsify/create-app": "^0.24.2",
135
- "@gjsify/node-globals": "^0.24.2",
136
- "@gjsify/node-polyfills": "^0.24.2",
137
- "@gjsify/npm-registry": "^0.24.2",
138
- "@gjsify/resolve-npm": "^0.24.2",
139
- "@gjsify/rolldown-plugin-gjsify": "^0.24.2",
140
- "@gjsify/rolldown-plugin-pnp": "^0.24.2",
141
- "@gjsify/semver": "^0.24.2",
142
- "@gjsify/tar": "^0.24.2",
143
- "@gjsify/tsc": "^0.24.2",
144
- "@gjsify/web-polyfills": "^0.24.2",
145
- "@gjsify/workspace": "^0.24.2",
133
+ "@gjsify/buffer": "^0.25.1",
134
+ "@gjsify/create-app": "^0.25.1",
135
+ "@gjsify/node-globals": "^0.25.1",
136
+ "@gjsify/node-polyfills": "^0.25.1",
137
+ "@gjsify/npm-registry": "^0.25.1",
138
+ "@gjsify/resolve-npm": "^0.25.1",
139
+ "@gjsify/rolldown-plugin-gjsify": "^0.25.1",
140
+ "@gjsify/rolldown-plugin-pnp": "^0.25.1",
141
+ "@gjsify/semver": "^0.25.1",
142
+ "@gjsify/tar": "^0.25.1",
143
+ "@gjsify/tsc": "^0.25.1",
144
+ "@gjsify/web-polyfills": "^0.25.1",
145
+ "@gjsify/workspace": "^0.25.1",
146
146
  "cosmiconfig": "^9.0.2",
147
147
  "get-tsconfig": "^4.14.0",
148
148
  "pkg-types": "^2.3.1",
@@ -150,13 +150,13 @@
150
150
  "yargs": "^18.0.0"
151
151
  },
152
152
  "devDependencies": {
153
- "@gjsify/unit": "^0.24.2",
153
+ "@gjsify/unit": "^0.25.1",
154
154
  "@types/yargs": "^17.0.35",
155
155
  "typescript": "^6.0.3"
156
156
  },
157
157
  "peerDependencies": {
158
- "@gjsify/oxfmt-native": "^0.24.2",
159
- "@gjsify/rolldown-native": "^0.24.2"
158
+ "@gjsify/oxfmt-native": "^0.25.1",
159
+ "@gjsify/rolldown-native": "^0.25.1"
160
160
  },
161
161
  "peerDependenciesMeta": {
162
162
  "@gjsify/oxfmt-native": {