@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,70 +1,51 @@
1
1
  // A runnable `gjsify` on PATH, for the two trees where the npm bin is not one.
2
2
  //
3
- // The CLI's npm bin (`node_modules/.bin/gjsify`) is a DISPATCHER written at
4
- // install time: it prefers `packages/infra/cli/dist/cli.gjs.mjs` on a host that
5
- // has `gjs`, and otherwise execs the Node entry `packages/infra/cli/lib/index.js`.
6
- // Multi-package orchestration and compound package scripts resolve `gjsify`
7
- // from PATH and go through it:
8
- // - `gjsify workspace`/`foreach` spawn `<pm> run <script>` per package; under
9
- // GJS the package manager is `gjsify` itself, and `spawn('gjsify', …)`
10
- // searches PATH.
11
- // - a compound script like `gjsify run a && gjsify run b` is executed via
12
- // `/bin/sh -c …` (or `%COMSPEC% /c …`), and the shell resolves each
13
- // `gjsify` from PATH.
3
+ // The CLI's npm bin (`node_modules/.bin/gjsify`) is a DISPATCHER written at install
4
+ // time: it prefers `packages/infra/cli/dist/cli.gjs.mjs` on a host with `gjs`, and
5
+ // otherwise execs the Node entry `packages/infra/cli/lib/index.js`. Orchestration
6
+ // goes through PATH to reach it — `gjsify workspace`/`foreach` spawn `<pm> run
7
+ // <script>` per package (under GJS the package manager is `gjsify` itself), and a
8
+ // compound script like `gjsify run a && gjsify run b` runs via `/bin/sh -c …`, whose
9
+ // shell resolves each `gjsify` from PATH.
14
10
  //
15
- // Fix: write a tiny wrapper that re-invokes the RUNNING CLI, and prepend its
16
- // directory to `process.env.PATH` so every child process (and grandchild, via
17
- // the inherited `GJSIFY_SHIM_DIR` marker) resolves `gjsify` to a CLI that
18
- // actually exists.
11
+ // So: write a wrapper that re-invokes the RUNNING CLI and prepend its directory to
12
+ // `process.env.PATH`, so every child (and grandchild, via the inherited
13
+ // `GJSIFY_SHIM_DIR` marker) resolves `gjsify` to a CLI that exists.
19
14
  //
20
15
  // TWO HOSTS NEED IT, FOR TWO DIFFERENT REASONS
21
16
  //
22
- // GJS — always. The npm bin is the NODE entry and is useless in a node-free
23
- // environment (the Flatpak sandbox has no Node): without the shim the failures
24
- // are `spawn npm ENOENT` and `gjsify: not found`.
17
+ // GJS — always. The npm bin is the NODE entry, useless in a node-free environment
18
+ // (the Flatpak sandbox has no Node): the failures are `spawn npm ENOENT` and
19
+ // `gjsify: not found`.
25
20
  //
26
- // Node — only when the running CLI came from OUTSIDE the workspace, which is
27
- // exactly the cold-tree bootstrap (`npx @gjsify/cli@latest …`, or a global
28
- // install). ADR 0002 untracked BOTH bundles, so on a tree that is installed but
29
- // not yet built, `node_modules/.bin/gjsify` exists and everything it dispatches
30
- // to is missing. The bin shim is then a live landmine: it resolves, and then
31
- // dies with `Cannot find module …/packages/infra/cli/lib/index.js` on the FIRST
32
- // nested `gjsify` — which for `gjsify run build:infra` is its first clause,
33
- // `gjsify workspace @gjsify/vite-plugin-blueprint build`.
34
- //
35
- // THE INCIDENT, because a rule without it gets simplified back into the bug.
36
- //
37
- // `windows-suites.yml` (added #1021) and `macos-suites.yml` (#1022) both landed
38
- // during the 2026-08-06 GitHub Actions degradation with NO checks reported on
39
- // their PRs, and neither leg had ever been green. Both ordered the cold-tree
40
- // bootstrap as `run build:infra` BEFORE `install`, which cannot work either:
41
- // `@gjsify/vite-plugin-blueprint` declares `"types": ["node"]`, so with no
42
- // `node_modules` its `gjsify tsc` fails `TS2688: Cannot find type definition
43
- // file for 'node'`. Correcting the order to install-then-build then exposed
44
- // THIS gap, one step later.
45
- //
46
- // Reproduced on Linux at de7f5525 in a cold worktree — nothing about it is
47
- // Windows-specific:
21
+ // Node — only when the running CLI came from OUTSIDE the workspace, i.e. the
22
+ // cold-tree bootstrap (`npx @gjsify/cli@latest …`, or a global install). ADR 0002
23
+ // untracked BOTH bundles, so on an installed-but-unbuilt tree
24
+ // `node_modules/.bin/gjsify` exists and everything it dispatches to is missing: it
25
+ // resolves, then dies with `Cannot find module …/lib/index.js` on the FIRST nested
26
+ // `gjsify`. Reproducible on Linux in a cold worktree, nothing Windows-specific:
48
27
  //
49
28
  // npx --yes @gjsify/cli@latest install --immutable # ok
50
29
  // npx --yes @gjsify/cli@latest run build:infra # Cannot find module …
51
30
  //
52
- // Linux CI never saw it because its bootstrap runs `gjs -m <published bundle>
53
- // run build:infra` (`.github/actions/gjsify-setup`), which takes the GJS branch
54
- // above. Windows is simply the only leg with no `gjs` to fall back on, so it is
55
- // where the gap became load-bearing.
31
+ // The bootstrap must therefore be ordered install-then-build, and the other order is
32
+ // not an alternative: `@gjsify/vite-plugin-blueprint` declares `"types": ["node"]`,
33
+ // so with no `node_modules` its `gjsify tsc` fails `TS2688: Cannot find type
34
+ // definition file for 'node'`. Linux CI never saw the gap because its bootstrap runs
35
+ // `gjs -m <published bundle> run build:infra` (`.github/actions/gjsify-setup`) and
36
+ // takes the GJS branch; Windows is the only leg with no `gjs` to fall back on.
56
37
  //
57
- // Scoped to an outside-the-workspace CLI ON PURPOSE: when the invocation
58
- // already came through the workspace's own `node_modules/.bin/gjsify`, that
59
- // shim IS the CLI the tree wants, and today's dispatch (GJS bundle first on a
60
- // host with `gjs`) stays byte-for-byte unchanged. Widening this to every Node
61
- // invocation would silently move in-repo nested builds off the GJS bundle.
62
- import { mkdtempSync, writeFileSync, chmodSync, existsSync } from 'node:fs';
38
+ // Scoped to an outside-the-workspace CLI ON PURPOSE: when the invocation already
39
+ // came through the workspace's own `node_modules/.bin/gjsify`, that shim IS the CLI
40
+ // the tree wants. Widening this to every Node invocation would silently move in-repo
41
+ // nested builds off the GJS bundle.
42
+ import { mkdtempSync, mkdirSync, writeFileSync, chmodSync, existsSync } from 'node:fs';
63
43
  import { tmpdir } from 'node:os';
64
44
  import { join, delimiter, relative, isAbsolute } from 'node:path';
65
45
  import { fileURLToPath } from 'node:url';
66
46
  import { isGjs } from '@gjsify/rolldown-plugin-gjsify/runtime';
67
47
  import { buildLauncherShims } from './bin-shim.js';
48
+ import { resolveBinOnPath } from './install-global.js';
68
49
  import { findWorkspaceRoot } from './workspace-root.js';
69
50
  let _ensured = false;
70
51
  let _selfShimActive = false;
@@ -73,20 +54,20 @@ let _selfShimActive = false;
73
54
  * a self-shim rather than through the workspace's `node_modules/.bin/gjsify`.
74
55
  *
75
56
  * `detectPackageManager()` reads this: a bootstrap CLI must run package scripts
76
- * ITSELF, because npm re-prepends `node_modules/.bin` for every lifecycle
77
- * script it starts — which puts the tree's dead shim back ahead of the shim dir
78
- * this module placed on PATH, and undoes the whole repair one level down.
57
+ * ITSELF, because npm re-prepends `node_modules/.bin` for every lifecycle script it
58
+ * starts — putting the tree's dead shim back ahead of this module's shim dir and
59
+ * undoing the repair one level down.
79
60
  */
80
61
  export function usingSelfShim() {
81
62
  return _selfShimActive;
82
63
  }
83
64
  /**
84
- * Does this invocation need its own `gjsify` on PATH, rather than the
85
- * workspace's npm bin?
65
+ * Does this invocation need its own `gjsify` on PATH, rather than the workspace's
66
+ * npm bin?
86
67
  *
87
- * Pure, and takes the host facts as PARAMETERS rather than reading them
88
- * ambiently — the same shape `platform-check.ts` documents, and what lets the
89
- * Windows-relevant branch be tested from a Linux host.
68
+ * Host facts are PARAMETERS rather than read ambiently — the shape
69
+ * `platform-check.ts` documents, and what lets the Windows branch be tested from
70
+ * a Linux host.
90
71
  *
91
72
  * @param gjs running under GJS (the npm bin is the Node entry there)
92
73
  * @param selfEntry absolute path of the CLI entry currently executing
@@ -95,9 +76,9 @@ export function usingSelfShim() {
95
76
  export function needsSelfShim(opts) {
96
77
  if (opts.gjs)
97
78
  return true;
98
- // Outside the workspace ⇒ a bootstrap CLI (npx / global install), so the
99
- // tree's own `.bin/gjsify` may well point at build outputs that do not
100
- // exist yet. Inside ⇒ the tree's CLI is the one running; leave PATH alone.
79
+ // Outside the workspace ⇒ a bootstrap CLI (npx / global install), whose tree's
80
+ // own `.bin/gjsify` may point at build outputs that do not exist yet. Inside ⇒
81
+ // the tree's CLI is the one running; leave PATH alone.
101
82
  const rel = relative(opts.workspaceRoot, opts.selfEntry);
102
83
  return rel === '' || rel.startsWith('..') || isAbsolute(rel);
103
84
  }
@@ -110,9 +91,9 @@ export function ensureGjsifyShimOnPath() {
110
91
  if (_ensured)
111
92
  return;
112
93
  _ensured = true;
113
- // A parent gjsify already created the shim — reuse it so nested
114
- // orchestration doesn't pile up temp dirs / PATH entries. The
115
- // extension-less member is written on every host, so it stays the probe.
94
+ // A parent gjsify already created the shim — reuse it so nested orchestration
95
+ // doesn't pile up temp dirs / PATH entries. The extension-less member is
96
+ // written on every host, so it stays the probe.
116
97
  const inherited = process.env.GJSIFY_SHIM_DIR;
117
98
  if (inherited && existsSync(join(inherited, 'gjsify'))) {
118
99
  _selfShimActive = true;
@@ -122,33 +103,27 @@ export function ensureGjsifyShimOnPath() {
122
103
  return;
123
104
  }
124
105
  const gjs = isGjs();
125
- // Under GJS `import.meta.url` IS the entry — the whole CLI collapses into
126
- // the single `cli.gjs.mjs` bundle, so this module's URL is it. Under Node
127
- // this module is `lib/utils/gjsify-shim.js` and the npm bin is
128
- // `lib/index.js` (package.json `bin`), one hop up.
106
+ // Under GJS the whole CLI collapses into one `cli.gjs.mjs` bundle, so
107
+ // `import.meta.url` IS the entry. Under Node this module is
108
+ // `lib/utils/gjsify-shim.js` and the npm bin is `lib/index.js`, one hop up.
129
109
  const selfEntry = gjs ? fileURLToPath(import.meta.url) : fileURLToPath(new URL('../index.js', import.meta.url));
130
110
  const workspaceRoot = findWorkspaceRoot(process.cwd()) ?? process.cwd();
131
111
  if (!needsSelfShim({ gjs, selfEntry, workspaceRoot }))
132
112
  return;
133
113
  const dir = mkdtempSync(join(tmpdir(), 'gjsify-shim-'));
134
114
  const shim = join(dir, 'gjsify');
135
- // The interpreter is named, not pathed, on every member — matching
136
- // `buildShLauncher`'s `exec gjs` / `exec node` and `buildLauncherShims`'
137
- // documented contract, so a missing interpreter fails the same way on every
138
- // OS. `process.execPath` is deliberately NOT used: the batch member cannot
139
- // quote an interpreter argument (`<interpreter> "<target>" %*`), so a path
140
- // like `C:\Program Files\nodejs\node.exe` would split, and a trio whose
141
- // members disagree about which interpreter they re-enter is worse than one
142
- // name resolved consistently.
115
+ // The interpreter is NAMED, not pathed, matching `buildShLauncher` and
116
+ // `buildLauncherShims`, so a missing one fails the same way on every OS. Not
117
+ // `process.execPath`: the batch member cannot quote an interpreter argument
118
+ // (`<interpreter> "<target>" %*`), so `C:\Program Files\nodejs\node.exe` splits.
143
119
  const interpreter = gjs ? process.env.GJS_CONSOLE || 'gjs' : 'node';
144
120
  const interpreterArgs = gjs ? ['-m'] : [];
145
121
  const argv = interpreterArgs.length > 0 ? `${interpreterArgs.join(' ')} ` : '';
146
122
  writeFileSync(shim, `#!/bin/sh\nexec "${interpreter}" ${argv}"${selfEntry}" "$@"\n`, { mode: 0o755 });
147
123
  chmodSync(shim, 0o755);
148
- // cmd.exe and pwsh cannot run the extension-less member: it is not on
149
- // PATHEXT and Windows has no shebang handling. npm's three-sibling answer
150
- // is the de-facto standard, and `buildLauncherShims` is this repo's
151
- // reviewed port of it — including the IF/ELSE ERRORLEVEL handling that
124
+ // cmd.exe and pwsh cannot run the extension-less member: not on PATHEXT, and
125
+ // Windows has no shebang handling. `buildLauncherShims` ports npm's
126
+ // three-sibling answer, including the IF/ELSE ERRORLEVEL handling that
152
127
  // `&&`/`||` chaining gets wrong.
153
128
  if (!gjs && process.platform === 'win32') {
154
129
  const { cmd, ps1 } = buildLauncherShims({ interpreter, interpreterArgs, target: selfEntry });
@@ -158,4 +133,77 @@ export function ensureGjsifyShimOnPath() {
158
133
  _selfShimActive = true;
159
134
  process.env.GJSIFY_SHIM_DIR = dir;
160
135
  process.env.PATH = dir + delimiter + (process.env.PATH ?? '');
136
+ writeNodeShim(dir, gjs, selfEntry);
137
+ }
138
+ /** Deliberately NOT the shim dir itself — see {@link nodeShimDir}. */
139
+ const NODE_SHIM_SUBDIR = 'node-shim';
140
+ /**
141
+ * Directory carrying a `node` that runs the file through
142
+ * `gjsify run --node-script`, or null when this host needs none.
143
+ *
144
+ * **It is NOT on `process.env.PATH`, and that is the whole point.** Only
145
+ * `runScript` puts it on the PATH of a PACKAGE SCRIPT's child (`commands/run.ts`).
146
+ * Prepending it globally breaks the CLI's ability to SEE a missing Node: `gjsify
147
+ * tsc`'s Node fallback spawns a real `node` for typescript's CJS entry, and
148
+ * `tests/e2e/tsc-node-fallback` asserts that on a PATH without one it fails loudly
149
+ * naming the missing interpreter — an assertion that exists because a silent exit
150
+ * 0 there once let `set-bin-mode.mjs` run against a `lib/index.js` tsc never wrote.
151
+ *
152
+ * The line the subdirectory draws: a PACKAGE SCRIPT saying `node x.mjs` means "run
153
+ * this script", which gjsify can serve; the CLI's own internals asking for `node`
154
+ * mean "I need a real Node", and lying to them turns an honest diagnosis into a
155
+ * confusing one two layers down.
156
+ */
157
+ export function nodeShimDir() {
158
+ const dir = process.env.GJSIFY_SHIM_DIR;
159
+ if (!dir)
160
+ return null;
161
+ const sub = join(dir, NODE_SHIM_SUBDIR);
162
+ return existsSync(join(sub, 'node')) ? sub : null;
163
+ }
164
+ /**
165
+ * Write a `node` that runs the file through `gjsify run --node-script`, when the
166
+ * host is GJS and there is NO REAL `node`.
167
+ *
168
+ * A SHIM AND NOT A MANIFEST REWRITE. Every `node scripts/*.mjs` in this build chain
169
+ * imports only polyfilled `node:` builtins, so the sole blocker on a Node-less host
170
+ * is GJS's ESM loader being unable to resolve `node:` for a file on disk — which
171
+ * `--node-script` solves. Spelling that flag in `package.json` cannot work: CI
172
+ * bootstraps a cold tree with `gjs -m "$GJSIFY_BOOTSTRAP" run build:infra` where
173
+ * `$GJSIFY_BOOTSTRAP` is the PREVIOUS RELEASE's CLI
174
+ * (`.github/workflows/release-cut.yml` hits the same trap), so a manifest using a
175
+ * flag the last release does not know reds every cold-cache run until a release
176
+ * ships — and the release runs that chain to build itself.
177
+ *
178
+ * The shim leaves manifests saying `node scripts/x.mjs`, so an older CLI is
179
+ * unaffected, and covers what a per-script rewrite cannot: COMPOUND scripts go
180
+ * through `/bin/sh`, which resolves `node` from PATH, as does anything they spawn.
181
+ *
182
+ * Two things keep it from shadowing a real Node: it is written only when `node`
183
+ * resolves NOWHERE, and it is reachable only from a package script's PATH (see
184
+ * {@link nodeShimDir}).
185
+ */
186
+ function writeNodeShim(dir, gjs, selfEntry) {
187
+ if (!gjs)
188
+ return;
189
+ const sub = join(dir, NODE_SHIM_SUBDIR);
190
+ if (existsSync(join(sub, 'node')))
191
+ return; // inherited from a parent gjsify
192
+ if (resolveBinOnPath('node'))
193
+ return; // a real Node is present — leave it alone
194
+ const interpreter = process.env.GJS_CONSOLE || 'gjs';
195
+ mkdirSync(sub, { recursive: true });
196
+ const shim = join(sub, 'node');
197
+ // A leading FLAG is refused rather than forwarded: `node --test x.mjs` wants
198
+ // Node's own test runner and `node -e '…'` an eval, neither of which
199
+ // `--node-script` can honour, and yargs would take `--test` FOR the script
200
+ // path (`unknown-options-as-args` turns an unknown flag into a positional).
201
+ writeFileSync(shim, '#!/bin/sh\n' +
202
+ 'case "$1" in\n' +
203
+ ' -*) echo "gjsify: this host has no node; the gjsify shim runs a SCRIPT FILE only" >&2\n' +
204
+ ' echo "gjsify: got: node $*" >&2\n' +
205
+ ' exit 127 ;;\n' +
206
+ 'esac\n' +
207
+ `exec "${interpreter}" -m "${selfEntry}" run --node-script "$@"\n`, { mode: 0o755 });
208
+ chmodSync(shim, 0o755);
161
209
  }
@@ -8,17 +8,14 @@ export interface ParsedHtmlEntry {
8
8
  scriptSrc: string;
9
9
  }
10
10
  /**
11
- * Return the `.html` path when `input` is a SINGLE html entry (a literal
12
- * `.html` string, a 1-element array of one, or a 1-key record whose value is
13
- * one) — else `null`. Multi-entry inputs and non-html entries (incl. globs
14
- * like `src/**\/*.ts`) are never treated as html. The path is returned as
15
- * authored (relative or absolute); the caller resolves + reads it.
11
+ * The `.html` path when `input` is a SINGLE html entry (a literal `.html` string, a 1-element
12
+ * array of one, or a 1-key record whose value is one) — else `null`. Multi-entry inputs and
13
+ * non-html entries are never treated as html. Returned as authored; the caller resolves + reads it.
16
14
  */
17
15
  export declare function detectHtmlEntry(input: RolldownOptions['input'] | undefined): string | null;
18
16
  /**
19
- * Find the FIRST `<script type="module" src="…">` whose `src` is a local
20
- * (non-`http(s)://`, non-`//`) path and resolve the referenced module against
21
- * the HTML file's directory. Throws when no such tag exists.
17
+ * The FIRST `<script type="module" src="…">` whose `src` is a local (non-`http(s)://`, non-`//`)
18
+ * path, with the module resolved against the HTML file's directory. Throws when there is none.
22
19
  */
23
20
  export declare function parseHtmlEntry(htmlPath: string, htmlSource: string): ParsedHtmlEntry;
24
21
  export interface EmitBrowserHtmlInput {
@@ -34,10 +31,9 @@ export interface EmitBrowserHtmlInput {
34
31
  outHtmlPath: string;
35
32
  }
36
33
  /**
37
- * Produce the emitted HTML: the original source with the entry script's `src`
38
- * rewritten to point at the built JS bundle, relative to the output HTML's own
39
- * directory (so the page is portable regardless of where `dist/` is served
40
- * from). Everything else — `<title>`, meta tags, other markup — is preserved.
34
+ * The original source with the entry script's `src` rewritten to the built JS bundle, relative
35
+ * to the output HTML's own directory so the page is portable wherever `dist/` is served from.
36
+ * All other markup is preserved.
41
37
  */
42
38
  export declare function emitBrowserHtml(input: EmitBrowserHtmlInput): string;
43
39
  /** Convenience: the emitted HTML filename for a given JS outfile (sibling `index.html`). */
@@ -1,27 +1,20 @@
1
1
  // HTML-entry support for `gjsify build --app browser`.
2
2
  //
3
- // Vite treats `index.html` as the build entry: it finds the
4
- // `<script type="module" src="...">`, bundles that module, and emits a
5
- // processed HTML with the script pointing at the built asset. `gjsify build`
6
- // only ever took a `.ts`/`.js` entry + `--outfile`, so browser apps hand-wrote
7
- // their shell page (the `write-app-html.mjs` pattern). This util brings the
8
- // Vite behaviour to the production Rolldown build.
3
+ // Brings Vite's HTML-entry behaviour to the production Rolldown build: treat `index.html` as
4
+ // the entry, find its `<script type="module" src="...">`, bundle that module, and emit a
5
+ // processed HTML whose script points at the built asset.
9
6
  //
10
- // HTML emission MUST run as a CLI post-bundle step (mirroring `applyShebang`),
11
- // NOT as a Rolldown plugin `emitFile`: the native `@gjsify/rolldown-native`
12
- // engine implements no `emitFile`, so a plugin-based emit would work on the
13
- // npm engine (Node) but throw under the GJS-bundled CLI. Parsing is regex-based
14
- // and tolerant on purpose — this replaces a ~25-line hand-written script, not a
15
- // full HTML pipeline; CSS is NOT extracted here (it travels in the JS bundle as
16
- // a string via css-as-string and is injected by the app — the unified pattern
17
- // shared with the `--app gjs` target).
7
+ // Emission MUST run as a CLI post-bundle step (mirroring `applyShebang`), NOT as a Rolldown
8
+ // plugin `emitFile`: the native `@gjsify/rolldown-native` engine implements no `emitFile`, so a
9
+ // plugin-based emit would work on the npm engine (Node) and throw under the GJS-bundled CLI.
10
+ // Parsing is regex-based and tolerant on purpose — this is a shell-page rewrite, not a full HTML
11
+ // pipeline. CSS is NOT extracted here: it travels in the JS bundle as a string via
12
+ // css-as-string and is injected by the app, the same pattern the `--app gjs` target uses.
18
13
  import { basename, dirname, isAbsolute, relative, resolve } from 'node:path';
19
14
  /**
20
- * Return the `.html` path when `input` is a SINGLE html entry (a literal
21
- * `.html` string, a 1-element array of one, or a 1-key record whose value is
22
- * one) — else `null`. Multi-entry inputs and non-html entries (incl. globs
23
- * like `src/**\/*.ts`) are never treated as html. The path is returned as
24
- * authored (relative or absolute); the caller resolves + reads it.
15
+ * The `.html` path when `input` is a SINGLE html entry (a literal `.html` string, a 1-element
16
+ * array of one, or a 1-key record whose value is one) — else `null`. Multi-entry inputs and
17
+ * non-html entries are never treated as html. Returned as authored; the caller resolves + reads it.
25
18
  */
26
19
  export function detectHtmlEntry(input) {
27
20
  if (!input)
@@ -43,9 +36,8 @@ const SCRIPT_MODULE_RE = /<script\b[^>]*\btype\s*=\s*["']module["'][^>]*>(?:<\/s
43
36
  const SRC_ATTR_RE = /\bsrc\s*=\s*["']([^"']+)["']/i;
44
37
  const EXTERNAL_SRC_RE = /^(?:https?:)?\/\//i;
45
38
  /**
46
- * Find the FIRST `<script type="module" src="…">` whose `src` is a local
47
- * (non-`http(s)://`, non-`//`) path and resolve the referenced module against
48
- * the HTML file's directory. Throws when no such tag exists.
39
+ * The FIRST `<script type="module" src="…">` whose `src` is a local (non-`http(s)://`, non-`//`)
40
+ * path, with the module resolved against the HTML file's directory. Throws when there is none.
49
41
  */
50
42
  export function parseHtmlEntry(htmlPath, htmlSource) {
51
43
  for (const match of htmlSource.matchAll(SCRIPT_MODULE_RE)) {
@@ -63,18 +55,17 @@ export function parseHtmlEntry(htmlPath, htmlSource) {
63
55
  `Add one pointing at your app entry (e.g. <script type="module" src="./main.ts">).`);
64
56
  }
65
57
  /**
66
- * Produce the emitted HTML: the original source with the entry script's `src`
67
- * rewritten to point at the built JS bundle, relative to the output HTML's own
68
- * directory (so the page is portable regardless of where `dist/` is served
69
- * from). Everything else — `<title>`, meta tags, other markup — is preserved.
58
+ * The original source with the entry script's `src` rewritten to the built JS bundle, relative
59
+ * to the output HTML's own directory so the page is portable wherever `dist/` is served from.
60
+ * All other markup is preserved.
70
61
  */
71
62
  export function emitBrowserHtml(input) {
72
63
  const { htmlSource, scriptTag, scriptSrc, jsOutPath, outHtmlPath } = input;
73
64
  let rel = relative(dirname(outHtmlPath), jsOutPath).split(/[\\/]/).join('/');
74
65
  if (!rel.startsWith('.'))
75
66
  rel = `./${rel}`;
76
- // Rewrite only the src value WITHIN the matched tag, then swap that tag
77
- // back into the source — avoids touching any other `src=` in the document.
67
+ // Rewrite the src WITHIN the matched tag, then swap that tag back in — so no other `src=`
68
+ // in the document is touched.
78
69
  const newTag = scriptTag.replace(SRC_ATTR_RE, (whole) => whole.replace(scriptSrc, rel));
79
70
  return htmlSource.replace(scriptTag, newTag);
80
71
  }