@gjsify/cli 0.35.0 → 0.37.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.
Files changed (124) hide show
  1. package/dist/cli.gjs.mjs +136 -126
  2. package/lib/actions/build.d.ts +19 -0
  3. package/lib/actions/build.js +2 -2
  4. package/lib/bundler-pick.js +72 -98
  5. package/lib/cli-fail.d.ts +4 -7
  6. package/lib/cli-fail.js +29 -55
  7. package/lib/commands/affected.js +161 -286
  8. package/lib/commands/install.d.ts +63 -66
  9. package/lib/commands/install.js +372 -536
  10. package/lib/commands/run.d.ts +1 -0
  11. package/lib/commands/run.js +37 -0
  12. package/lib/templates/install.mjs.tmpl +38 -68
  13. package/lib/types/cli-build-options.d.ts +33 -67
  14. package/lib/types/command.d.ts +9 -12
  15. package/lib/types/config-data.d.ts +125 -221
  16. package/lib/utils/auth-npmrc.d.ts +5 -10
  17. package/lib/utils/auth-npmrc.js +11 -21
  18. package/lib/utils/check-system-deps.d.ts +16 -0
  19. package/lib/utils/check-system-deps.js +49 -0
  20. package/lib/utils/clear-targets.d.ts +8 -11
  21. package/lib/utils/clear-targets.js +22 -29
  22. package/lib/utils/copy-targets.d.ts +13 -19
  23. package/lib/utils/copy-targets.js +31 -44
  24. package/lib/utils/dep-aggregation.d.ts +7 -15
  25. package/lib/utils/dep-aggregation.js +9 -22
  26. package/lib/utils/detect-native-packages.d.ts +83 -116
  27. package/lib/utils/detect-native-packages.js +111 -149
  28. package/lib/utils/dir-link.d.ts +29 -48
  29. package/lib/utils/dir-link.js +33 -56
  30. package/lib/utils/discover-showcases.d.ts +6 -7
  31. package/lib/utils/discover-showcases.js +11 -15
  32. package/lib/utils/dlx-cache.d.ts +29 -38
  33. package/lib/utils/dlx-cache.js +53 -63
  34. package/lib/utils/force-exit.d.ts +0 -3
  35. package/lib/utils/force-exit.js +13 -24
  36. package/lib/utils/gi-search-path.d.ts +16 -24
  37. package/lib/utils/gi-search-path.js +54 -82
  38. package/lib/utils/gjs-bundle-guard.d.ts +5 -13
  39. package/lib/utils/gjs-bundle-guard.js +35 -57
  40. package/lib/utils/gjs-source-escape.d.ts +2 -2
  41. package/lib/utils/gjs-source-escape.js +21 -39
  42. package/lib/utils/gjsify-shim.d.ts +26 -8
  43. package/lib/utils/gjsify-shim.js +128 -80
  44. package/lib/utils/html-entry.d.ts +8 -12
  45. package/lib/utils/html-entry.js +19 -28
  46. package/lib/utils/install-backend-native.d.ts +123 -161
  47. package/lib/utils/install-backend-native.js +536 -737
  48. package/lib/utils/install-backend.d.ts +42 -57
  49. package/lib/utils/install-backend.js +16 -21
  50. package/lib/utils/install-cache-fs.d.ts +10 -15
  51. package/lib/utils/install-cache-fs.js +18 -26
  52. package/lib/utils/install-lock.d.ts +5 -6
  53. package/lib/utils/install-lock.js +46 -65
  54. package/lib/utils/install-packument-cache.d.ts +27 -33
  55. package/lib/utils/install-packument-cache.js +44 -57
  56. package/lib/utils/install-progress.d.ts +2 -9
  57. package/lib/utils/install-progress.js +14 -39
  58. package/lib/utils/install-tarball-cache.d.ts +14 -20
  59. package/lib/utils/install-tarball-cache.js +41 -59
  60. package/lib/utils/library-output.d.ts +9 -22
  61. package/lib/utils/library-output.js +19 -37
  62. package/lib/utils/load-npmrc.d.ts +5 -6
  63. package/lib/utils/load-npmrc.js +18 -27
  64. package/lib/utils/node-bundle-guard.d.ts +4 -8
  65. package/lib/utils/node-bundle-guard.js +21 -35
  66. package/lib/utils/node-script.d.ts +12 -0
  67. package/lib/utils/node-script.js +95 -0
  68. package/lib/utils/node-version.d.ts +5 -14
  69. package/lib/utils/node-version.js +15 -24
  70. package/lib/utils/normalize-bundler-options.d.ts +7 -11
  71. package/lib/utils/normalize-bundler-options.js +24 -42
  72. package/lib/utils/npm-oidc.d.ts +23 -37
  73. package/lib/utils/npm-oidc.js +28 -53
  74. package/lib/utils/npm-otp-cache.d.ts +4 -4
  75. package/lib/utils/npm-otp-cache.js +20 -25
  76. package/lib/utils/npm-otp.d.ts +20 -30
  77. package/lib/utils/npm-otp.js +32 -51
  78. package/lib/utils/onboard-probe.d.ts +7 -13
  79. package/lib/utils/onboard-probe.js +16 -27
  80. package/lib/utils/parse-spec.d.ts +3 -3
  81. package/lib/utils/parse-spec.js +4 -5
  82. package/lib/utils/pkg-json-edit.d.ts +7 -12
  83. package/lib/utils/pkg-json-edit.js +13 -26
  84. package/lib/utils/pkg-json.d.ts +11 -18
  85. package/lib/utils/pkg-json.js +19 -28
  86. package/lib/utils/prefixed-output.d.ts +3 -4
  87. package/lib/utils/prefixed-output.js +13 -19
  88. package/lib/utils/prompt.d.ts +3 -4
  89. package/lib/utils/prompt.js +14 -18
  90. package/lib/utils/publish-diagnose.d.ts +11 -16
  91. package/lib/utils/publish-diagnose.js +15 -28
  92. package/lib/utils/publish-headers.d.ts +7 -11
  93. package/lib/utils/publish-headers.js +36 -65
  94. package/lib/utils/publishable-packages.d.ts +4 -6
  95. package/lib/utils/publishable-packages.js +14 -18
  96. package/lib/utils/resolve-gjs-entry.d.ts +5 -8
  97. package/lib/utils/resolve-gjs-entry.js +10 -27
  98. package/lib/utils/resolve-npm-package.d.ts +6 -11
  99. package/lib/utils/resolve-npm-package.js +29 -63
  100. package/lib/utils/resolve-plugin-by-name.d.ts +11 -19
  101. package/lib/utils/resolve-plugin-by-name.js +9 -14
  102. package/lib/utils/run-gjs.d.ts +19 -17
  103. package/lib/utils/run-gjs.js +37 -51
  104. package/lib/utils/run-lifecycle-script.d.ts +5 -9
  105. package/lib/utils/run-lifecycle-script.js +23 -42
  106. package/lib/utils/run-node.d.ts +23 -33
  107. package/lib/utils/run-node.js +56 -60
  108. package/lib/utils/runtimes.d.ts +25 -35
  109. package/lib/utils/runtimes.js +34 -46
  110. package/lib/utils/spawn.d.ts +32 -48
  111. package/lib/utils/spawn.js +51 -84
  112. package/lib/utils/stdin.d.ts +2 -2
  113. package/lib/utils/stdin.js +10 -25
  114. package/lib/utils/suggest.d.ts +6 -7
  115. package/lib/utils/suggest.js +8 -10
  116. package/lib/utils/system-gi.d.ts +18 -27
  117. package/lib/utils/system-gi.js +60 -114
  118. package/lib/utils/trust-registry.d.ts +4 -8
  119. package/lib/utils/trust-registry.js +7 -15
  120. package/lib/utils/win32-command.d.ts +8 -14
  121. package/lib/utils/win32-command.js +29 -43
  122. package/lib/utils/workspace-root.js +7 -12
  123. package/package.json +18 -17
  124. package/showcases.json +6 -0
@@ -1,27 +1,20 @@
1
1
  // Guard against the recurring "library build leaks into src/" footgun.
2
2
  //
3
- // `BuildAction.buildLibrary` derives its output directory from
4
- // `dirname(package.json "module" ?? "main")`. When a package points `main`
5
- // at its runtime SOURCE entry (`src/index.ts` — the intended entry for
6
- // Vite/gjsify-compiled consumers, e.g. `@gjsify/adwaita-web`), that derived
7
- // outdir is `src/`, so `gjsify build --library` writes the compiled `.js`
8
- // tree straight next to the sources: `src/*.js` / `*2.js` duplicates, a
9
- // `src/_virtual/` helper dir, and a nested `src/<pkg>/src/**` preserve-modules
10
- // tree. These pure helpers let the build action refuse that up front instead
11
- // of silently corrupting the source tree.
3
+ // `BuildAction.buildLibrary` derives its outdir from `dirname(package.json "module" ?? "main")`.
4
+ // When a package points `main` at its runtime SOURCE entry (`src/index.ts` — the intended entry
5
+ // for Vite/gjsify-compiled consumers, e.g. `@gjsify/adwaita-web`), that outdir is `src/`, so
6
+ // `gjsify build --library` writes the compiled tree next to the sources: `src/*.js` / `*2.js`
7
+ // duplicates, a `src/_virtual/` helper dir, and a nested `src/<pkg>/src/**` preserve-modules
8
+ // tree. These pure helpers let the build action refuse that up front.
12
9
  import { dirname, extname, isAbsolute, relative, resolve } from 'node:path';
13
10
  /**
14
- * The literal (non-glob) base directories the build's input source files live
15
- * in, derived from the bundler `input` patterns. A file entry maps to its
16
- * containing dir, a glob is reduced to the literal prefix before the first
17
- * wildcard:
18
- * - `src/index.ts` → `<cwd>/src`
19
- * - `src/**` + `/*.ts` → `<cwd>/src`
20
- * - `src` → `<cwd>/src`
11
+ * The literal (non-glob) base directories the input source files live in, derived from the
12
+ * bundler `input` patterns: a file entry maps to its containing dir, a glob is reduced to the
13
+ * literal prefix before the first wildcard (`src/index.ts`, `src` and `src/**` + `/*.ts` all
14
+ * give `<cwd>/src`).
21
15
  *
22
- * The project root itself is intentionally excluded — a flat library that
23
- * keeps its source at the project root and emits there is an explicit layout,
24
- * not the `src/`-subdir footgun this guard targets.
16
+ * The project root is intentionally excluded — a flat library keeping its source at the root and
17
+ * emitting there is an explicit layout, not the `src/`-subdir footgun this guard targets.
25
18
  */
26
19
  export function inputSourceDirs(input, cwd) {
27
20
  const patterns = [];
@@ -39,18 +32,16 @@ export function inputSourceDirs(input, cwd) {
39
32
  const dirs = new Set();
40
33
  for (const raw of patterns) {
41
34
  const norm = raw.replace(/\\/g, '/');
42
- // Cut the pattern at the first glob metacharacter — everything before
43
- // it is a literal path prefix.
35
+ // Everything before the first glob metacharacter is a literal path prefix.
44
36
  const globIdx = norm.search(/[*?{}[\]()!+@]/);
45
37
  let base = globIdx === -1 ? norm : norm.slice(0, globIdx);
46
38
  if (globIdx === -1) {
47
- // Whole pattern is literal: a file path collapses to its dirname,
48
- // a bare directory stays as-is.
39
+ // Wholly literal: a file path collapses to its dirname, a bare directory stays.
49
40
  base = extname(base) ? dirname(base) : base;
50
41
  }
51
42
  else {
52
- // The glob cut mid-segment (`src/`, `src/fo*`) — drop the trailing
53
- // partial segment so we keep only the complete literal directory.
43
+ // The glob cut mid-segment (`src/fo*`) — drop the trailing partial segment so only
44
+ // the complete literal directory remains.
54
45
  base = base.replace(/\/[^/]*$/, '');
55
46
  }
56
47
  const abs = resolve(root, base || '.');
@@ -65,23 +56,14 @@ function isInsideOrEqual(child, parent) {
65
56
  return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
66
57
  }
67
58
  /**
68
- * `true` when writing library output to `outdir` would land build artifacts
69
- * inside (or directly on top of) the input source tree. Unsafe iff `outdir`
70
- * equals, or is nested under, any input source directory.
71
- *
72
- * isOutdirInsideSource('src', ['<cwd>/src']) → true (leak)
73
- * isOutdirInsideSource('src/nest', ['<cwd>/src']) → true (leak)
74
- * isOutdirInsideSource('dist/esm', ['<cwd>/src']) → false (safe)
75
- * isOutdirInsideSource('lib', ['<cwd>/src']) → false (safe)
59
+ * `true` when library output written to `outdir` would land inside (or on top of) the input
60
+ * source tree — i.e. `outdir` equals, or is nested under, any input source directory.
76
61
  */
77
62
  export function isOutdirInsideSource(outdir, sourceDirs, cwd) {
78
63
  const absOut = resolve(cwd, outdir);
79
64
  return sourceDirs.some((dir) => isInsideOrEqual(absOut, dir));
80
65
  }
81
- /**
82
- * The error thrown when a `--library` build would emit into its own source
83
- * tree. Names both remedies so the fix is actionable at the point of failure.
84
- */
66
+ /** The error thrown when a `--library` build would emit into its own source tree. */
85
67
  export function libraryOutputLeakError(outdir) {
86
68
  return new Error(`gjsify build --library: refusing to write output into "${outdir}", which holds the ` +
87
69
  `input source — a library build there produces src/*.js duplicates, *2.js, a ` +
@@ -1,14 +1,13 @@
1
1
  import { type NpmrcConfig } from '@gjsify/npm-registry';
2
2
  export declare function loadNpmrc(cwd: string): Promise<NpmrcConfig>;
3
3
  /**
4
- * True iff the parsed npmrc contains *any* `_authToken` entry. Cheap helper
5
- * for commands that want a clear "no token configured" error message
6
- * instead of a generic 401/empty response.
4
+ * True iff the parsed npmrc contains *any* `_authToken` entry — lets a command
5
+ * say "no token configured" instead of relaying a generic 401.
7
6
  */
8
7
  export declare function hasAnyAuthToken(npmrc: NpmrcConfig): boolean;
9
8
  /**
10
- * True iff the parsed npmrc has *any* credential (bearer token or basic
11
- * auth). Used by `gjsify whoami` to differentiate "no token configured"
12
- * from "token configured but rejected by the registry".
9
+ * True iff the parsed npmrc has *any* credential (bearer token or basic auth).
10
+ * `gjsify whoami` uses it to tell "no token configured" apart from "token
11
+ * configured but rejected by the registry".
13
12
  */
14
13
  export declare function hasAnyCredential(npmrc: NpmrcConfig): boolean;
@@ -1,25 +1,17 @@
1
- // Shared npmrc loader for `gjsify publish` / `gjsify whoami` / future
2
- // auth-aware commands. Mirrors npm CLI's resolution order so a maintainer's
3
- // `~/.npmrc` token AND a CI-injected `NPM_CONFIG_USERCONFIG` token both
4
- // reach the registry call.
1
+ // Shared npmrc loader for the auth-aware commands (`gjsify publish`,
2
+ // `gjsify whoami`, …), so that both a maintainer's `~/.npmrc` token and a
3
+ // CI-injected `NPM_CONFIG_USERCONFIG` token reach the registry call.
4
+ // `actions/setup-node` writes its auth-token npmrc to `$RUNNER_TEMP/.npmrc` and
5
+ // exports `NPM_CONFIG_USERCONFIG` at it — it never touches `~/.npmrc`, so
6
+ // honouring that env var is what makes CI authentication work, and `${VAR}`
7
+ // placeholders must be expanded on read (npm's own behaviour):
8
+ // `_authToken=${NODE_AUTH_TOKEN}` arrives as a literal placeholder.
5
9
  //
6
- // Resolution order (lowest → highest precedence — last write wins on key
7
- // collisions because the sources are concatenated and re-parsed):
8
- // 1. globalconfig: /etc/npmrc (system; intentionally NOT read here —
9
- // gjsify's user-facing commands operate on per-user
10
- // credentials, matching `npm whoami`'s behavior)
11
- // 2. userconfig: $NPM_CONFIG_USERCONFIG (overrides ~/.npmrc)
12
- // or ~/.npmrc (default)
13
- // 3. projectconfig: ./.npmrc (closest workspace)
14
- //
15
- // `actions/setup-node` writes the auth-token npmrc to $RUNNER_TEMP/.npmrc
16
- // and exports NPM_CONFIG_USERCONFIG pointing at it — it does NOT touch
17
- // ~/.npmrc. Honor the env var so CI authentication works end-to-end.
18
- //
19
- // `${VAR}` placeholders are expanded inline (npm CLI's expand-on-read
20
- // behavior). The auth-token npmrc from actions/setup-node ships
21
- // `_authToken=${NODE_AUTH_TOKEN}` as a literal placeholder; the env var is
22
- // set on the publish step.
10
+ // `/etc/npmrc` (globalconfig) is deliberately NOT read — these commands operate
11
+ // on per-user credentials, matching `npm whoami`. The remaining sources are
12
+ // concatenated and re-parsed, so LAST WRITE WINS on a key collision: appended
13
+ // project-first then user, the user/CI config overrides the project `.npmrc`,
14
+ // which is the inverse of npm's own precedence.
23
15
  import { existsSync, readFileSync } from 'node:fs';
24
16
  import { homedir } from 'node:os';
25
17
  import { join } from 'node:path';
@@ -44,17 +36,16 @@ export async function loadNpmrc(cwd) {
44
36
  return parseNpmrc(merged);
45
37
  }
46
38
  /**
47
- * True iff the parsed npmrc contains *any* `_authToken` entry. Cheap helper
48
- * for commands that want a clear "no token configured" error message
49
- * instead of a generic 401/empty response.
39
+ * True iff the parsed npmrc contains *any* `_authToken` entry — lets a command
40
+ * say "no token configured" instead of relaying a generic 401.
50
41
  */
51
42
  export function hasAnyAuthToken(npmrc) {
52
43
  return Object.keys(npmrc.authTokens).length > 0;
53
44
  }
54
45
  /**
55
- * True iff the parsed npmrc has *any* credential (bearer token or basic
56
- * auth). Used by `gjsify whoami` to differentiate "no token configured"
57
- * from "token configured but rejected by the registry".
46
+ * True iff the parsed npmrc has *any* credential (bearer token or basic auth).
47
+ * `gjsify whoami` uses it to tell "no token configured" apart from "token
48
+ * configured but rejected by the registry".
58
49
  */
59
50
  export function hasAnyCredential(npmrc) {
60
51
  return hasAnyAuthToken(npmrc) || Object.keys(npmrc.basicAuth).length > 0;
@@ -1,16 +1,12 @@
1
1
  /**
2
2
  * The GJS ambient globals referenced as BARE identifiers in `code`, sorted.
3
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.
4
+ * Empty for a bundle that only uses the `globalThis.imports` isomorphic-guard shape — that
5
+ * form is a runtime probe, not a dependency, and the detector ignores it for that reason.
7
6
  */
8
7
  export declare function findUnshimmedGjsGlobals(code: string): string[];
9
8
  /**
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
9
+ * Throw when a `--app node` bundle references GJS ambient globals but was built without
10
+ * the `@gjsify/node-gi/globals` shim.
15
11
  */
16
12
  export declare function assertNodeBundleGlobalsShimmed(code: string, outfile: string): void;
@@ -1,35 +1,25 @@
1
1
  // Build-time guard: a `--app node` bundle that USES the GJS ambient globals
2
2
  // must also have been given `@gjsify/node-gi/globals`.
3
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.
4
+ // `print`/`printerr`/`log`/`logError`/`ARGV`/`imports` exist implicitly under gjs and
5
+ // nowhere else. On the node target the side-effect shim `@gjsify/node-gi/globals` seeds
6
+ // them, injected only when `detectNodeGiGlobals` finds one in an analysis bundle —
7
+ // injecting unconditionally would eagerly load the native node-gi addon, so a
8
+ // cross-platform package's node bundle would stop loading on plain Node.
10
9
  //
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
10
+ // That makes the decision a PREDICTION about an artifact that does not exist yet, and it
11
+ // was wrong once: the analysis pass ran before the explicit-`--globals` register stub was
12
+ // resolved, and that stub is one of the two signals `isGjsSourceBuild()` reads, so the
13
+ // analysis bundle routed `/register` subpaths to `@gjsify/empty` and emptied `@girs/*`
14
+ // while the REAL build resolved them — making every ambient global reachable only through
15
+ // a register module invisible to the detector. The build exited 0 and the bundle died at
16
+ // its first canvas op with `ReferenceError: imports is not defined` on node AND bun (the
17
+ // excalibur-jelly-jumper showcase), while gjs ran fine.
19
18
  //
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.
19
+ // So this is a POST-condition on what was actually emitted, deliberately NOT a second
20
+ // detector: it runs the SAME `detectGjsAmbientGlobals` over the SAME kind of input
21
+ // (bundled, tree-shaken output, parsed by acorn, bare identifiers only). Two passes over
22
+ // one oracle that must agree; a disagreement is a bug in the build pipeline, not user code.
33
23
  //
34
24
  // Scope: only when the CLI decided NOT to inject (`--globals auto`, no shim).
35
25
  // `--globals none` opts out of the whole mechanism explicitly and is skipped.
@@ -37,19 +27,15 @@ import { detectGjsAmbientGlobals } from '@gjsify/rolldown-plugin-gjsify/globals'
37
27
  /**
38
28
  * The GJS ambient globals referenced as BARE identifiers in `code`, sorted.
39
29
  *
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.
30
+ * Empty for a bundle that only uses the `globalThis.imports` isomorphic-guard shape — that
31
+ * form is a runtime probe, not a dependency, and the detector ignores it for that reason.
43
32
  */
44
33
  export function findUnshimmedGjsGlobals(code) {
45
34
  return [...detectGjsAmbientGlobals(code)].sort();
46
35
  }
47
36
  /**
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
37
+ * Throw when a `--app node` bundle references GJS ambient globals but was built without
38
+ * the `@gjsify/node-gi/globals` shim.
53
39
  */
54
40
  export function assertNodeBundleGlobalsShimmed(code, outfile) {
55
41
  const offenders = findUnshimmedGjsGlobals(code);
@@ -0,0 +1,12 @@
1
+ export interface RunNodeScriptOptions {
2
+ /** Exit the process when the script succeeds. See `RunGjsBundleOptions.exitOnSuccess`. */
3
+ exitOnSuccess?: boolean;
4
+ }
5
+ /**
6
+ * Run `file` — an unbundled ESM script that imports `node:` builtins — on the
7
+ * CLI's own host runtime, bundling it for GJS first when that is where we are.
8
+ *
9
+ * Terminal callers pass `exitOnSuccess` (a GJS main loop armed by the spawn
10
+ * parks the process otherwise); see {@link runGjsBundle}.
11
+ */
12
+ export declare function runNodeScript(file: string, extraArgs?: string[], options?: RunNodeScriptOptions): Promise<void>;
@@ -0,0 +1,95 @@
1
+ // `gjsify run --node-script <file>` — run an UNBUNDLED `.mjs`/`.js` that imports
2
+ // `node:*` builtins, on whatever runtime the CLI itself is executing on.
3
+ //
4
+ // A repo script that imports only `node:fs`/`node:path`/`node:url` — all polyfilled — is not
5
+ // intrinsically Node-bound; what was missing is a way to RUN one under GJS, whose ESM loader
6
+ // has no `node:` URI scheme and no node_modules walker, so `gjs -m <script>.mjs` dies on the
7
+ // first import. That gap is why a Node-less host could install gjsify, resolve every native
8
+ // prebuild, and then fail `build:infra` at `node scripts/process-template.mjs` (see
9
+ // `status/open-todos.md`).
10
+ //
11
+ // Rejected alternative: commit a `dist/*.gjs.mjs` per script — one more artifact each under
12
+ // the committed-bundle freshness gate, scaling with the number of scripts. This is ONE
13
+ // mechanism serving every script in the repo and every consumer's too.
14
+ //
15
+ // node / bun / deno host → spawn the file on that runtime; no bundling, no cost.
16
+ // gjs host → bundle `--app gjs` (which maps `node:fs` → `@gjsify/fs`,
17
+ // injects the globals the script reads, and resolves bare
18
+ // specifiers), then run the bundle.
19
+ //
20
+ // `import.meta.url` is the whole trick: the bundle does not live where the source lives, and
21
+ // a script's own location is how it finds what it operates on — all four scripts this was
22
+ // built for derive their package root from `dirname(fileURLToPath(import.meta.url))`. Left
23
+ // alone that is the BUNDLE's URL, so every one would write its output into the cache
24
+ // directory; the source file's URL is therefore baked in as a compile-time define, exactly
25
+ // as `config.ts`'s `loadConfigViaGjsBundle` does for a `gjsify.config.js` that reads its own
26
+ // siblings. `process.argv` needs no such care: the `--app gjs` bundle already exposes
27
+ // `[gjs, <bundle>, ...args]`.
28
+ import { existsSync, statSync } from 'node:fs';
29
+ import { basename, resolve } from 'node:path';
30
+ import { pathToFileURL } from 'node:url';
31
+ import { hostRuntime } from '@gjsify/rolldown-plugin-gjsify/runtime';
32
+ import { runGjsBundle } from './run-gjs.js';
33
+ import { runRuntimeBundle } from './run-node.js';
34
+ /**
35
+ * Whether the on-the-fly GJS bundle may be reused across runs.
36
+ *
37
+ * OFF by default: `bundleFileForGjsCached` invalidates on the ENTRY file's mtime plus the
38
+ * project lockfile, which covers a plugin or a config but NOT a repo script, whose imports
39
+ * are workspace packages that change without any lockfile write. A stale bundle there does
40
+ * not fail loudly — the script runs and EMITS BUILD OUTPUT from code that no longer exists
41
+ * in the tree, and a build script is the worst place to reintroduce that.
42
+ *
43
+ * The cost is one bundle per invocation, and only under GJS. `GJSIFY_NODE_SCRIPT_CACHE=1`
44
+ * opts in for a host that would rather have the seconds back.
45
+ */
46
+ function scriptCacheEnabled() {
47
+ const v = process.env['GJSIFY_NODE_SCRIPT_CACHE'];
48
+ return v === '1' || v === 'true' || v === 'yes';
49
+ }
50
+ /**
51
+ * Run `file` — an unbundled ESM script that imports `node:` builtins — on the
52
+ * CLI's own host runtime, bundling it for GJS first when that is where we are.
53
+ *
54
+ * Terminal callers pass `exitOnSuccess` (a GJS main loop armed by the spawn
55
+ * parks the process otherwise); see {@link runGjsBundle}.
56
+ */
57
+ export async function runNodeScript(file, extraArgs = [], options = {}) {
58
+ const scriptPath = resolve(file);
59
+ if (!existsSync(scriptPath) || !statSync(scriptPath).isFile()) {
60
+ console.error(`gjsify run --node-script: no such file: ${scriptPath}`);
61
+ // `return process.exit` — a bare exit falls through under GJS (see run.ts).
62
+ return process.exit(1);
63
+ }
64
+ const host = hostRuntime();
65
+ if (host !== 'gjs') {
66
+ // Node/bun/deno load the file as-is; bundling would only add a step and a way to
67
+ // differ from what the runtime would have done itself. `quiet` because the echo
68
+ // would print `node <file>` — what the package.json script already reads — behind
69
+ // ~2 kB of native-library env, a dozen times per build chain.
70
+ await runRuntimeBundle(host, scriptPath, extraArgs, { exitOnSuccess: options.exitOnSuccess, quiet: true });
71
+ return;
72
+ }
73
+ // Dynamic import keeps the bundler machinery out of the eager module graph
74
+ // of every `gjsify run`, matching how config.ts pulls it.
75
+ const { BuildAction } = await import('../actions/build.js');
76
+ let bundlePath;
77
+ try {
78
+ bundlePath = await BuildAction.bundleFileForGjsCached(scriptPath, {
79
+ cacheSubdir: 'node-scripts',
80
+ label: basename(scriptPath),
81
+ cache: scriptCacheEnabled(),
82
+ // A script is executed, never imported — no default export to preserve.
83
+ preserveDefaultExport: false,
84
+ define: { 'import.meta.url': JSON.stringify(pathToFileURL(scriptPath).href) },
85
+ });
86
+ }
87
+ catch (err) {
88
+ throw new Error(`gjsify run --node-script: failed to bundle ${scriptPath} for GJS. ` +
89
+ `The script must import only what gjsify can provide under GJS (node: builtins, ` +
90
+ `workspace packages, pure-JS npm deps). (${err.message})`);
91
+ }
92
+ // Quiet for the same reason as the node path above; the bundle's location is
93
+ // deterministic (`node_modules/.cache/gjsify/node-scripts/<name>-<hash>.mjs`).
94
+ await runGjsBundle(bundlePath, extraArgs, { exitOnSuccess: options.exitOnSuccess, quiet: true });
95
+ }
@@ -1,21 +1,12 @@
1
- /**
2
- * Minimum Node.js major the native install backend supports — its prebuilt native
3
- * modules are ABI-locked to this line (kept in sync with `.nvmrc`).
4
- */
1
+ /** Minimum Node major the native install backend supports — keep in sync with `.nvmrc`. */
5
2
  export declare const MIN_NODE_MAJOR = 24;
6
3
  /** Parse the major from a `process.versions.node`-style string (`'24.18.0'` → 24). */
7
4
  export declare function parseNodeMajor(version: string): number;
8
5
  /**
9
- * The preflight verdict for the native install backend, factored out so it can be
10
- * unit-tested without spawning a foreign Node. Returns an error message when the
11
- * runtime is real Node older than {@link MIN_NODE_MAJOR}, else `null` — i.e. on a
12
- * supported Node, on GJS (which fakes `process.versions.node` and does not use
13
- * these prebuilds), or on a version string that does not parse.
6
+ * The preflight verdict, factored out so it can be unit-tested without spawning a
7
+ * foreign Node. `null` (pass) on a supported Node, under GJS, and on a version
8
+ * string that does not parse — an unparseable version must not block an install.
14
9
  */
15
10
  export declare function nativeBackendNodeError(version: string, onRealNode: boolean): string | null;
16
- /**
17
- * Guard the native install backend: throw a clear error on an unsupported Node
18
- * major instead of letting the ABI-mismatched prebuilds SIGSEGV mid-install.
19
- * No-op under GJS and on supported Node.
20
- */
11
+ /** Throw the preflight verdict as an error; no-op under GJS and on a supported Node. */
21
12
  export declare function assertNativeBackendNodeVersion(): void;
@@ -1,28 +1,22 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  // Node-version preflight for the native install backend.
3
3
  //
4
- // The native backend composes native deps whose prebuilt binaries are ABI-locked
5
- // to the Node line gjsify targets (see `.nvmrc`). On an older Node major they load
6
- // a mismatched ABI and SIGSEGV mid-install instead of failing cleanly — a
7
- // confusing crash with no actionable message (observed: Node 22 in a Fedora
8
- // container segfaults `gjsify install --backend native` during download/extract).
9
- // This preflight turns that into a clear, actionable error.
4
+ // Its prebuilt binaries are ABI-locked to the Node line gjsify targets (`.nvmrc`).
5
+ // On an older major they load a mismatched ABI and SIGSEGV mid-install instead of
6
+ // failing cleanly — measured: Node 22 in a Fedora container segfaults
7
+ // `gjsify install --backend native` during download/extract. This turns that into
8
+ // an actionable error.
10
9
  import { isNode, gjsSystemVersion } from '@gjsify/rolldown-plugin-gjsify/runtime';
11
- /**
12
- * Minimum Node.js major the native install backend supports — its prebuilt native
13
- * modules are ABI-locked to this line (kept in sync with `.nvmrc`).
14
- */
10
+ /** Minimum Node major the native install backend supports — keep in sync with `.nvmrc`. */
15
11
  export const MIN_NODE_MAJOR = 24;
16
12
  /** Parse the major from a `process.versions.node`-style string (`'24.18.0'` → 24). */
17
13
  export function parseNodeMajor(version) {
18
14
  return Number.parseInt(String(version).split('.', 1)[0] ?? '', 10);
19
15
  }
20
16
  /**
21
- * The preflight verdict for the native install backend, factored out so it can be
22
- * unit-tested without spawning a foreign Node. Returns an error message when the
23
- * runtime is real Node older than {@link MIN_NODE_MAJOR}, else `null` — i.e. on a
24
- * supported Node, on GJS (which fakes `process.versions.node` and does not use
25
- * these prebuilds), or on a version string that does not parse.
17
+ * The preflight verdict, factored out so it can be unit-tested without spawning a
18
+ * foreign Node. `null` (pass) on a supported Node, under GJS, and on a version
19
+ * string that does not parse — an unparseable version must not block an install.
26
20
  */
27
21
  export function nativeBackendNodeError(version, onRealNode) {
28
22
  if (!onRealNode)
@@ -34,21 +28,18 @@ export function nativeBackendNodeError(version, onRealNode) {
34
28
  `Its prebuilt native modules are ABI-locked to Node ${MIN_NODE_MAJOR} and crash on older majors. ` +
35
29
  `Use Node ${MIN_NODE_MAJOR} (nvm/asdf locally, actions/setup-node in CI), or run with --backend npm.`);
36
30
  }
37
- /** True when the CLI runs on real Node — NOT GJS, which fakes `process.versions.node`. */
31
+ /**
32
+ * True when the CLI runs on real Node. GJS must be ruled out FIRST: `@gjsify/process`
33
+ * fakes `process.versions.node = '20.0.0'` for compat, so reading that field would
34
+ * be a false Node positive there (and a false preflight failure, since 20 < 24).
35
+ */
38
36
  function onRealNode() {
39
- // GJS exposes `imports.system.version`; @gjsify/process fakes
40
- // `process.versions.node = '20.0.0'` for compat, so check GJS FIRST — a plain
41
- // `process.versions.node` read would be a false Node positive under GJS.
42
37
  const sysVersion = gjsSystemVersion();
43
38
  if (sysVersion !== undefined)
44
39
  return false;
45
40
  return isNode();
46
41
  }
47
- /**
48
- * Guard the native install backend: throw a clear error on an unsupported Node
49
- * major instead of letting the ABI-mismatched prebuilds SIGSEGV mid-install.
50
- * No-op under GJS and on supported Node.
51
- */
42
+ /** Throw the preflight verdict as an error; no-op under GJS and on a supported Node. */
52
43
  export function assertNativeBackendNodeVersion() {
53
44
  const message = nativeBackendNodeError(process.versions.node, onRealNode());
54
45
  if (message)
@@ -1,17 +1,13 @@
1
1
  import type { ConfigData, BundlerOptions } from '../types/config-data.js';
2
2
  export declare function normalizeBundlerOptions(configData: ConfigData): BundlerOptions;
3
3
  /**
4
- * Shallow merge with deep-merge of `output`, `transform`, and `resolve`. The
5
- * second argument wins on conflicts, matching `merge(target, ...sources)`
6
- * semantics from `@gjsify/rolldown-plugin-gjsify/utils/merge`.
4
+ * Shallow merge with deep-merge of `output`, `transform` and `resolve`; `overrides` wins,
5
+ * matching `merge(target, ...sources)` from `@gjsify/rolldown-plugin-gjsify/utils/merge`. `base`
6
+ * is the orchestrator's Rolldown-generic shape, `overrides` the user's config plus CLI flags.
7
7
  *
8
- * `base` is typically the Rolldown-generic shape returned by the orchestrator;
9
- * `overrides` is the user's `BundlerOptions` from `.gjsifyrc.js` plus CLI
10
- * flag merges. Single-output assumption matches `BundlerOptions['output']`.
11
- *
12
- * The orchestrator-side `input` is authoritative — it's the post-glob-expansion
13
- * value. Overriding it with the user's raw glob string would re-introduce
14
- * unresolved glob patterns into the final Rolldown call. Same for `external`,
15
- * which the orchestrator concatenates with platform defaults already.
8
+ * The orchestrator's `input` is authoritative because it is the post-glob-expansion value —
9
+ * taking the user's raw glob string would re-introduce unresolved patterns into the Rolldown
10
+ * call. Same for `external`, which the orchestrator has already concatenated with the platform
11
+ * defaults.
16
12
  */
17
13
  export declare function mergeBundlerOptions(base: BundlerOptions, overrides: BundlerOptions): BundlerOptions;
@@ -1,24 +1,18 @@
1
- // One-release deprecation shim: maps the legacy `esbuild?: BuildOptions`
2
- // field on `.gjsifyrc.js` / `package.json#gjsify` into the equivalent
3
- // `bundler?: RolldownOptions` shape. Logs a single warning per build.
1
+ // Deprecation shim: maps the legacy `esbuild?: BuildOptions` field on `.gjsifyrc.js` /
2
+ // `package.json#gjsify` into the equivalent `bundler?: RolldownOptions` shape, warning once per
3
+ // build. It was slated for removal in 0.5.0 and is still here at 0.35.0 — the warning text it
4
+ // prints still names 0.5.0, so removing the shim means fixing that string too.
4
5
  //
5
- // Also handles the top-level `bundler.define` alias: Rolldown reads
6
- // `transform.define`, not a top-level `define`. A user who flat-renames
7
- // `esbuild: { define: {...} }` → `bundler: { define: {...} }` puts `define`
8
- // at the top level where Rolldown silently ignores it, causing
9
- // `ReferenceError: <TOKEN> is not defined` at GJS load time. We auto-map
10
- // top-level `bundler.define` into `bundler.transform.define` and emit a
11
- // one-time warning so the user can correct their config.
12
- //
13
- // Drop `esbuild` shim in 0.5.0; the `bundler.define` alias is permanent.
6
+ // Also handles the top-level `bundler.define` alias, which is PERMANENT: Rolldown reads
7
+ // `transform.define`, so a user who flat-renames `esbuild: { define }` → `bundler: { define }`
8
+ // lands `define` at the top level where Rolldown silently ignores it, producing
9
+ // `ReferenceError: <TOKEN> is not defined` at GJS load time.
14
10
  let warnedOnce = false;
15
11
  let warnedDefineOnce = false;
16
12
  export function normalizeBundlerOptions(configData) {
17
13
  const raw = (configData.bundler ?? {});
18
- // --- Top-level bundler.define alias ------------------------------------------
19
- // Rolldown only reads `transform.define`; a top-level `define` key is silently
20
- // ignored. Detect it, warn once, and move it into `transform.define` so the
21
- // user's flat esbuild→bundler rename of `define` Just Works.
14
+ // Rolldown reads only `transform.define`, so a top-level `define` is moved there (warning
15
+ // once) rather than being silently ignored — see the header.
22
16
  let fromBundler = raw;
23
17
  if (typeof raw['define'] === 'object' &&
24
18
  raw['define'] !== null) {
@@ -49,11 +43,9 @@ export function normalizeBundlerOptions(configData) {
49
43
  "Rename it to 'bundler' (typed as RolldownOptions). See the migration notes in the gjsify CHANGELOG.");
50
44
  }
51
45
  const fromEsbuild = legacyEsbuildToRolldown(configData.esbuild);
52
- // Plain user-config merge — we deliberately do NOT call
53
- // `mergeBundlerOptions` here, because that function strips `input` and
54
- // `external` from its overrides arg (it assumes the *orchestrator* is
55
- // the override source and the user the base). Here both inputs are
56
- // user-provided config and `input` must survive the merge.
46
+ // Deliberately NOT `mergeBundlerOptions`: that strips `input`/`external` from its overrides
47
+ // arg because it assumes the ORCHESTRATOR is the override source. Here both sides are user
48
+ // config, and `input` must survive the merge.
57
49
  const out = { ...fromEsbuild, ...fromBundler };
58
50
  if (fromEsbuild.output || fromBundler.output) {
59
51
  out.output = { ...fromEsbuild.output, ...fromBundler.output };
@@ -111,33 +103,23 @@ function legacyEsbuildToRolldown(esb) {
111
103
  out.transform = transform;
112
104
  if (Object.keys(resolve).length > 0)
113
105
  out.resolve = resolve;
114
- // Discarded (handled elsewhere):
115
- // esb.inject — esbuild's array-of-side-effect-files; surfaced at the
116
- // CLI layer instead, via input expansion.
117
- // esb.loader — replaced by top-level `gjsify.loaders` (see ConfigData);
118
- // migration: `esbuild.loader: { '.png': 'dataurl', '.glsl': 'text' }`
119
- // → `loaders: { '.png': 'dataurl', '.glsl': 'text' }`.
106
+ // Discarded, handled elsewhere: `esb.inject` (surfaced at the CLI layer via input
107
+ // expansion) and `esb.loader` (replaced by top-level `gjsify.loaders`, see ConfigData).
120
108
  return out;
121
109
  }
122
110
  /**
123
- * Shallow merge with deep-merge of `output`, `transform`, and `resolve`. The
124
- * second argument wins on conflicts, matching `merge(target, ...sources)`
125
- * semantics from `@gjsify/rolldown-plugin-gjsify/utils/merge`.
126
- *
127
- * `base` is typically the Rolldown-generic shape returned by the orchestrator;
128
- * `overrides` is the user's `BundlerOptions` from `.gjsifyrc.js` plus CLI
129
- * flag merges. Single-output assumption matches `BundlerOptions['output']`.
111
+ * Shallow merge with deep-merge of `output`, `transform` and `resolve`; `overrides` wins,
112
+ * matching `merge(target, ...sources)` from `@gjsify/rolldown-plugin-gjsify/utils/merge`. `base`
113
+ * is the orchestrator's Rolldown-generic shape, `overrides` the user's config plus CLI flags.
130
114
  *
131
- * The orchestrator-side `input` is authoritative — it's the post-glob-expansion
132
- * value. Overriding it with the user's raw glob string would re-introduce
133
- * unresolved glob patterns into the final Rolldown call. Same for `external`,
134
- * which the orchestrator concatenates with platform defaults already.
115
+ * The orchestrator's `input` is authoritative because it is the post-glob-expansion value —
116
+ * taking the user's raw glob string would re-introduce unresolved patterns into the Rolldown
117
+ * call. Same for `external`, which the orchestrator has already concatenated with the platform
118
+ * defaults.
135
119
  */
136
120
  export function mergeBundlerOptions(base, overrides) {
137
- // Strip fields the orchestrator owns authoritatively — the user has
138
- // already had their say via the orchestrator's `userExternal` / input
139
- // expansion; merging the raw values back on top would clobber the
140
- // post-processing.
121
+ // Strip the fields the orchestrator owns: the user already had their say through
122
+ // `userExternal` / input expansion, and merging the raw values back would clobber it.
141
123
  const { input: _ignoredInput, external: _ignoredExternal, ...overridesRest } = overrides;
142
124
  const out = { ...base, ...overridesRest };
143
125
  if (base.output || overrides.output) {