@gjsify/cli 0.36.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 (116) hide show
  1. package/dist/cli.gjs.mjs +3 -3
  2. package/lib/bundler-pick.js +61 -87
  3. package/lib/cli-fail.d.ts +4 -7
  4. package/lib/cli-fail.js +29 -55
  5. package/lib/commands/affected.js +161 -286
  6. package/lib/commands/install.d.ts +63 -66
  7. package/lib/commands/install.js +372 -536
  8. package/lib/templates/install.mjs.tmpl +38 -68
  9. package/lib/types/cli-build-options.d.ts +33 -67
  10. package/lib/types/command.d.ts +9 -12
  11. package/lib/types/config-data.d.ts +125 -221
  12. package/lib/utils/auth-npmrc.d.ts +5 -10
  13. package/lib/utils/auth-npmrc.js +11 -21
  14. package/lib/utils/clear-targets.d.ts +8 -11
  15. package/lib/utils/clear-targets.js +22 -29
  16. package/lib/utils/copy-targets.d.ts +13 -19
  17. package/lib/utils/copy-targets.js +31 -44
  18. package/lib/utils/dep-aggregation.d.ts +7 -15
  19. package/lib/utils/dep-aggregation.js +9 -22
  20. package/lib/utils/detect-native-packages.d.ts +83 -116
  21. package/lib/utils/detect-native-packages.js +111 -149
  22. package/lib/utils/dir-link.d.ts +29 -48
  23. package/lib/utils/dir-link.js +33 -56
  24. package/lib/utils/discover-showcases.d.ts +6 -7
  25. package/lib/utils/discover-showcases.js +11 -15
  26. package/lib/utils/dlx-cache.d.ts +29 -38
  27. package/lib/utils/dlx-cache.js +53 -63
  28. package/lib/utils/force-exit.d.ts +0 -3
  29. package/lib/utils/force-exit.js +13 -24
  30. package/lib/utils/gi-search-path.d.ts +16 -24
  31. package/lib/utils/gi-search-path.js +54 -82
  32. package/lib/utils/gjs-bundle-guard.d.ts +5 -13
  33. package/lib/utils/gjs-bundle-guard.js +35 -57
  34. package/lib/utils/gjs-source-escape.d.ts +2 -2
  35. package/lib/utils/gjs-source-escape.js +21 -39
  36. package/lib/utils/gjsify-shim.d.ts +17 -20
  37. package/lib/utils/gjsify-shim.js +80 -120
  38. package/lib/utils/html-entry.d.ts +8 -12
  39. package/lib/utils/html-entry.js +19 -28
  40. package/lib/utils/install-backend-native.d.ts +123 -161
  41. package/lib/utils/install-backend-native.js +536 -737
  42. package/lib/utils/install-backend.d.ts +42 -57
  43. package/lib/utils/install-backend.js +16 -21
  44. package/lib/utils/install-cache-fs.d.ts +10 -15
  45. package/lib/utils/install-cache-fs.js +18 -26
  46. package/lib/utils/install-lock.d.ts +5 -6
  47. package/lib/utils/install-lock.js +46 -65
  48. package/lib/utils/install-packument-cache.d.ts +27 -33
  49. package/lib/utils/install-packument-cache.js +44 -57
  50. package/lib/utils/install-progress.d.ts +2 -9
  51. package/lib/utils/install-progress.js +14 -39
  52. package/lib/utils/install-tarball-cache.d.ts +14 -20
  53. package/lib/utils/install-tarball-cache.js +41 -59
  54. package/lib/utils/library-output.d.ts +9 -22
  55. package/lib/utils/library-output.js +19 -37
  56. package/lib/utils/load-npmrc.d.ts +5 -6
  57. package/lib/utils/load-npmrc.js +18 -27
  58. package/lib/utils/node-bundle-guard.d.ts +4 -8
  59. package/lib/utils/node-bundle-guard.js +21 -35
  60. package/lib/utils/node-script.js +33 -54
  61. package/lib/utils/node-version.d.ts +5 -14
  62. package/lib/utils/node-version.js +15 -24
  63. package/lib/utils/normalize-bundler-options.d.ts +7 -11
  64. package/lib/utils/normalize-bundler-options.js +24 -42
  65. package/lib/utils/npm-oidc.d.ts +23 -37
  66. package/lib/utils/npm-oidc.js +28 -53
  67. package/lib/utils/npm-otp-cache.d.ts +4 -4
  68. package/lib/utils/npm-otp-cache.js +20 -25
  69. package/lib/utils/npm-otp.d.ts +20 -30
  70. package/lib/utils/npm-otp.js +32 -51
  71. package/lib/utils/onboard-probe.d.ts +7 -13
  72. package/lib/utils/onboard-probe.js +16 -27
  73. package/lib/utils/parse-spec.d.ts +3 -3
  74. package/lib/utils/parse-spec.js +4 -5
  75. package/lib/utils/pkg-json-edit.d.ts +7 -12
  76. package/lib/utils/pkg-json-edit.js +13 -26
  77. package/lib/utils/pkg-json.d.ts +11 -18
  78. package/lib/utils/pkg-json.js +19 -28
  79. package/lib/utils/prefixed-output.d.ts +3 -4
  80. package/lib/utils/prefixed-output.js +13 -19
  81. package/lib/utils/prompt.d.ts +3 -4
  82. package/lib/utils/prompt.js +14 -18
  83. package/lib/utils/publish-diagnose.d.ts +11 -16
  84. package/lib/utils/publish-diagnose.js +15 -28
  85. package/lib/utils/publish-headers.d.ts +7 -11
  86. package/lib/utils/publish-headers.js +36 -65
  87. package/lib/utils/publishable-packages.d.ts +4 -6
  88. package/lib/utils/publishable-packages.js +14 -18
  89. package/lib/utils/resolve-gjs-entry.d.ts +5 -8
  90. package/lib/utils/resolve-gjs-entry.js +10 -27
  91. package/lib/utils/resolve-npm-package.d.ts +6 -11
  92. package/lib/utils/resolve-npm-package.js +29 -63
  93. package/lib/utils/resolve-plugin-by-name.d.ts +11 -19
  94. package/lib/utils/resolve-plugin-by-name.js +9 -14
  95. package/lib/utils/run-gjs.d.ts +16 -21
  96. package/lib/utils/run-gjs.js +31 -56
  97. package/lib/utils/run-lifecycle-script.d.ts +5 -9
  98. package/lib/utils/run-lifecycle-script.js +23 -42
  99. package/lib/utils/run-node.d.ts +21 -33
  100. package/lib/utils/run-node.js +43 -64
  101. package/lib/utils/runtimes.d.ts +25 -35
  102. package/lib/utils/runtimes.js +34 -46
  103. package/lib/utils/spawn.d.ts +32 -48
  104. package/lib/utils/spawn.js +51 -84
  105. package/lib/utils/stdin.d.ts +2 -2
  106. package/lib/utils/stdin.js +10 -25
  107. package/lib/utils/suggest.d.ts +6 -7
  108. package/lib/utils/suggest.js +8 -10
  109. package/lib/utils/system-gi.d.ts +18 -27
  110. package/lib/utils/system-gi.js +60 -114
  111. package/lib/utils/trust-registry.d.ts +4 -8
  112. package/lib/utils/trust-registry.js +7 -15
  113. package/lib/utils/win32-command.d.ts +8 -14
  114. package/lib/utils/win32-command.js +29 -43
  115. package/lib/utils/workspace-root.js +7 -12
  116. package/package.json +18 -18
@@ -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);
@@ -1,43 +1,30 @@
1
1
  // `gjsify run --node-script <file>` — run an UNBUNDLED `.mjs`/`.js` that imports
2
2
  // `node:*` builtins, on whatever runtime the CLI itself is executing on.
3
3
  //
4
- // WHY THIS EXISTS
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`).
5
10
  //
6
- // A repo script like `scripts/build-assets.mjs` imports only `node:fs` /
7
- // `node:path` / `node:url` — every one of which gjsify polyfills — so nothing
8
- // about it is intrinsically Node-bound. What was missing is a way to RUN one
9
- // under GJS: GJS's ESM loader has no `node:` URI scheme and no node_modules
10
- // walker, so `gjs -m scripts/build-assets.mjs` dies on the first import. That
11
- // single gap is why a Node-less host could install gjsify, resolve every native
12
- // prebuild, and then fail `build:infra` at `node scripts/process-template.mjs`
13
- // (`status/open-todos.md`, "A Node-less host cannot bootstrap a fresh CLONE").
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
14
  //
15
- // The alternative that was considered and rejected: commit a `dist/*.gjs.mjs`
16
- // for each script. That is one more artifact per script under the committed-
17
- // bundle freshness gate, and it scales with the number of scripts. This is ONE
18
- // mechanism that serves every script in the repo and every consumer's too.
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
19
  //
20
- // WHAT IT DOES
21
- //
22
- // node / bun / deno host → spawn the file on that runtime. Byte-for-byte what
23
- // `node <file>` did before; no bundling, no cost.
24
- // gjs host → bundle the file `--app gjs` (which is what maps
25
- // `node:fs` → `@gjsify/fs`, injects the globals the
26
- // script reads, and resolves bare specifiers), then
27
- // run the bundle.
28
- //
29
- // `import.meta.url` IS THE WHOLE TRICK
30
- //
31
- // The bundle does not live where the source lives, and a script's own location
32
- // is how it finds what it operates on — all four scripts this was built for open
33
- // with `dirname(fileURLToPath(import.meta.url))` and derive their package root
34
- // from it. Left alone, `import.meta.url` in the bundle is the BUNDLE's URL, so
35
- // every one of them would write its output into the cache directory. So the
36
- // source file's URL is baked in as a compile-time define, exactly as
37
- // `config.ts`'s `loadConfigViaGjsBundle` does for a `gjsify.config.js` that
38
- // reads its own siblings. `process.argv` needs no such care: the `--app gjs`
39
- // bundle already exposes `[gjs, <bundle>, ...args]`, so `argv.slice(2)` and
40
- // `argv.includes('--flag')` behave as they do on Node.
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]`.
41
28
  import { existsSync, statSync } from 'node:fs';
42
29
  import { basename, resolve } from 'node:path';
43
30
  import { pathToFileURL } from 'node:url';
@@ -47,19 +34,14 @@ import { runRuntimeBundle } from './run-node.js';
47
34
  /**
48
35
  * Whether the on-the-fly GJS bundle may be reused across runs.
49
36
  *
50
- * OFF by default, and that is deliberate. The cache `bundleFileForGjsCached`
51
- * implements invalidates on the ENTRY file's mtime plus the project lockfile —
52
- * which covers a plugin or a config (both leaf-ish, both consuming installed
53
- * deps) but NOT a repo script, whose imports are workspace packages that change
54
- * without any lockfile write. A stale bundle there does not fail loudly: the
55
- * script runs and EMITS BUILD OUTPUT from code that no longer exists in the
56
- * tree. That is the failure this repo has paid for repeatedly under a different
57
- * name ("it never runs the source you just edited"), and a build script is the
58
- * worst place to reintroduce it.
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.
59
42
  *
60
- * The cost of being right is one bundle per invocation, and only under GJS —
61
- * node/bun/deno never bundle at all. `GJSIFY_NODE_SCRIPT_CACHE=1` opts in for a
62
- * host that would rather have the seconds back.
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.
63
45
  */
64
46
  function scriptCacheEnabled() {
65
47
  const v = process.env['GJSIFY_NODE_SCRIPT_CACHE'];
@@ -81,12 +63,10 @@ export async function runNodeScript(file, extraArgs = [], options = {}) {
81
63
  }
82
64
  const host = hostRuntime();
83
65
  if (host !== 'gjs') {
84
- // Node/bun/deno can load the file as-is; bundling would only add a step
85
- // and a way to differ from what the runtime would have done itself.
86
- // `quiet`: the echo would print `node <file>` — which is exactly what the
87
- // package.json script already reads — behind ~2 kB of native-library env
88
- // that a build script never loads. Nothing is lost by leaving it out, and
89
- // a build chain calls this a dozen times.
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.
90
70
  await runRuntimeBundle(host, scriptPath, extraArgs, { exitOnSuccess: options.exitOnSuccess, quiet: true });
91
71
  return;
92
72
  }
@@ -110,7 +90,6 @@ export async function runNodeScript(file, extraArgs = [], options = {}) {
110
90
  `workspace packages, pure-JS npm deps). (${err.message})`);
111
91
  }
112
92
  // Quiet for the same reason as the node path above; the bundle's location is
113
- // deterministic (`node_modules/.cache/gjsify/node-scripts/<name>-<hash>.mjs`)
114
- // for anyone who needs to inspect it.
93
+ // deterministic (`node_modules/.cache/gjsify/node-scripts/<name>-<hash>.mjs`).
115
94
  await runGjsBundle(bundlePath, extraArgs, { exitOnSuccess: options.exitOnSuccess, quiet: true });
116
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;