@gjsify/cli 0.23.0 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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;
@@ -0,0 +1,103 @@
1
+ // Build-time guard: a `--app gjs` bundle must not keep bare `node:` imports.
2
+ //
3
+ // Stock GJS has NO `node:` module loader. A single surviving
4
+ // `import … from "node:fs"` makes it abort the ENTIRE module graph before one
5
+ // line executes:
6
+ //
7
+ // Gjs-WARNING **: JS ERROR: ImportError: Unsupported URI scheme for importing: node
8
+ // Gjs-CRITICAL **: Failed to resolve imports for module: '…/dist/cli.gjs.mjs'
9
+ //
10
+ // `--app gjs` maps every Node builtin to its `@gjsify/*` polyfill, so a healthy
11
+ // bundle has ZERO of them. But when an alias TARGET cannot be resolved — the
12
+ // classic case being a workspace checkout whose `@gjsify/*` packages have no
13
+ // built `lib/`, so `exports["."]` points at a file that does not exist — the
14
+ // bundler falls back to treating the import as external and re-emits the
15
+ // ORIGINAL `node:*` specifier. That is a WARNING, not an error: the build
16
+ // "succeeds" and writes a bundle that cannot load anywhere. It cost three red
17
+ // CI jobs to notice, because nothing between `gjsify build` and CI ever
18
+ // executes the emitted GJS bundle (tsc, lint, format and the unit/e2e suites
19
+ // all run the Node entry).
20
+ //
21
+ // The pre-commit hook makes it worse rather than better: it rebuilds and
22
+ // AUTO-STAGES `dist/cli.gjs.mjs`, so a developer whose checkout cannot resolve
23
+ // the polyfills commits a broken artifact without ever seeing it.
24
+ //
25
+ // So the refusal belongs at build time, in `gjsify build --app gjs` itself —
26
+ // not in a git hook and not in one package's `build:gjs-bundle` script. The
27
+ // invariant is a property of the ARTIFACT, and every showcase, every downstream
28
+ // app (buchhaltung, bauplaner, Learn6502) and CI all produce it through this
29
+ // one code path. A hook would protect only commits in this repo; a per-package
30
+ // script would protect one package.
31
+ //
32
+ // ## The oracle is the module graph, NOT the emitted text
33
+ //
34
+ // The first cut of this guard grepped the output for `from "node:…"`. That is
35
+ // wrong, and CI caught it: `@gjsify/process`'s own test bundle names a case
36
+ //
37
+ // await it(`named-import hrtime (from "node:process") preserves .bigint`, …)
38
+ //
39
+ // so the phrase appears verbatim inside a STRING, in a bundle that loads and
40
+ // runs under GJS perfectly well. The same trap sits in `@gjsify/cli`'s own
41
+ // bundle, which embeds `giNodeShimSource()`'s `--app node` codegen template
42
+ // (`import { createRequire } from 'node:module';`). Telling a statement from a
43
+ // quoted lookalike needs a JS lexer — but the bundler already did that work:
44
+ // every chunk carries the list of external specifiers it actually imports. That
45
+ // list is exact, engine-independent (npm `rolldown` returns it directly,
46
+ // `@gjsify/rolldown-native` forwards it through `synthRolldownOutput`), and
47
+ // free.
48
+ //
49
+ // Scope is STATIC imports, which is precisely the failure mode above: they are
50
+ // resolved when GJS links the module, so one bad specifier kills the whole
51
+ // bundle. A dynamic `import('node:x')` throws lazily at its call site instead —
52
+ // a real bug, but not "the artifact cannot load", and not what this gate is
53
+ // for.
54
+ /**
55
+ * Bare `node:` specifiers a `--app gjs` bundle may keep as static imports.
56
+ *
57
+ * Deliberately EMPTY. Every Node builtin has an `@gjsify/*` polyfill that
58
+ * `--app gjs` aliases to, so there is no specifier that both survives into the
59
+ * module graph and loads under GJS. Reading the graph instead of the text is
60
+ * what keeps this list empty — the entries an earlier text-scanning version
61
+ * needed (`node:module`, `node:process`) were quoted lookalikes, never imports.
62
+ *
63
+ * Add an entry ONLY with a comment naming why GJS can resolve it. Each one is a
64
+ * specifier this guard can no longer catch for real.
65
+ */
66
+ export const GJS_ALLOWED_NODE_IMPORTS = [];
67
+ /**
68
+ * The distinct bare `node:` specifiers `chunks` statically import, minus the
69
+ * allowlist. Sorted, so the error message and any test are stable.
70
+ */
71
+ export function findDisallowedNodeImports(chunks, allowed = GJS_ALLOWED_NODE_IMPORTS) {
72
+ const found = new Set();
73
+ for (const chunk of chunks) {
74
+ for (const specifier of chunk.imports ?? []) {
75
+ if (!specifier.startsWith('node:'))
76
+ continue;
77
+ if (allowed.includes(specifier))
78
+ continue;
79
+ found.add(specifier);
80
+ }
81
+ }
82
+ return [...found].sort();
83
+ }
84
+ /**
85
+ * Throw when a built `--app gjs` bundle statically imports Node builtins.
86
+ *
87
+ * @param chunks the emitted chunks, as reported by the bundler
88
+ * @param outfile path of the bundle, for the error message
89
+ */
90
+ export function assertGjsBundleLoadable(chunks, outfile) {
91
+ const offenders = findDisallowedNodeImports(chunks);
92
+ if (offenders.length === 0)
93
+ return;
94
+ throw new Error(`gjsify build --app gjs: ${outfile} still imports ${offenders.length} bare \`node:\` module(s), ` +
95
+ 'which stock GJS cannot resolve — it would fail at load with ' +
96
+ '"ImportError: Unsupported URI scheme for importing: node".\n' +
97
+ offenders.map((s) => ` - ${s}`).join('\n') +
98
+ '\n\nThese should have been aliased to their `@gjsify/*` polyfills. The usual cause is that the ' +
99
+ 'alias TARGET could not be resolved, so the bundler externalised the import instead (look for ' +
100
+ 'UNRESOLVED_IMPORT warnings above): e.g. a workspace checkout whose `@gjsify/*` packages have no ' +
101
+ 'built `lib/`. Build the workspace first (`gjsify run build:infra`), or fix the resolution, then ' +
102
+ 'rebuild.');
103
+ }
@@ -46,7 +46,7 @@
46
46
  // lives in the Node launcher, so a native oxlint bridge could only run the
47
47
  // Rust rule subset. Lint stays Node-spawned until that trade-off is decided
48
48
  // (tracked in STATUS.md Open TODOs).
49
- import { existsSync, readFileSync } from 'node:fs';
49
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
50
50
  import { join, resolve } from 'node:path';
51
51
  import { createRequire } from 'node:module';
52
52
  import { pathToFileURL } from 'node:url';
@@ -89,7 +89,12 @@ function oxcBindingSuffix() {
89
89
  // glibc systems have `/lib/ld-linux-*`, musl has `/lib/ld-musl-*`.
90
90
  let libc = 'gnu';
91
91
  try {
92
- const { readdirSync } = require('node:fs');
92
+ // `readdirSync` is imported STATICALLY at the top of this file. It
93
+ // used to be a lazy `require('node:fs')` inside this try — a bare
94
+ // `require` in an ESM module, so it threw ReferenceError, the catch
95
+ // below swallowed it, and `libc` silently stayed 'gnu': a musl host
96
+ // would be told to install the WRONG binding package. Same defect
97
+ // class as commands/affected.ts, and even quieter.
93
98
  const libEntries = readdirSync('/lib');
94
99
  if (libEntries.some((e) => e.startsWith('ld-musl-'))) {
95
100
  libc = 'musl';
@@ -6,6 +6,22 @@ import { type ExampleRuntime } from './runtimes.js';
6
6
  * not available under the GJS-bundled CLI.
7
7
  */
8
8
  export declare function resolveNodeGi(startDir: string): string | null;
9
+ /**
10
+ * Resolve `@gjsify/node-gi` for a bundle we are about to launch: from the
11
+ * BUNDLE's own directory first, then from `cwd`.
12
+ *
13
+ * The bundle's directory is the correct base and cwd is the fallback, for the
14
+ * same reason {@link computeNativeEnvForBundle} walks both: `@gjsify/node-gi` is
15
+ * kept EXTERNAL, so what has to be satisfiable is the RUNTIME's resolution of a
16
+ * bare specifier from inside the bundle — which Node performs relative to the
17
+ * importing file, never relative to cwd. A dlx-cache layout
18
+ * (`~/.cache/gjsify/dlx/<sha>/node_modules/<pkg>/dist/bundle.mjs`) has node-gi
19
+ * sitting right beside the bundle and nothing at all under the user's cwd, so a
20
+ * cwd-only probe rejected a run that would have worked. The cwd probe is kept
21
+ * because a project-local `node_modules` legitimately satisfies a bundle built
22
+ * into a `dist/` outside it (a `--outfile /tmp/…` build from a project root).
23
+ */
24
+ export declare function resolveNodeGiForBundle(bundlePath: string, cwd: string): string | null;
9
25
  /** Pick the node executable: the current one when on Node, else PATH `node`. */
10
26
  export declare function nodeBinary(): string;
11
27
  export interface RunNodeBundleOptions {
@@ -5,10 +5,12 @@
5
5
  // through `@gjsify/node-gi` (the Axis-5 reverse bridge). Two things have to be
6
6
  // true at runtime:
7
7
  //
8
- // 1. `@gjsify/node-gi` must resolve from the project — it is kept EXTERNAL by
9
- // the bundler (a native addon), so it has to physically exist in the
10
- // consumer's node_modules. We verify this up front and emit a clear hint
11
- // instead of letting Node throw `ERR_MODULE_NOT_FOUND` deep in the bundle.
8
+ // 1. `@gjsify/node-gi` must resolve FROM THE BUNDLE — it is kept EXTERNAL by
9
+ // the bundler (a native addon), so it has to physically exist in a
10
+ // node_modules the bundle's own directory can reach (that is where Node
11
+ // will look), with the CWD as a fallback. We verify this up front and emit
12
+ // a clear hint instead of letting Node throw `ERR_MODULE_NOT_FOUND` deep in
13
+ // the bundle. See `resolveNodeGiForBundle`.
12
14
  // 2. `gi://Gtk` / `gi://Adw` etc. must resolve. System typelibs come from the
13
15
  // default GI search path, but any `@gjsify/*` Vala bridge a story pulls in
14
16
  // ships its typelib in `prebuilds/` — so we reuse the SAME
@@ -21,7 +23,7 @@
21
23
  // `gjs`, which cannot run a `--app node` bundle).
22
24
  import { spawn } from 'node:child_process';
23
25
  import { existsSync, readFileSync } from 'node:fs';
24
- import { join, resolve } from 'node:path';
26
+ import { dirname, join, resolve } from 'node:path';
25
27
  import { isNode } from '@gjsify/rolldown-plugin-gjsify/runtime';
26
28
  import { computeNativeEnvForBundle } from './run-gjs.js';
27
29
  import { RUNTIMES } from './runtimes.js';
@@ -43,6 +45,25 @@ export function resolveNodeGi(startDir) {
43
45
  dir = parent;
44
46
  }
45
47
  }
48
+ /**
49
+ * Resolve `@gjsify/node-gi` for a bundle we are about to launch: from the
50
+ * BUNDLE's own directory first, then from `cwd`.
51
+ *
52
+ * The bundle's directory is the correct base and cwd is the fallback, for the
53
+ * same reason {@link computeNativeEnvForBundle} walks both: `@gjsify/node-gi` is
54
+ * kept EXTERNAL, so what has to be satisfiable is the RUNTIME's resolution of a
55
+ * bare specifier from inside the bundle — which Node performs relative to the
56
+ * importing file, never relative to cwd. A dlx-cache layout
57
+ * (`~/.cache/gjsify/dlx/<sha>/node_modules/<pkg>/dist/bundle.mjs`) has node-gi
58
+ * sitting right beside the bundle and nothing at all under the user's cwd, so a
59
+ * cwd-only probe rejected a run that would have worked. The cwd probe is kept
60
+ * because a project-local `node_modules` legitimately satisfies a bundle built
61
+ * into a `dist/` outside it (a `--outfile /tmp/…` build from a project root).
62
+ */
63
+ export function resolveNodeGiForBundle(bundlePath, cwd) {
64
+ const resolvedBundle = resolve(bundlePath);
65
+ return resolveNodeGi(dirname(resolvedBundle)) ?? resolveNodeGi(cwd);
66
+ }
46
67
  /** Pick the node executable: the current one when on Node, else PATH `node`. */
47
68
  export function nodeBinary() {
48
69
  return isNode() && process.execPath ? process.execPath : 'node';
@@ -55,7 +76,7 @@ export function nodeBinary() {
55
76
  */
56
77
  export async function runNodeBundle(bundlePath, extraArgs = [], options = {}) {
57
78
  const cwd = process.cwd();
58
- if (!resolveNodeGi(cwd)) {
79
+ if (!resolveNodeGiForBundle(bundlePath, cwd)) {
59
80
  throw new Error('Cannot run the storybook on Node: `@gjsify/node-gi` is not installed.\n' +
60
81
  'Add @gjsify/node-gi as a dependency to run the storybook on Node, e.g.:\n' +
61
82
  ' "devDependencies": { "@gjsify/node-gi": "^0.13.0" }');
@@ -113,7 +134,7 @@ export function bundleRequiresNodeGi(bundlePath) {
113
134
  export async function runRuntimeBundle(runtime, bundlePath, extraArgs = [], options = {}) {
114
135
  const cwd = process.cwd();
115
136
  const resolvedBundle = resolve(bundlePath);
116
- if (bundleRequiresNodeGi(resolvedBundle) && !resolveNodeGi(cwd)) {
137
+ if (bundleRequiresNodeGi(resolvedBundle) && !resolveNodeGiForBundle(resolvedBundle, cwd)) {
117
138
  throw new Error(`Cannot run this bundle on ${runtime}: it uses gi:// via \`@gjsify/node-gi\`, which is not installed.\n` +
118
139
  'Add @gjsify/node-gi as a dependency to run gi:// code off GJS, e.g.:\n' +
119
140
  ' "dependencies": { "@gjsify/node-gi": "^0.13.0" }');
@@ -37,6 +37,27 @@ export declare function isRuntimeOnPath(probe: string): boolean;
37
37
  export declare function isRuntimeAvailable(runtime: ExampleRuntime): boolean;
38
38
  /** Which of gjs/node/bun/deno are runnable on this host. */
39
39
  export declare function availableRuntimes(): ExampleRuntime[];
40
+ /**
41
+ * The default `--runtime` for running a prebuilt EXAMPLE/SHOWCASE: `gjs`
42
+ * whenever gjs is runnable here, else the host runtime.
43
+ *
44
+ * This is deliberately NOT a plain `hostRuntime()`. A showcase's canonical
45
+ * artifact is its `--app gjs` bundle (`gjsify.main`) — that is what every
46
+ * showcase ships, what the `gjs` path installs via `gjsify dlx`, and what the
47
+ * docs call the default. Following the host instead means `npx @gjsify/cli
48
+ * showcase <name>` (host = node) silently asks for the `--app node` bundle,
49
+ * which most showcases do not ship and which additionally needs
50
+ * `@gjsify/node-gi` in the consumer's project — so the FIRST-RUN experience of
51
+ * the primary documented entry point failed on a missing file.
52
+ *
53
+ * Same rule `gjsify run` already applies in its bare-file path, where a
54
+ * `--app gjs` bundle stays on gjs regardless of host (`isLikelyGjsBundle`);
55
+ * here the "is it a gjs artifact" question is answered by the command instead
56
+ * of by sniffing the file. `--runtime` still overrides explicitly, and a host
57
+ * WITHOUT gjs (a plain Node/bun/deno box) keeps following the host so the
58
+ * node-capable showcases stay runnable there.
59
+ */
60
+ export declare function defaultExampleRuntime(): ExampleRuntime;
40
61
  interface ExampleGjsifyField {
41
62
  example?: {
42
63
  runtimes?: unknown;
@@ -64,6 +64,29 @@ export function isRuntimeAvailable(runtime) {
64
64
  export function availableRuntimes() {
65
65
  return EXAMPLE_RUNTIMES.filter((rt) => isRuntimeAvailable(rt));
66
66
  }
67
+ /**
68
+ * The default `--runtime` for running a prebuilt EXAMPLE/SHOWCASE: `gjs`
69
+ * whenever gjs is runnable here, else the host runtime.
70
+ *
71
+ * This is deliberately NOT a plain `hostRuntime()`. A showcase's canonical
72
+ * artifact is its `--app gjs` bundle (`gjsify.main`) — that is what every
73
+ * showcase ships, what the `gjs` path installs via `gjsify dlx`, and what the
74
+ * docs call the default. Following the host instead means `npx @gjsify/cli
75
+ * showcase <name>` (host = node) silently asks for the `--app node` bundle,
76
+ * which most showcases do not ship and which additionally needs
77
+ * `@gjsify/node-gi` in the consumer's project — so the FIRST-RUN experience of
78
+ * the primary documented entry point failed on a missing file.
79
+ *
80
+ * Same rule `gjsify run` already applies in its bare-file path, where a
81
+ * `--app gjs` bundle stays on gjs regardless of host (`isLikelyGjsBundle`);
82
+ * here the "is it a gjs artifact" question is answered by the command instead
83
+ * of by sniffing the file. `--runtime` still overrides explicitly, and a host
84
+ * WITHOUT gjs (a plain Node/bun/deno box) keeps following the host so the
85
+ * node-capable showcases stay runnable there.
86
+ */
87
+ export function defaultExampleRuntime() {
88
+ return isRuntimeAvailable('gjs') ? 'gjs' : hostRuntime();
89
+ }
67
90
  /**
68
91
  * Read a package's declared example runtimes. Returns the (validated) list, or
69
92
  * `null` when the package declares none — the "unconstrained / permissive" case.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/cli",
3
- "version": "0.23.0",
3
+ "version": "0.24.1",
4
4
  "description": "CLI for Gjsify",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -130,19 +130,19 @@
130
130
  "cli"
131
131
  ],
132
132
  "dependencies": {
133
- "@gjsify/buffer": "^0.23.0",
134
- "@gjsify/create-app": "^0.23.0",
135
- "@gjsify/node-globals": "^0.23.0",
136
- "@gjsify/node-polyfills": "^0.23.0",
137
- "@gjsify/npm-registry": "^0.23.0",
138
- "@gjsify/resolve-npm": "^0.23.0",
139
- "@gjsify/rolldown-plugin-gjsify": "^0.23.0",
140
- "@gjsify/rolldown-plugin-pnp": "^0.23.0",
141
- "@gjsify/semver": "^0.23.0",
142
- "@gjsify/tar": "^0.23.0",
143
- "@gjsify/tsc": "^0.23.0",
144
- "@gjsify/web-polyfills": "^0.23.0",
145
- "@gjsify/workspace": "^0.23.0",
133
+ "@gjsify/buffer": "^0.24.1",
134
+ "@gjsify/create-app": "^0.24.1",
135
+ "@gjsify/node-globals": "^0.24.1",
136
+ "@gjsify/node-polyfills": "^0.24.1",
137
+ "@gjsify/npm-registry": "^0.24.1",
138
+ "@gjsify/resolve-npm": "^0.24.1",
139
+ "@gjsify/rolldown-plugin-gjsify": "^0.24.1",
140
+ "@gjsify/rolldown-plugin-pnp": "^0.24.1",
141
+ "@gjsify/semver": "^0.24.1",
142
+ "@gjsify/tar": "^0.24.1",
143
+ "@gjsify/tsc": "^0.24.1",
144
+ "@gjsify/web-polyfills": "^0.24.1",
145
+ "@gjsify/workspace": "^0.24.1",
146
146
  "cosmiconfig": "^9.0.2",
147
147
  "get-tsconfig": "^4.14.0",
148
148
  "pkg-types": "^2.3.1",
@@ -150,13 +150,13 @@
150
150
  "yargs": "^18.0.0"
151
151
  },
152
152
  "devDependencies": {
153
- "@gjsify/unit": "^0.23.0",
153
+ "@gjsify/unit": "^0.24.1",
154
154
  "@types/yargs": "^17.0.35",
155
155
  "typescript": "^6.0.3"
156
156
  },
157
157
  "peerDependencies": {
158
- "@gjsify/oxfmt-native": "^0.23.0",
159
- "@gjsify/rolldown-native": "^0.23.0"
158
+ "@gjsify/oxfmt-native": "^0.24.1",
159
+ "@gjsify/rolldown-native": "^0.24.1"
160
160
  },
161
161
  "peerDependenciesMeta": {
162
162
  "@gjsify/oxfmt-native": {