@gjsify/cli 0.23.0 → 0.24.2

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.
@@ -7,8 +7,7 @@ import { ensurePkgDir } from './dlx.js';
7
7
  import { parseSpec } from '../utils/parse-spec.js';
8
8
  import { resolveNodeEntry } from '../utils/resolve-gjs-entry.js';
9
9
  import { runRuntimeBundle } from '../utils/run-node.js';
10
- import { hostRuntime } from '@gjsify/rolldown-plugin-gjsify/runtime';
11
- import { EXAMPLE_RUNTIMES, isExampleRuntime, readDeclaredRuntimes, checkRuntimeSupported, } from '../utils/runtimes.js';
10
+ import { EXAMPLE_RUNTIMES, isExampleRuntime, readDeclaredRuntimes, checkRuntimeSupported, defaultExampleRuntime, } from '../utils/runtimes.js';
12
11
  function readCliVersion() {
13
12
  try {
14
13
  const pkgUrl = new URL('../../package.json', import.meta.url);
@@ -19,6 +18,27 @@ function readCliVersion() {
19
18
  return undefined;
20
19
  }
21
20
  }
21
+ /**
22
+ * `[gjsify <version>] ` for the banner — the CLI naming ITSELF.
23
+ *
24
+ * `showcase` pins the showcase package to the CLI's own version, so a stale CLI
25
+ * silently runs a stale showcase, and the only version in the output was the
26
+ * showcase's. That is not a hypothetical: every runner that caches an unpinned
27
+ * bin reuses whatever it resolved once (`npx @gjsify/cli` → a 0.4.x `_npx`
28
+ * entry; `deno run npm:@gjsify/cli` → whatever sits in `DENO_DIR`), so the same
29
+ * command produced 0.24.1 under bunx, 0.4.25 under npx and 0.23.0 under deno on
30
+ * ONE machine on ONE day — and the two old ones failed in ways that read as
31
+ * gjsify bugs. gjsify cannot flush somebody else's cache, but it can stop the
32
+ * output from hiding which gjsify is talking.
33
+ */
34
+ function runningAs(cliVersion) {
35
+ return cliVersion ? `[gjsify ${cliVersion}] ` : '';
36
+ }
37
+ /** Pinned invocations for the three runners that cache an unpinned bin. */
38
+ const PIN_HINT = 'A package runner reuses a CACHED copy of an unpinned bin. Force the current one:\n' +
39
+ ' npx @gjsify/cli@latest showcase <name>\n' +
40
+ ' bunx @gjsify/cli@latest showcase <name>\n' +
41
+ ' deno run -A --reload npm:@gjsify/cli@latest showcase <name>';
22
42
  export const showcaseCommand = {
23
43
  command: 'showcase [name]',
24
44
  description: 'List or run curated gjsify showcase applications.',
@@ -38,18 +58,23 @@ export const showcaseCommand = {
38
58
  default: false,
39
59
  })
40
60
  .option('runtime', {
41
- // Runtime selector, mirroring `gjsify storybook --runtime`. The
42
- // default FOLLOWS the host runtime the CLI executes in (gjs under
43
- // gjs, node under node/bun/deno). gjs runs the showcase's GJS
44
- // bundle via `gjsify dlx`; node/bun/deno run its `--app node`
45
- // bundle on that runtime. Validated against the showcase's
46
- // declared `gjsify.example.runtimes` — a GTK-only showcase
47
- // requested under node fails with a clear message, not a crash.
61
+ // Runtime selector, mirroring `gjsify storybook --runtime`. gjs
62
+ // runs the showcase's GJS bundle via `gjsify dlx`; node/bun/deno
63
+ // run its `--app node` bundle on that runtime. Validated against
64
+ // the showcase's declared `gjsify.example.runtimes` — a GTK-only
65
+ // showcase requested under node fails with a clear message, not
66
+ // a crash.
67
+ //
68
+ // NO eager yargs `default:` — it is resolved in the handler via
69
+ // `defaultExampleRuntime()` (gjs when gjs is installed, else the
70
+ // host runtime). Two reasons: yargs evaluates a `default:` while
71
+ // BUILDING the command, before we know a showcase was even
72
+ // named, and resolving it probes PATH — work that must not
73
+ // happen for `gjsify showcase` in list mode.
48
74
  type: 'string',
49
75
  choices: EXAMPLE_RUNTIMES,
50
- default: hostRuntime(),
51
- defaultDescription: 'host runtime (gjs on gjs, else node/bun/deno)',
52
- description: 'Runtime to run the showcase on: gjs | node | bun | deno (default: the host runtime). node/bun/deno run its `--app node` bundle.',
76
+ defaultDescription: 'gjs when gjs is installed, else the host runtime',
77
+ description: 'Runtime to run the showcase on: gjs | node | bun | deno (default: gjs when installed, else the host runtime). node/bun/deno run its `--app node` bundle.',
53
78
  }),
54
79
  handler: async (args) => {
55
80
  // List mode: no name given, or --list flag
@@ -116,7 +141,7 @@ export const showcaseCommand = {
116
141
  // `return` — bare `process.exit` falls through under GJS (see above).
117
142
  return process.exit(1);
118
143
  }
119
- const runtime = (args.runtime ?? hostRuntime());
144
+ const runtime = (args.runtime ?? defaultExampleRuntime());
120
145
  if (!isExampleRuntime(runtime)) {
121
146
  console.error(`Unknown --runtime "${runtime}" (expected: ${EXAMPLE_RUNTIMES.join(', ')}).`);
122
147
  // `return` — bare `process.exit` falls through under GJS (see above).
@@ -142,7 +167,7 @@ export const showcaseCommand = {
142
167
  // newer CLI assumes (the `@gjsify/http-soup-bridge` regression
143
168
  // reported against `@gjsify/cli@0.3.17`).
144
169
  const dlxSpec = cliVersion ? `${showcase.packageName}@${cliVersion}` : showcase.packageName;
145
- console.log(`Running showcase: ${showcase.name} (via gjsify dlx ${dlxSpec})\n`);
170
+ console.log(`Running showcase: ${showcase.name} ${runningAs(cliVersion)}(via gjsify dlx ${dlxSpec})\n`);
146
171
  // Dispatch `dlx` IN-PROCESS (not `spawn(process.execPath, [cliBin, …])`):
147
172
  // under the committed GJS bundle `process.execPath` is `gjs` and
148
173
  // `../index.js` is the Node entry, so the spawn ran `gjs cli.gjs.mjs
@@ -165,7 +190,7 @@ export const showcaseCommand = {
165
190
  * in-tree build), falling back to `gjsify dlx`'s install-into-cache for a
166
191
  * published showcase. Returns the directory containing the package.json.
167
192
  */
168
- async function resolveShowcaseDir(showcase, cliVersion) {
193
+ async function resolveShowcaseDir(showcase, cliVersion, extraSpecs = []) {
169
194
  // Local-first: a workspace showcase resolves via its own package name.
170
195
  try {
171
196
  const req = createRequire(import.meta.url);
@@ -181,6 +206,7 @@ async function resolveShowcaseDir(showcase, cliVersion) {
181
206
  verbose: false,
182
207
  cacheMaxAge: 60 * 24 * 7,
183
208
  frozen: false,
209
+ extraSpecs,
184
210
  });
185
211
  return pkgDir;
186
212
  }
@@ -193,7 +219,16 @@ async function resolveShowcaseDir(showcase, cliVersion) {
193
219
  async function runShowcaseOnRuntime(showcase, runtime, cliVersion) {
194
220
  let pkgDir;
195
221
  try {
196
- pkgDir = await resolveShowcaseDir(showcase, cliVersion);
222
+ // `@gjsify/node-gi` is the `gi://` bridge the `--app node` bundle
223
+ // resolves at RUNTIME. Showcases carry it as a devDependency (it is a
224
+ // build input for them), so a dlx tree — the package plus its own
225
+ // `dependencies` — does not contain it and the launch died with "add
226
+ // @gjsify/node-gi as a dependency": advice the user cannot act on,
227
+ // since the tree is the CLI's cache. The launcher knows the runtime, so
228
+ // it supplies the bridge. Pinned to the CLI's version like the showcase
229
+ // itself, so the bridge and the bundle move together.
230
+ const nodeGiSpec = cliVersion ? `@gjsify/node-gi@${cliVersion}` : '@gjsify/node-gi';
231
+ pkgDir = await resolveShowcaseDir(showcase, cliVersion, [nodeGiSpec]);
197
232
  }
198
233
  catch (err) {
199
234
  console.error(`Could not resolve showcase "${showcase.name}": ${err.message}`);
@@ -218,10 +253,19 @@ async function runShowcaseOnRuntime(showcase, runtime, cliVersion) {
218
253
  entry = resolveNodeEntry(pkgDir);
219
254
  }
220
255
  catch (err) {
221
- console.error(`Cannot run showcase "${showcase.name}" on ${runtime}: ${err.message}`);
256
+ // The showcase promised this runtime and did not ship the bundle. Two
257
+ // causes, and the user can only act on one of them, so name both: the
258
+ // package version is genuinely broken (fixed by a newer one), or THIS
259
+ // CLI is a cached old copy that pinned a genuinely broken old showcase.
260
+ const version = typeof pkg.name === 'string' ? `${pkg.name}@${cliVersion ?? '?'}` : showcase.packageName;
261
+ console.error(`Cannot run showcase "${showcase.name}" on ${runtime}: ${err.message}\n\n` +
262
+ ` ${version} declares --runtime ${runtime} but ships no \`--app node\` bundle, so that\n` +
263
+ ` version of the showcase cannot run there. You are running gjsify ${cliVersion ?? '(unknown)'},\n` +
264
+ ` and \`showcase\` pins the showcase to the CLI's own version.\n\n ${PIN_HINT}\n\n` +
265
+ ` Or run it on GJS, which every showcase ships: gjsify showcase ${showcase.name} --runtime gjs`);
222
266
  return process.exit(1);
223
267
  }
224
- console.log(`Running showcase: ${showcase.name} on ${runtime} (${entry})\n`);
268
+ console.log(`Running showcase: ${showcase.name} ${runningAs(cliVersion)}on ${runtime} (${entry})\n`);
225
269
  // Terminal call — exit on success.
226
270
  await runRuntimeBundle(runtime, entry, [], { exitOnSuccess: true });
227
271
  }
@@ -181,9 +181,12 @@ export const storybookCommand = {
181
181
  // typelib env wired like `gjsify run`. All three need @gjsify/node-gi
182
182
  // (the storybook uses gi://), kept external by the bundler.
183
183
  const { computeNativeEnvForBundle } = await import('../utils/run-gjs.js');
184
- const { resolveNodeGi, nodeBinary } = await import('../utils/run-node.js');
184
+ const { resolveNodeGiForBundle, nodeBinary } = await import('../utils/run-node.js');
185
185
  const { RUNTIMES } = await import('../utils/runtimes.js');
186
- if (!resolveNodeGi(cwd)) {
186
+ // Same resolution base as `runRuntimeBundle`: the bundle's own dir
187
+ // first (that is where the runtime resolves the external specifier
188
+ // from), cwd as the fallback.
189
+ if (!resolveNodeGiForBundle(outPath, cwd)) {
187
190
  throw new Error(`Cannot run the storybook on ${runtime}: \`@gjsify/node-gi\` is not installed. ` +
188
191
  `Add @gjsify/node-gi as a dependency to run the storybook on ${runtime}.`);
189
192
  }
@@ -64,3 +64,52 @@ export declare function buildLauncherShims(opts: {
64
64
  cmd: string;
65
65
  ps1: string;
66
66
  };
67
+ /**
68
+ * The `sh` preamble a GJS launcher needs so `imports.gi.X` resolves against the
69
+ * `@gjsify/*` native prebuilds installed next to it.
70
+ *
71
+ * WHY IT SCANS AT RUN TIME INSTEAD OF BAKING A LIST
72
+ *
73
+ * The typelib/library lookup happens inside the GJS runtime, before a single
74
+ * line of the CLI bundle runs, so the env MUST be set by the launcher — the CLI
75
+ * cannot repair it from the inside. The launcher used to bake the directories
76
+ * `detectNativePackages()` found at INSTALL time, which makes the env a SNAPSHOT
77
+ * of one moment:
78
+ *
79
+ * - a native package installed into the same prefix LATER (`gjsify install -g
80
+ * @gjsify/<x>-native`, a project `gjsify install` that adds one) is invisible
81
+ * until something happens to rewrite the launcher;
82
+ * - and if the scan comes back empty for any reason, the launcher is written
83
+ * with NO preamble at all and nothing says so. The failure then surfaces
84
+ * much later and somewhere else entirely — `gjsify build` dying with "no
85
+ * usable bundler engine under GJS" in an unrelated project, which reads as a
86
+ * broken install rather than a stale launcher. That is what shipped in
87
+ * v0.24.1: a `~/.local/bin/gjsify` with a bare `exec gjs -m …` line while
88
+ * `@gjsify/rolldown-native` sat correctly installed in the prefix.
89
+ *
90
+ * Deriving the env from disk on every launch removes both: the launcher states
91
+ * WHERE to look rather than WHAT was there, so it cannot go stale, and an empty
92
+ * result is a fact about the current prefix instead of a silently frozen one.
93
+ * The cost is a handful of shell globs per launch (they are builtins, and a
94
+ * pattern that matches nothing stays literal and is rejected by `[ -d ]`).
95
+ *
96
+ * `bakedDirs` covers what a run-time scan of `scanRoot` structurally cannot see:
97
+ * `detectNativePackages()` walks UP the directory chain, so in a hoisted layout
98
+ * a prebuild may live in an ancestor's `node_modules`. Those are still embedded
99
+ * verbatim; only the primary root is dynamic. Duplicates are harmless (both
100
+ * variables are search paths).
101
+ *
102
+ * Windows is deliberately excluded: there the `sh` file is only reachable from
103
+ * git-bash/MSYS, the real launchers are the `.cmd`/`.ps1` companions built by
104
+ * {@link buildLauncherShims} from the same baked list, and `PATH` doubles as the
105
+ * DLL search path with `;` separators. It keeps the snapshot behaviour.
106
+ *
107
+ * @param scanRoot Directory whose `node_modules` is globbed at launch time.
108
+ * @param bakedDirs Prebuild dirs found at write time; entries outside `scanRoot`
109
+ * are embedded (entries inside it are dropped — the scan finds them).
110
+ * @returns the preamble, or `''` when there is nothing to export.
111
+ */
112
+ export declare function buildNativeEnvPreamble(scanRoot: string, bakedDirs?: readonly string[], target?: {
113
+ platform?: string;
114
+ arch?: string;
115
+ }): string;
@@ -24,6 +24,7 @@
24
24
  // All three re-derive the interpreter from the target's shebang, so a bin
25
25
  // declaring `#!/usr/bin/env node` and one declaring `#!/usr/bin/env -S gjs -m`
26
26
  // both work without the caller knowing which it is.
27
+ import { libraryPathVar, prebuildDirCandidates } from './detect-native-packages.js';
27
28
  // Matches `#!<prog> <args>` and `#!/usr/bin/env [-S] [K=V ...] <prog> <args>`.
28
29
  // Verbatim from cmd-shim's `shebangExpr`.
29
30
  const SHEBANG_RE = /^#!\s*(?:\/usr\/bin\/env\s+(?:-S\s+)?((?:[^ \t=]+=[^ \t=]+\s+)*))?([^ \t]+)(.*)$/;
@@ -244,3 +245,107 @@ export function buildLauncherShims(opts) {
244
245
  'exit $LASTEXITCODE\n';
245
246
  return { cmd, ps1 };
246
247
  }
248
+ /** POSIX `sh` single-quoting — the one place the escape lives. */
249
+ function shQuote(s) {
250
+ return `'${s.replace(/'/g, `'\\''`)}'`;
251
+ }
252
+ /**
253
+ * The `sh` preamble a GJS launcher needs so `imports.gi.X` resolves against the
254
+ * `@gjsify/*` native prebuilds installed next to it.
255
+ *
256
+ * WHY IT SCANS AT RUN TIME INSTEAD OF BAKING A LIST
257
+ *
258
+ * The typelib/library lookup happens inside the GJS runtime, before a single
259
+ * line of the CLI bundle runs, so the env MUST be set by the launcher — the CLI
260
+ * cannot repair it from the inside. The launcher used to bake the directories
261
+ * `detectNativePackages()` found at INSTALL time, which makes the env a SNAPSHOT
262
+ * of one moment:
263
+ *
264
+ * - a native package installed into the same prefix LATER (`gjsify install -g
265
+ * @gjsify/<x>-native`, a project `gjsify install` that adds one) is invisible
266
+ * until something happens to rewrite the launcher;
267
+ * - and if the scan comes back empty for any reason, the launcher is written
268
+ * with NO preamble at all and nothing says so. The failure then surfaces
269
+ * much later and somewhere else entirely — `gjsify build` dying with "no
270
+ * usable bundler engine under GJS" in an unrelated project, which reads as a
271
+ * broken install rather than a stale launcher. That is what shipped in
272
+ * v0.24.1: a `~/.local/bin/gjsify` with a bare `exec gjs -m …` line while
273
+ * `@gjsify/rolldown-native` sat correctly installed in the prefix.
274
+ *
275
+ * Deriving the env from disk on every launch removes both: the launcher states
276
+ * WHERE to look rather than WHAT was there, so it cannot go stale, and an empty
277
+ * result is a fact about the current prefix instead of a silently frozen one.
278
+ * The cost is a handful of shell globs per launch (they are builtins, and a
279
+ * pattern that matches nothing stays literal and is rejected by `[ -d ]`).
280
+ *
281
+ * `bakedDirs` covers what a run-time scan of `scanRoot` structurally cannot see:
282
+ * `detectNativePackages()` walks UP the directory chain, so in a hoisted layout
283
+ * a prebuild may live in an ancestor's `node_modules`. Those are still embedded
284
+ * verbatim; only the primary root is dynamic. Duplicates are harmless (both
285
+ * variables are search paths).
286
+ *
287
+ * Windows is deliberately excluded: there the `sh` file is only reachable from
288
+ * git-bash/MSYS, the real launchers are the `.cmd`/`.ps1` companions built by
289
+ * {@link buildLauncherShims} from the same baked list, and `PATH` doubles as the
290
+ * DLL search path with `;` separators. It keeps the snapshot behaviour.
291
+ *
292
+ * @param scanRoot Directory whose `node_modules` is globbed at launch time.
293
+ * @param bakedDirs Prebuild dirs found at write time; entries outside `scanRoot`
294
+ * are embedded (entries inside it are dropped — the scan finds them).
295
+ * @returns the preamble, or `''` when there is nothing to export.
296
+ */
297
+ export function buildNativeEnvPreamble(scanRoot, bakedDirs = [], target = {}) {
298
+ const platform = target.platform ?? process.platform;
299
+ const arch = target.arch ?? process.arch;
300
+ const { name: libVar, separator } = libraryPathVar(platform);
301
+ if (platform === 'win32') {
302
+ if (bakedDirs.length === 0)
303
+ return '';
304
+ const joined = shQuote(bakedDirs.join(separator));
305
+ return (`GI_TYPELIB_PATH=${joined}\${GI_TYPELIB_PATH:+"${separator}$GI_TYPELIB_PATH"}\n` +
306
+ `${libVar}=${joined}\${${libVar}:+"${separator}$${libVar}"}\n` +
307
+ `export GI_TYPELIB_PATH ${libVar}\n`);
308
+ }
309
+ // Only the ancestors' hits need embedding — anything under `scanRoot` is
310
+ // what the loop below finds, and re-embedding it would reintroduce the
311
+ // snapshot for exactly the packages the scan exists to keep current.
312
+ const nmRoot = `${scanRoot.replace(/\/+$/, '')}/node_modules/`;
313
+ const external = bakedDirs.filter((d) => !d.startsWith(nmRoot));
314
+ // The same `<os>-<arch>` spellings `resolvePrebuildDirName()` probes, in the
315
+ // same order: the canonical `${platform}-${arch}` plus the legacy uname one
316
+ // a tarball published before the rename still ships. Passing no declared
317
+ // platforms is correct here — the shell cannot read a package.json, and a
318
+ // package declaring ONLY a non-canonical spelling of this host has not
319
+ // existed since the audit made that state impossible.
320
+ const candidates = prebuildDirCandidates(platform, arch);
321
+ const patterns = [];
322
+ for (const candidate of candidates) {
323
+ // Scoped (`@gjsify/x`) and unscoped packages, matching `scanNodeModules`.
324
+ patterns.push(`${shQuote(scanRoot)}/node_modules/@*/*/prebuilds/${candidate}`);
325
+ patterns.push(`${shQuote(scanRoot)}/node_modules/*/prebuilds/${candidate}`);
326
+ }
327
+ const seed = external.length > 0 ? shQuote(external.join(separator)) : `''`;
328
+ // A directory match is not enough: `prebuilds/<os>-<arch>/` is ALSO the
329
+ // prebuildify convention, so a plain glob sweeps in `bare-fs`, `bare-os`
330
+ // and friends — packages with no typelib at all, which `detectNativePackages`
331
+ // never returned because it keyed on `gjsify.prebuilds`. Onto
332
+ // GI_TYPELIB_PATH that is merely noise; onto the LOADER path it is a
333
+ // directory of foreign shared objects ahead of the system ones. The
334
+ // `.typelib` probe restores the old precision from the filesystem alone —
335
+ // and it is exactly what the artifact invariant already guarantees every
336
+ // gjsify GI bridge ships (a library AND the typelib without which
337
+ // GI_TYPELIB_PATH resolves nothing).
338
+ return (`gjsify_np=${seed}\n` +
339
+ `for gjsify_d in ${patterns.join(' ')}; do\n` +
340
+ ` [ -d "$gjsify_d" ] || continue\n` +
341
+ ` for gjsify_t in "$gjsify_d"/*.typelib; do\n` +
342
+ ` if [ -f "$gjsify_t" ]; then gjsify_np="\${gjsify_np:+$gjsify_np${separator}}$gjsify_d"; break; fi\n` +
343
+ ` done\n` +
344
+ `done\n` +
345
+ `if [ -n "$gjsify_np" ]; then\n` +
346
+ ` GI_TYPELIB_PATH="$gjsify_np\${GI_TYPELIB_PATH:+${separator}$GI_TYPELIB_PATH}"\n` +
347
+ ` ${libVar}="$gjsify_np\${${libVar}:+${separator}$${libVar}}"\n` +
348
+ ` export GI_TYPELIB_PATH ${libVar}\n` +
349
+ `fi\n` +
350
+ `unset gjsify_np gjsify_d gjsify_t\n`);
351
+ }
@@ -1,7 +1,7 @@
1
1
  export interface NativePackage {
2
2
  /** npm package name, e.g. "@gjsify/webgl" */
3
3
  name: string;
4
- /** Absolute path to the arch-specific prebuilds dir, e.g. "/…/@gjsify/webgl/prebuilds/linux-x86_64" */
4
+ /** Absolute path to the arch-specific prebuilds dir, e.g. "/…/@gjsify/webgl/prebuilds/linux-x64" */
5
5
  prebuildsDir: string;
6
6
  }
7
7
  /** The host target a resolution is performed for. Defaults to the running process. */
@@ -31,34 +31,39 @@ export interface NativeEnv {
31
31
  [key: string]: string | undefined;
32
32
  }
33
33
  /**
34
- * Canonical `<os>-<arch>` form so `linux-x64` and `linux-x86_64` — or
35
- * `darwin-arm64` and `darwin-aarch64` — compare equal. Only the *arch* half is
36
- * normalised; the OS half is always a `process.platform` token
37
- * (`linux`/`darwin`/`win32`), which has no competing spelling.
34
+ * Canonical `<os>-<arch>` form — the node spelling — so `linux-x86_64` and
35
+ * `linux-x64` compare equal. Only the *arch* half is normalised; the OS half
36
+ * is always a `process.platform` token (`linux`/`darwin`/`win32`), which has
37
+ * no competing spelling.
38
38
  */
39
39
  export declare function canonicalPlatformToken(token: string): string;
40
40
  /**
41
41
  * The prebuild directory names to probe for a host, most-specific first.
42
42
  *
43
- * There are two live spellings in this repo and neither can be declared the
44
- * winner:
43
+ * The canonical name is `${process.platform}-${process.arch}` — `linux-x64`,
44
+ * `linux-arm64`, `darwin-arm64`, `win32-x64`. Every package in the release
45
+ * train declares it (`gjsify.platforms`), `scripts/stage-prebuild.mjs` is the
46
+ * only thing that creates it, and `scripts/audit-runtimes.mjs --check` rejects
47
+ * any other spelling, so the three can no longer drift.
45
48
  *
46
- * * the Vala/meson bridges stage `prebuilds/linux-x86_64/` — uname-style,
47
- * because the meson jobs name the directory after `uname -m`;
48
- * * `@gjsify/node-gi` (and `@gjsify/napi`'s prebuild staging) use
49
- * `prebuilds/${process.platform}-${process.arch}/` — node-style
50
- * (`linux-x64`, `darwin-arm64`, `win32-x64`).
49
+ * Two probes remain BEHIND that canonical name, and both are backward
50
+ * compatibility with tarballs already on npm — not a second convention:
51
51
  *
52
- * So we probe both, and — when the package declares `gjsify.platforms` — we
53
- * probe the package's OWN spelling first. That is strictly better than
54
- * guessing: a package that declares `darwin-arm64` tells us the exact
55
- * directory name it stages, whatever convention it picked.
52
+ * 1. **The package's own declared spelling.** A tarball published before the
53
+ * rename declares (and ships) `linux-x86_64`; probing its declaration
54
+ * first loads it without the CLI having to guess. This is also what makes
55
+ * the rename work in the OTHER direction — an older CLI resolving a newer
56
+ * package — because `canonicalPlatformToken` folds both spellings onto
57
+ * one form at both ends.
58
+ * 2. **The legacy uname spelling.** Only reachable for a package that ships
59
+ * a prebuild dir and declares NO `gjsify.platforms` at all — i.e. a
60
+ * tarball predating the OS-axis audit, or a third-party package using
61
+ * `gjsify.prebuilds` without the declaration. Inside this repo the audit
62
+ * makes that state impossible.
56
63
  *
57
- * Ordering of the generic fallbacks is per-OS rather than fixed, so the first
58
- * probe is the spelling that platform's toolchain actually produces:
59
- * uname-style on Linux (`linux-x86_64`), node-style on macOS/Windows
60
- * (`darwin-arm64`, `win32-x64` — on Apple silicon `uname -m` also reports
61
- * `arm64`, so there node-style *is* the native spelling).
64
+ * Both are pure array entries matched against an already-read directory
65
+ * listing — no extra I/O — so tolerance on the READ side costs nothing, while
66
+ * the single spelling is enforced everywhere a name is WRITTEN.
62
67
  *
63
68
  * @param platform `process.platform` value.
64
69
  * @param arch `process.arch` value.
@@ -22,33 +22,42 @@ import { readdirSync, existsSync } from 'node:fs';
22
22
  import { join, resolve } from 'node:path';
23
23
  import { readPackageJson } from './pkg-json.js';
24
24
  /**
25
- * Map a Node `process.arch` value to the uname-style spelling used by the
26
- * Vala/meson bridges, which stage into `prebuilds/linux-x86_64/` (the output
27
- * of `uname -m` on the build host).
25
+ * Map a Node `process.arch` value to the uname-style spelling the Vala/meson
26
+ * bridges used to stage into (`prebuilds/linux-x86_64/`, the output of
27
+ * `uname -m` on the build host).
28
+ *
29
+ * LEGACY. Every `@gjsify/*` package now declares and stages the node spelling
30
+ * (`linux-x64`); this table exists only so the CLI can still load a prebuild
31
+ * out of a tarball published before the rename. See
32
+ * {@link prebuildDirCandidates}.
28
33
  */
29
- const NODE_ARCH_TO_UNAME = {
34
+ const NODE_ARCH_TO_LEGACY_UNAME = {
30
35
  x64: 'x86_64',
31
36
  arm64: 'aarch64',
32
37
  arm: 'armv7',
33
38
  ia32: 'i686',
34
39
  };
35
40
  /**
36
- * Arch spellings that name the same target. Mirrors `ARCH_ALIASES` in
37
- * `scripts/audit-runtimes.mjs` — that script canonicalises
38
- * `package.json#gjsify.platforms` the same way for the platform-support
39
- * matrix, and the two MUST agree or a package could pass the audit while the
40
- * CLI fails to find its artifact.
41
+ * Arch spellings that name the same target, folded onto the NODE spelling.
42
+ * Mirrors `ARCH_ALIASES` in `scripts/audit-runtimes.mjs` — that script
43
+ * canonicalises `package.json#gjsify.platforms` the same way for the
44
+ * platform-support matrix, and the two MUST agree or a package could pass the
45
+ * audit while the CLI fails to find its artifact.
46
+ *
47
+ * The direction matters: `${process.platform}-${process.arch}` is what a
48
+ * running process can compute about itself, so folding *onto* it means the
49
+ * host identity never has to be translated in the hot path.
41
50
  */
42
51
  const ARCH_ALIASES = {
43
- x64: 'x86_64',
44
- amd64: 'x86_64',
45
- arm64: 'aarch64',
52
+ x86_64: 'x64',
53
+ amd64: 'x64',
54
+ aarch64: 'arm64',
46
55
  };
47
56
  /**
48
- * Canonical `<os>-<arch>` form so `linux-x64` and `linux-x86_64` — or
49
- * `darwin-arm64` and `darwin-aarch64` — compare equal. Only the *arch* half is
50
- * normalised; the OS half is always a `process.platform` token
51
- * (`linux`/`darwin`/`win32`), which has no competing spelling.
57
+ * Canonical `<os>-<arch>` form — the node spelling — so `linux-x86_64` and
58
+ * `linux-x64` compare equal. Only the *arch* half is normalised; the OS half
59
+ * is always a `process.platform` token (`linux`/`darwin`/`win32`), which has
60
+ * no competing spelling.
52
61
  */
53
62
  export function canonicalPlatformToken(token) {
54
63
  const dash = token.indexOf('-');
@@ -61,36 +70,38 @@ export function canonicalPlatformToken(token) {
61
70
  /**
62
71
  * The prebuild directory names to probe for a host, most-specific first.
63
72
  *
64
- * There are two live spellings in this repo and neither can be declared the
65
- * winner:
73
+ * The canonical name is `${process.platform}-${process.arch}` — `linux-x64`,
74
+ * `linux-arm64`, `darwin-arm64`, `win32-x64`. Every package in the release
75
+ * train declares it (`gjsify.platforms`), `scripts/stage-prebuild.mjs` is the
76
+ * only thing that creates it, and `scripts/audit-runtimes.mjs --check` rejects
77
+ * any other spelling, so the three can no longer drift.
66
78
  *
67
- * * the Vala/meson bridges stage `prebuilds/linux-x86_64/` — uname-style,
68
- * because the meson jobs name the directory after `uname -m`;
69
- * * `@gjsify/node-gi` (and `@gjsify/napi`'s prebuild staging) use
70
- * `prebuilds/${process.platform}-${process.arch}/` — node-style
71
- * (`linux-x64`, `darwin-arm64`, `win32-x64`).
79
+ * Two probes remain BEHIND that canonical name, and both are backward
80
+ * compatibility with tarballs already on npm — not a second convention:
72
81
  *
73
- * So we probe both, and — when the package declares `gjsify.platforms` — we
74
- * probe the package's OWN spelling first. That is strictly better than
75
- * guessing: a package that declares `darwin-arm64` tells us the exact
76
- * directory name it stages, whatever convention it picked.
82
+ * 1. **The package's own declared spelling.** A tarball published before the
83
+ * rename declares (and ships) `linux-x86_64`; probing its declaration
84
+ * first loads it without the CLI having to guess. This is also what makes
85
+ * the rename work in the OTHER direction — an older CLI resolving a newer
86
+ * package — because `canonicalPlatformToken` folds both spellings onto
87
+ * one form at both ends.
88
+ * 2. **The legacy uname spelling.** Only reachable for a package that ships
89
+ * a prebuild dir and declares NO `gjsify.platforms` at all — i.e. a
90
+ * tarball predating the OS-axis audit, or a third-party package using
91
+ * `gjsify.prebuilds` without the declaration. Inside this repo the audit
92
+ * makes that state impossible.
77
93
  *
78
- * Ordering of the generic fallbacks is per-OS rather than fixed, so the first
79
- * probe is the spelling that platform's toolchain actually produces:
80
- * uname-style on Linux (`linux-x86_64`), node-style on macOS/Windows
81
- * (`darwin-arm64`, `win32-x64` — on Apple silicon `uname -m` also reports
82
- * `arm64`, so there node-style *is* the native spelling).
94
+ * Both are pure array entries matched against an already-read directory
95
+ * listing — no extra I/O — so tolerance on the READ side costs nothing, while
96
+ * the single spelling is enforced everywhere a name is WRITTEN.
83
97
  *
84
98
  * @param platform `process.platform` value.
85
99
  * @param arch `process.arch` value.
86
100
  * @param declaredPlatforms `package.json#gjsify.platforms`, when present.
87
101
  */
88
102
  export function prebuildDirCandidates(platform, arch, declaredPlatforms) {
89
- const unameArch = NODE_ARCH_TO_UNAME[arch] ?? arch;
90
- const preferred = platform === 'linux' ? unameArch : arch;
91
- const alternate = platform === 'linux' ? arch : unameArch;
92
- const generic = [`${platform}-${preferred}`, `${platform}-${alternate}`];
93
- const hostCanonical = canonicalPlatformToken(generic[0]);
103
+ const canonical = `${platform}-${arch}`;
104
+ const legacyArch = NODE_ARCH_TO_LEGACY_UNAME[arch] ?? arch;
94
105
  const out = [];
95
106
  const push = (name) => {
96
107
  if (!out.includes(name))
@@ -100,12 +111,13 @@ export function prebuildDirCandidates(platform, arch, declaredPlatforms) {
100
111
  for (const declared of declaredPlatforms ?? []) {
101
112
  if (typeof declared !== 'string')
102
113
  continue;
103
- if (canonicalPlatformToken(declared) === hostCanonical)
114
+ if (canonicalPlatformToken(declared) === canonical)
104
115
  push(declared);
105
116
  }
106
- // 2. The two generic spellings.
107
- for (const name of generic)
108
- push(name);
117
+ // 2. The canonical node spelling — what everything in the train stages.
118
+ push(canonical);
119
+ // 3. The legacy uname spelling, for undeclared pre-rename tarballs.
120
+ push(`${platform}-${legacyArch}`);
109
121
  return out;
110
122
  }
111
123
  /**
@@ -0,0 +1,32 @@
1
+ /** The one bundler-output field this guard needs, from either engine. */
2
+ export interface GuardedChunk {
3
+ /** Emitted chunk name, for the error message. */
4
+ fileName?: string;
5
+ /** External specifiers the chunk STATICALLY imports (rollup/rolldown contract). */
6
+ imports?: readonly string[];
7
+ }
8
+ /**
9
+ * Bare `node:` specifiers a `--app gjs` bundle may keep as static imports.
10
+ *
11
+ * Deliberately EMPTY. Every Node builtin has an `@gjsify/*` polyfill that
12
+ * `--app gjs` aliases to, so there is no specifier that both survives into the
13
+ * module graph and loads under GJS. Reading the graph instead of the text is
14
+ * what keeps this list empty — the entries an earlier text-scanning version
15
+ * needed (`node:module`, `node:process`) were quoted lookalikes, never imports.
16
+ *
17
+ * Add an entry ONLY with a comment naming why GJS can resolve it. Each one is a
18
+ * specifier this guard can no longer catch for real.
19
+ */
20
+ export declare const GJS_ALLOWED_NODE_IMPORTS: readonly string[];
21
+ /**
22
+ * The distinct bare `node:` specifiers `chunks` statically import, minus the
23
+ * allowlist. Sorted, so the error message and any test are stable.
24
+ */
25
+ export declare function findDisallowedNodeImports(chunks: readonly GuardedChunk[], allowed?: readonly string[]): string[];
26
+ /**
27
+ * Throw when a built `--app gjs` bundle statically imports Node builtins.
28
+ *
29
+ * @param chunks the emitted chunks, as reported by the bundler
30
+ * @param outfile path of the bundle, for the error message
31
+ */
32
+ export declare function assertGjsBundleLoadable(chunks: readonly GuardedChunk[], outfile: string): void;