@gjsify/cli 0.17.0 → 0.19.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.
- package/dist/cli.gjs.mjs +158 -155
- package/lib/actions/build.d.ts +15 -4
- package/lib/actions/build.js +34 -6
- package/lib/cli-app.js +16 -8
- package/lib/commands/build.js +13 -3
- package/lib/commands/check.js +2 -13
- package/lib/commands/dlx.d.ts +17 -0
- package/lib/commands/dlx.js +7 -1
- package/lib/commands/install.js +10 -5
- package/lib/commands/run.d.ts +1 -0
- package/lib/commands/run.js +109 -4
- package/lib/commands/showcase.d.ts +1 -0
- package/lib/commands/showcase.js +125 -25
- package/lib/commands/storybook.js +48 -21
- package/lib/config.js +12 -1
- package/lib/index.js +2 -2
- package/lib/types/config-data.d.ts +9 -0
- package/lib/utils/detect-native-packages.js +2 -10
- package/lib/utils/discover-showcases.js +10 -2
- package/lib/utils/node-version.js +2 -2
- package/lib/utils/oxc-resolve.js +2 -4
- package/lib/utils/pkg-json-edit.d.ts +2 -0
- package/lib/utils/pkg-json.d.ts +6 -0
- package/lib/utils/pkg-json.js +24 -0
- package/lib/utils/resolve-gjs-entry.d.ts +11 -0
- package/lib/utils/resolve-gjs-entry.js +42 -0
- package/lib/utils/run-gjs.js +15 -2
- package/lib/utils/run-node.d.ts +21 -0
- package/lib/utils/run-node.js +62 -1
- package/lib/utils/runtimes.d.ts +66 -0
- package/lib/utils/runtimes.js +97 -0
- package/lib/utils/workspace-root.js +2 -9
- package/package.json +20 -17
package/lib/actions/build.d.ts
CHANGED
|
@@ -18,11 +18,22 @@ export declare class BuildAction {
|
|
|
18
18
|
private parseGlobalsValue;
|
|
19
19
|
/**
|
|
20
20
|
* Resolve the `--globals` CLI list into a pre-computed inject stub path
|
|
21
|
-
* that the orchestrator appends to its input list.
|
|
22
|
-
* `--app
|
|
21
|
+
* that the orchestrator appends to its input list. Runs for `--app gjs`
|
|
22
|
+
* AND `--app node`: on gjs the registers install our polyfills over the
|
|
23
|
+
* bare GJS runtime; on node an EXPLICIT list is the reverse bridge's
|
|
24
|
+
* DOM-surface request (a genuine GJS source built via `@gjsify/node-gi`
|
|
25
|
+
* needs the same `@gjsify/*` registers — document, HTMLCanvasElement, … —
|
|
26
|
+
* it gets under `--app gjs`, backed by gi:// through node-gi). Browser
|
|
27
|
+
* builds rely on native globals.
|
|
23
28
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
29
|
+
* Node is EXPLICIT-ONLY by design: plain `--globals auto` never injects
|
|
30
|
+
* web/dom registers into a node bundle (cross-platform packages' node
|
|
31
|
+
* builds must stay loadable on plain Node without node-gi), so the
|
|
32
|
+
* `@gjsify/empty` routing is only lifted when the USER names the globals.
|
|
33
|
+
*
|
|
34
|
+
* Used for the explicit-only path (no `auto` token) and, on node, for the
|
|
35
|
+
* `auto,<extras>` extras. The gjs auto path is handled in `buildApp` via
|
|
36
|
+
* the iterative multi-pass build.
|
|
26
37
|
*/
|
|
27
38
|
private resolveGlobalsInject;
|
|
28
39
|
/**
|
package/lib/actions/build.js
CHANGED
|
@@ -207,21 +207,38 @@ export class BuildAction {
|
|
|
207
207
|
}
|
|
208
208
|
/**
|
|
209
209
|
* Resolve the `--globals` CLI list into a pre-computed inject stub path
|
|
210
|
-
* that the orchestrator appends to its input list.
|
|
211
|
-
* `--app
|
|
210
|
+
* that the orchestrator appends to its input list. Runs for `--app gjs`
|
|
211
|
+
* AND `--app node`: on gjs the registers install our polyfills over the
|
|
212
|
+
* bare GJS runtime; on node an EXPLICIT list is the reverse bridge's
|
|
213
|
+
* DOM-surface request (a genuine GJS source built via `@gjsify/node-gi`
|
|
214
|
+
* needs the same `@gjsify/*` registers — document, HTMLCanvasElement, … —
|
|
215
|
+
* it gets under `--app gjs`, backed by gi:// through node-gi). Browser
|
|
216
|
+
* builds rely on native globals.
|
|
212
217
|
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
218
|
+
* Node is EXPLICIT-ONLY by design: plain `--globals auto` never injects
|
|
219
|
+
* web/dom registers into a node bundle (cross-platform packages' node
|
|
220
|
+
* builds must stay loadable on plain Node without node-gi), so the
|
|
221
|
+
* `@gjsify/empty` routing is only lifted when the USER names the globals.
|
|
222
|
+
*
|
|
223
|
+
* Used for the explicit-only path (no `auto` token) and, on node, for the
|
|
224
|
+
* `auto,<extras>` extras. The gjs auto path is handled in `buildApp` via
|
|
225
|
+
* the iterative multi-pass build.
|
|
215
226
|
*/
|
|
216
227
|
async resolveGlobalsInject(app, globals, verbose) {
|
|
217
|
-
if (app !== 'gjs')
|
|
228
|
+
if (app !== 'gjs' && app !== 'node')
|
|
218
229
|
return undefined;
|
|
219
230
|
if (!globals)
|
|
220
231
|
return undefined;
|
|
221
232
|
const registerPaths = resolveGlobalsList(globals);
|
|
222
233
|
if (registerPaths.size === 0)
|
|
223
234
|
return undefined;
|
|
224
|
-
|
|
235
|
+
// On `--app node` (the reverse bridge) the injected register bodies must
|
|
236
|
+
// OVERRIDE the runtime-native web globals gjsify owns (the `fetch`
|
|
237
|
+
// family — undici rejects the root-relative `/res/...` asset paths a GJS
|
|
238
|
+
// app uses). See writeRegisterInjectFile / REVERSE_BRIDGE_NATIVE_OVERRIDE_IDENTS.
|
|
239
|
+
const injectPath = await writeRegisterInjectFile(registerPaths, process.cwd(), {
|
|
240
|
+
reverseBridgeOverride: app === 'node',
|
|
241
|
+
});
|
|
225
242
|
if (verbose && injectPath) {
|
|
226
243
|
console.debug(`[gjsify] globals: injected ${registerPaths.size} register module(s) from --globals ${globals}`);
|
|
227
244
|
}
|
|
@@ -517,6 +534,17 @@ export class BuildAction {
|
|
|
517
534
|
}, pluginOpts, gjsifyPluginFactory, bundleToChunks);
|
|
518
535
|
if (needsGiGlobals)
|
|
519
536
|
pluginOpts.nodeGiGlobalsInject = true;
|
|
537
|
+
// Explicit extras on a node build (`--globals auto,dom,…`) are the
|
|
538
|
+
// reverse bridge's DOM-surface request: inject the SAME register
|
|
539
|
+
// modules the gjs target would for those identifiers, so a genuine
|
|
540
|
+
// GJS source (an Excalibur/WebGLBridge app) gets document /
|
|
541
|
+
// HTMLCanvasElement / matchMedia / … on Node via node-gi. The
|
|
542
|
+
// orchestrator lifts the `@gjsify/empty` register routing only when
|
|
543
|
+
// this inject stub is present (see app/node.ts) — plain `auto`
|
|
544
|
+
// node builds are byte-unchanged.
|
|
545
|
+
if (extras) {
|
|
546
|
+
pluginOpts.autoGlobalsInject = await this.resolveGlobalsInject(app, extras, verbose);
|
|
547
|
+
}
|
|
520
548
|
}
|
|
521
549
|
else if (extras) {
|
|
522
550
|
pluginOpts.autoGlobalsInject = await this.resolveGlobalsInject(app, extras, verbose);
|
package/lib/cli-app.js
CHANGED
|
@@ -21,23 +21,31 @@ import { fileURLToPath } from 'node:url';
|
|
|
21
21
|
import yargs from 'yargs';
|
|
22
22
|
import { buildCommand as build, testCommand as test, runCommand as run, infoCommand as info, systemCheckCommand as systemCheck, checkCommand as check, showcaseCommand as showcase, createCommand as create, gresourceCommand as gresource, gettextCommand as gettext, gsettingsCommand as gsettings, flatpakCommand as flatpak, dlxCommand as dlx, installCommand as install, foreachCommand as foreach, workspaceCommand as workspace, packCommand as pack, publishCommand as publish, whoamiCommand as whoami, loginCommand as login, logoutCommand as logout, trustCommand as trust, onboardCommand as onboard, selfUpdateCommand as selfUpdate, generateInstallerCommand as generateInstaller, uninstallCommand as uninstall, formatCommand as format, lintCommand as lint, fixCommand as fix, upgradeCommand as upgrade, barrelsCommand as barrels, tscCommand as tsc, affectedCommand as affected, storybookCommand as storybook, debugCommand as debug, browseCommand as browse, } from './commands/index.js';
|
|
23
23
|
import { APP_NAME } from './constants.js';
|
|
24
|
-
import { isNode } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
24
|
+
import { isBun, isDeno, isNode, gjsSystemVersion } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
25
25
|
import { ensureGjsifyShimOnPath } from './utils/gjsify-shim.js';
|
|
26
|
-
// Detect which runtime is executing the CLI (GJS
|
|
27
|
-
//
|
|
28
|
-
// `process.versions.node
|
|
29
|
-
//
|
|
26
|
+
// Detect which runtime is executing the CLI (GJS, Node.js, Bun or Deno) for the
|
|
27
|
+
// help epilogue. Bun and Deno MUST be checked before Node because both — like
|
|
28
|
+
// GJS's @gjsify/process shim — fake `process.versions.node` for npm
|
|
29
|
+
// compatibility, so a plain Node probe is a false positive on either.
|
|
30
30
|
function runtimeLabel() {
|
|
31
31
|
try {
|
|
32
|
-
const
|
|
33
|
-
if (
|
|
34
|
-
const v = Number(
|
|
32
|
+
const sysVersion = gjsSystemVersion();
|
|
33
|
+
if (sysVersion !== undefined) {
|
|
34
|
+
const v = Number(sysVersion);
|
|
35
35
|
return `GJS ${Math.floor(v / 10000)}.${Math.floor((v % 10000) / 100)}.${v % 100} (SpiderMonkey)`;
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
catch {
|
|
39
39
|
/* not GJS */
|
|
40
40
|
}
|
|
41
|
+
if (isBun()) {
|
|
42
|
+
const version = globalThis.Bun?.version ?? process.versions.bun;
|
|
43
|
+
return `Bun ${version ?? ''}`.trim();
|
|
44
|
+
}
|
|
45
|
+
if (isDeno()) {
|
|
46
|
+
const version = globalThis.Deno?.version?.deno;
|
|
47
|
+
return `Deno ${version ?? ''}`.trim();
|
|
48
|
+
}
|
|
41
49
|
if (isNode()) {
|
|
42
50
|
return `Node.js ${process.version}`;
|
|
43
51
|
}
|
package/lib/commands/build.js
CHANGED
|
@@ -39,11 +39,17 @@ export const buildCommand = {
|
|
|
39
39
|
default: false,
|
|
40
40
|
})
|
|
41
41
|
.option('app', {
|
|
42
|
-
description: '
|
|
42
|
+
description: 'Build target for an application. Defaults to the target of the HOST runtime running the CLI: gjs when run under gjs, node when run under node/bun/deno (both consume the --app node bundle). Set `gjsify.app` in package.json#gjsify to override the host default.',
|
|
43
43
|
type: 'string',
|
|
44
44
|
choices: ['gjs', 'node', 'browser', 'nativescript'],
|
|
45
45
|
normalize: true,
|
|
46
|
-
default: 'gjs'
|
|
46
|
+
// No yargs `default: 'gjs'` — a yargs default is
|
|
47
|
+
// indistinguishable from a user-set value and would clobber
|
|
48
|
+
// `package.json#gjsify.app` in config.ts (same footgun as
|
|
49
|
+
// `globals`/`entryPoints`). The host-derived fallback is applied
|
|
50
|
+
// post-merge in `Config.forBuild`: CLI flag > config file > host
|
|
51
|
+
// default (`buildAppForRuntime(hostRuntime())`).
|
|
52
|
+
defaultDescription: 'host runtime target (gjs on gjs, else node)',
|
|
47
53
|
})
|
|
48
54
|
.option('format', {
|
|
49
55
|
description: 'Override the default output format',
|
|
@@ -160,7 +166,11 @@ export const buildCommand = {
|
|
|
160
166
|
const action = new BuildAction(configData);
|
|
161
167
|
await action.start({
|
|
162
168
|
library: args.library,
|
|
163
|
-
|
|
169
|
+
// `configData.app` carries the CLI-flag > config-file > host-default
|
|
170
|
+
// resolution done in `Config.forBuild` (the yargs `--app` default was
|
|
171
|
+
// dropped so the config-file value survives). `BuildAction.start`
|
|
172
|
+
// falls back to 'gjs' only if this is somehow undefined.
|
|
173
|
+
app: configData.app,
|
|
164
174
|
watch: args.watch,
|
|
165
175
|
});
|
|
166
176
|
},
|
package/lib/commands/check.js
CHANGED
|
@@ -16,22 +16,11 @@
|
|
|
16
16
|
// (since the typescript-check binding is positional `[paths..]`).
|
|
17
17
|
import { spawn, spawnSync } from 'node:child_process';
|
|
18
18
|
import { prefixLines } from '../utils/prefixed-output.js';
|
|
19
|
-
import {
|
|
19
|
+
import { readPackageJson } from '../utils/pkg-json.js';
|
|
20
20
|
import { join } from 'node:path';
|
|
21
21
|
import { cpus } from 'node:os';
|
|
22
22
|
import { discoverWorkspaces, filterWorkspaces } from '@gjsify/workspace';
|
|
23
23
|
import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
24
|
-
function readPackageJson(dir) {
|
|
25
|
-
const path = join(dir, 'package.json');
|
|
26
|
-
if (!existsSync(path))
|
|
27
|
-
return null;
|
|
28
|
-
try {
|
|
29
|
-
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
24
|
/**
|
|
36
25
|
* Run a single workspace's `check` script. Returns the exit code; non-zero
|
|
37
26
|
* indicates failure. Output is forwarded to the parent's stdout/stderr,
|
|
@@ -108,7 +97,7 @@ export const checkCommand = {
|
|
|
108
97
|
// through gjsify so the command surface stays uniform with format/
|
|
109
98
|
// lint/fix.
|
|
110
99
|
if (workspaceRoot && cwd !== workspaceRoot) {
|
|
111
|
-
const pkg = readPackageJson(cwd);
|
|
100
|
+
const pkg = readPackageJson(join(cwd, 'package.json'));
|
|
112
101
|
if (pkg?.scripts?.check) {
|
|
113
102
|
if (args.verbose) {
|
|
114
103
|
console.log(`[check] cwd=${cwd} → npm run check`);
|
package/lib/commands/dlx.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Command } from '../types/index.js';
|
|
2
|
+
import { type ParsedSpec } from '../utils/parse-spec.js';
|
|
2
3
|
interface DlxOptions {
|
|
3
4
|
spec: string;
|
|
4
5
|
binOrArg?: string;
|
|
@@ -10,4 +11,20 @@ interface DlxOptions {
|
|
|
10
11
|
registry?: string;
|
|
11
12
|
}
|
|
12
13
|
export declare const dlxCommand: Command<unknown, DlxOptions>;
|
|
14
|
+
export interface EnsureOpts {
|
|
15
|
+
verbose: boolean;
|
|
16
|
+
registry?: string;
|
|
17
|
+
cacheMaxAge: number;
|
|
18
|
+
frozen: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolve (installing + caching when remote) the on-disk directory of a package
|
|
22
|
+
* spec — the shared "get me this package's files" step behind `gjsify dlx` and
|
|
23
|
+
* `gjsify showcase`. Local paths resolve directly; remote specs install into the
|
|
24
|
+
* per-key dlx cache (atomic symlink-swap) and reuse a fresh cache entry.
|
|
25
|
+
*/
|
|
26
|
+
export declare function ensurePkgDir(parsed: ParsedSpec, opts: EnsureOpts): Promise<{
|
|
27
|
+
pkgDir: string;
|
|
28
|
+
cachedPkgName: string | null;
|
|
29
|
+
}>;
|
|
13
30
|
export {};
|
package/lib/commands/dlx.js
CHANGED
|
@@ -96,7 +96,13 @@ export const dlxCommand = {
|
|
|
96
96
|
await runGjsBundle(entry.bundlePath, extraArgs, { exitOnSuccess: true });
|
|
97
97
|
},
|
|
98
98
|
};
|
|
99
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Resolve (installing + caching when remote) the on-disk directory of a package
|
|
101
|
+
* spec — the shared "get me this package's files" step behind `gjsify dlx` and
|
|
102
|
+
* `gjsify showcase`. Local paths resolve directly; remote specs install into the
|
|
103
|
+
* per-key dlx cache (atomic symlink-swap) and reuse a fresh cache entry.
|
|
104
|
+
*/
|
|
105
|
+
export async function ensurePkgDir(parsed, opts) {
|
|
100
106
|
if (parsed.kind === 'local') {
|
|
101
107
|
return { pkgDir: parsed.path, cachedPkgName: null };
|
|
102
108
|
}
|
package/lib/commands/install.js
CHANGED
|
@@ -31,12 +31,14 @@ import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, rmSync, syml
|
|
|
31
31
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
32
32
|
import { fileURLToPath } from 'node:url';
|
|
33
33
|
import { spawn, spawnSync } from 'node:child_process';
|
|
34
|
+
import { gjsExit } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
34
35
|
import { discoverWorkspaces } from '@gjsify/workspace';
|
|
35
36
|
import { buildInstallCommand, detectPackageManager, runMinimalChecks } from '../utils/check-system-deps.js';
|
|
36
37
|
import { detectNativePackages } from '../utils/detect-native-packages.js';
|
|
37
38
|
import { installPackages, makeProgressReporter } from '../utils/install-backend.js';
|
|
38
39
|
import { atomicWriteStrict } from '../utils/install-cache-fs.js';
|
|
39
40
|
import { acquireInstallLock } from '../utils/install-lock.js';
|
|
41
|
+
import { nodeBinary } from '../utils/run-node.js';
|
|
40
42
|
import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
41
43
|
import { binDirOnPath, defaultGlobalLayout, installGjsEnginePackages, linkGlobalBins, specToPackageName, } from '../utils/install-global.js';
|
|
42
44
|
import { addDependencyEntry, defaultRangeFromVersion, parseSpec, projectSpecsFromPackageJson, readPackageJson, writePackageJson, } from '../utils/pkg-json-edit.js';
|
|
@@ -247,11 +249,8 @@ function isAbortedFromOverallTimeout(err) {
|
|
|
247
249
|
*/
|
|
248
250
|
function forceExit(code) {
|
|
249
251
|
process.exitCode = code;
|
|
250
|
-
|
|
251
|
-
if (gjs?.system?.exit) {
|
|
252
|
-
gjs.system.exit(code);
|
|
252
|
+
if (gjsExit(code))
|
|
253
253
|
return;
|
|
254
|
-
}
|
|
255
254
|
process.exit(code);
|
|
256
255
|
}
|
|
257
256
|
const CLI_PACKAGE_NAME = '@gjsify/cli';
|
|
@@ -1221,7 +1220,13 @@ function maybeInstallGitHooks() {
|
|
|
1221
1220
|
if (!existsSync(join(cwd, '.git')))
|
|
1222
1221
|
return;
|
|
1223
1222
|
try {
|
|
1224
|
-
|
|
1223
|
+
// `nodeBinary()`, NOT `process.execPath`: under the committed GJS bundle
|
|
1224
|
+
// `process.execPath` is `gjs`, so `spawnSync(process.execPath, [script])`
|
|
1225
|
+
// ran `gjs cli.gjs.mjs install-git-hooks.mjs` (wrong argv). The hook
|
|
1226
|
+
// installer is a plain Node ESM script; run it with a real node binary
|
|
1227
|
+
// (PATH `node` under GJS, the current node otherwise). Best-effort —
|
|
1228
|
+
// wrapped in try/catch, so a missing node just warns and continues.
|
|
1229
|
+
const result = spawnSync(nodeBinary(), [scriptPath, '--quiet'], {
|
|
1225
1230
|
cwd,
|
|
1226
1231
|
stdio: 'inherit',
|
|
1227
1232
|
env: process.env,
|
package/lib/commands/run.d.ts
CHANGED
package/lib/commands/run.js
CHANGED
|
@@ -13,14 +13,16 @@
|
|
|
13
13
|
// extension, or that resolves to an existing path on disk is treated
|
|
14
14
|
// as a bundle file. Everything else is a script name. Users who want
|
|
15
15
|
// to disambiguate can pass `./<file>` explicitly.
|
|
16
|
-
import { statSync } from 'node:fs';
|
|
16
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
17
17
|
import { basename, delimiter, join, resolve } from 'node:path';
|
|
18
18
|
import { spawn } from 'node:child_process';
|
|
19
19
|
import { runGjsBundle } from '../utils/run-gjs.js';
|
|
20
|
+
import { runRuntimeBundle } from '../utils/run-node.js';
|
|
20
21
|
import { readPackageJson } from '../utils/pkg-json-edit.js';
|
|
21
22
|
import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
22
23
|
import { discoverWorkspaces } from '@gjsify/workspace';
|
|
23
|
-
import { isGjs } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
24
|
+
import { isGjs, hostRuntime } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
25
|
+
import { EXAMPLE_RUNTIMES, isExampleRuntime, readDeclaredRuntimes, checkRuntimeSupported, } from '../utils/runtimes.js';
|
|
24
26
|
export const runCommand = {
|
|
25
27
|
command: 'run <target> [args..]',
|
|
26
28
|
description: 'Run a script from package.json (yarn-run-style) or a GJS bundle file. If <target> resolves to a file on disk (or has a path-like prefix), it is launched via gjs with LD_LIBRARY_PATH + GI_TYPELIB_PATH set for native packages. Otherwise it is looked up in the current package.json `scripts`.',
|
|
@@ -45,6 +47,18 @@ export const runCommand = {
|
|
|
45
47
|
alias: 'w',
|
|
46
48
|
type: 'string',
|
|
47
49
|
description: 'Run <target> as a script in the named workspace (by name or path), like `npm run <script> -w <name>`.',
|
|
50
|
+
})
|
|
51
|
+
.option('runtime', {
|
|
52
|
+
// Runtime selector for a BUNDLE FILE. `gjs` (default) launches
|
|
53
|
+
// the `--app gjs` bundle via `gjs -m`; node/bun/deno launch the
|
|
54
|
+
// `--app node` bundle on that runtime (Node-API is their common
|
|
55
|
+
// ABI, so bun/deno reuse the same node bundle). Declaring it here
|
|
56
|
+
// means yargs captures the flag instead of forwarding it to the
|
|
57
|
+
// target (this command routes unknown flags to the child).
|
|
58
|
+
// Passing --runtime FORCES file mode.
|
|
59
|
+
type: 'string',
|
|
60
|
+
choices: EXAMPLE_RUNTIMES,
|
|
61
|
+
description: 'Runtime to launch a bundle FILE on: gjs (default, `--app gjs` bundle) | node | bun | deno (the `--app node` bundle). Forces file mode.',
|
|
48
62
|
})
|
|
49
63
|
// Pass-through runner: the TARGET owns its whole flag namespace, so
|
|
50
64
|
// disable gjsify's own `--version` / `--help` for THIS command. Without
|
|
@@ -85,6 +99,24 @@ export const runCommand = {
|
|
|
85
99
|
const positionalArgs = args.args ?? [];
|
|
86
100
|
const doubleDashArgs = (args['--'] ?? []).filter((v) => typeof v === 'string');
|
|
87
101
|
const extraArgs = [...positionalArgs, ...doubleDashArgs];
|
|
102
|
+
// `--runtime <gjs|node|bun|deno>` (explicit) → run a BUNDLE FILE on the
|
|
103
|
+
// chosen runtime. This FORCES file mode: the runtime selector only makes
|
|
104
|
+
// sense for a prebuilt bundle, and it must beat the script-name lookup.
|
|
105
|
+
// gjs → `runGjsBundle` (gjs -m); node/bun/deno → `runRuntimeBundle` (the
|
|
106
|
+
// `--app node` bundle; bun/deno reuse it). Default gjs keeps back-compat.
|
|
107
|
+
const runtimeArg = args.runtime;
|
|
108
|
+
if (typeof runtimeArg === 'string' && runtimeArg.length > 0) {
|
|
109
|
+
if (typeof args.workspace === 'string' && args.workspace.length > 0) {
|
|
110
|
+
console.error('gjsify run: --runtime cannot be combined with --workspace (file mode vs script mode).');
|
|
111
|
+
return process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
if (!isExampleRuntime(runtimeArg)) {
|
|
114
|
+
console.error(`gjsify run: unknown --runtime "${runtimeArg}" (expected: ${EXAMPLE_RUNTIMES.join(', ')}).`);
|
|
115
|
+
return process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
await runTargetOnRuntime(runtimeArg, target, extraArgs);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
88
120
|
// `-w <name>` (npm/yarn parity): run <target> as a script in the named
|
|
89
121
|
// workspace's directory rather than the current one. Never file mode.
|
|
90
122
|
if (typeof args.workspace === 'string' && args.workspace.length > 0) {
|
|
@@ -101,14 +133,87 @@ export const runCommand = {
|
|
|
101
133
|
const hasScript = pkg?.scripts && typeof pkg.scripts[target] === 'string';
|
|
102
134
|
if (!hasScript && looksLikeFile(target)) {
|
|
103
135
|
const file = resolve(target);
|
|
104
|
-
//
|
|
136
|
+
// The default runtime for a bare `gjsify run <file>` FOLLOWS the host
|
|
137
|
+
// runtime the CLI executes in: gjs (global install) → gjs; node/bun/
|
|
138
|
+
// deno (npx/bunx/deno) → that runtime. EXCEPTION: a `--app gjs` bundle
|
|
139
|
+
// can only run on gjs (its gi:// imports are externalized, there is no
|
|
140
|
+
// node-gi shim), so it is always launched on gjs regardless of the
|
|
141
|
+
// host — this keeps `gjsify run dist/index.gjs.js` working when the
|
|
142
|
+
// CLI itself runs under node (e.g. `gjsify foreach test` for the
|
|
143
|
+
// node-cli examples). Pass `--runtime` explicitly to override.
|
|
144
|
+
const host = hostRuntime();
|
|
145
|
+
const runtime = host === 'gjs' || isLikelyGjsBundle(file) ? 'gjs' : host;
|
|
146
|
+
// Terminal calls — exit on success, or the GJS main loop parks
|
|
105
147
|
// this process forever (see RunGjsBundleOptions.exitOnSuccess).
|
|
106
|
-
|
|
148
|
+
if (runtime === 'gjs') {
|
|
149
|
+
await runGjsBundle(file, extraArgs, { exitOnSuccess: true });
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
await runRuntimeBundle(runtime, file, extraArgs, { exitOnSuccess: true });
|
|
153
|
+
}
|
|
107
154
|
return;
|
|
108
155
|
}
|
|
109
156
|
await runScript(target, extraArgs);
|
|
110
157
|
},
|
|
111
158
|
};
|
|
159
|
+
/**
|
|
160
|
+
* Run a bundle-file `target` on `runtime`. Validates that the target looks like
|
|
161
|
+
* a file (the runtime selector is for prebuilt bundles, not script names) and,
|
|
162
|
+
* best-effort, that the cwd package's `gjsify.example.runtimes` declaration
|
|
163
|
+
* permits the runtime — turning "this showcase can't run on node yet" into a
|
|
164
|
+
* clean, actionable error instead of a deep bundle crash. gjs → `runGjsBundle`;
|
|
165
|
+
* node/bun/deno → `runRuntimeBundle` (the shared `--app node` launcher).
|
|
166
|
+
*/
|
|
167
|
+
async function runTargetOnRuntime(runtime, target, extraArgs) {
|
|
168
|
+
if (!looksLikeFile(target)) {
|
|
169
|
+
console.error(`gjsify run: --runtime ${runtime} needs a bundle FILE, but "${target}" is not a path.\n` +
|
|
170
|
+
` Build first (e.g. \`gjsify build src/app.ts --app ${runtime === 'gjs' ? 'gjs' : 'node'} --outfile dist/app.${runtime === 'gjs' ? 'gjs' : 'node'}.mjs\`), then run the output file.`);
|
|
171
|
+
return process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
// Best-effort declaration check against the cwd package (the example dir).
|
|
174
|
+
const pkg = readPackageJson(join(process.cwd(), 'package.json'));
|
|
175
|
+
const declared = readDeclaredRuntimes(pkg);
|
|
176
|
+
const support = checkRuntimeSupported(runtime, declared, pkg?.name ?? target);
|
|
177
|
+
if (!support.ok) {
|
|
178
|
+
console.error(`gjsify run: ${support.message}`);
|
|
179
|
+
return process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
const file = resolve(target);
|
|
182
|
+
// Terminal call — exit on success (the GJS/GLib loop would otherwise park).
|
|
183
|
+
if (runtime === 'gjs') {
|
|
184
|
+
await runGjsBundle(file, extraArgs, { exitOnSuccess: true });
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
await runRuntimeBundle(runtime, file, extraArgs, { exitOnSuccess: true });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Whether `file` is a `--app gjs` bundle (which can ONLY run on gjs — its
|
|
192
|
+
* `gi://` imports stay as external string literals and boots through a `gjs -m`
|
|
193
|
+
* shebang / `imports.system` stub). Used to keep such a bundle on gjs even when
|
|
194
|
+
* the host is node/bun/deno, so the host-following default in the bare-file run
|
|
195
|
+
* path never sends a gjs bundle to a runtime it cannot load.
|
|
196
|
+
*
|
|
197
|
+
* A `--app node` bundle (even a node-gi reverse-bridge one) rewrites `gi://` to
|
|
198
|
+
* `@gjsify/node-gi` and never keeps a `gi://` import literal, so it is NOT
|
|
199
|
+
* matched here and follows the host runtime.
|
|
200
|
+
*/
|
|
201
|
+
function isLikelyGjsBundle(file) {
|
|
202
|
+
// Fast path on the conventional `.gjs.js` / `.gjs.mjs` naming.
|
|
203
|
+
if (/\.gjs\.[mc]?js$/.test(file))
|
|
204
|
+
return true;
|
|
205
|
+
try {
|
|
206
|
+
// gi:// imports + the process/system stub are hoisted to the top of the
|
|
207
|
+
// bundle, so a bounded prefix is enough — bundles can be several MB.
|
|
208
|
+
const head = readFileSync(file, 'utf-8').slice(0, 65536);
|
|
209
|
+
return (head.startsWith('#!/usr/bin/env -S gjs') ||
|
|
210
|
+
/(?:from|import)\s*["']gi:\/\//.test(head) ||
|
|
211
|
+
/\bimports\.(?:gi|system)\b/.test(head));
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
112
217
|
function looksLikeFile(target) {
|
|
113
218
|
if (target.startsWith('./') || target.startsWith('../') || target.startsWith('/'))
|
|
114
219
|
return true;
|
package/lib/commands/showcase.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { discoverShowcases, findShowcase } from '../utils/discover-showcases.js';
|
|
2
2
|
import { runMinimalChecks, detectPackageManager, buildInstallCommand } from '../utils/check-system-deps.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { dirname } from 'node:path';
|
|
5
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
6
|
+
import { ensurePkgDir } from './dlx.js';
|
|
7
|
+
import { parseSpec } from '../utils/parse-spec.js';
|
|
8
|
+
import { resolveNodeEntry } from '../utils/resolve-gjs-entry.js';
|
|
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';
|
|
6
12
|
function readCliVersion() {
|
|
7
13
|
try {
|
|
8
14
|
const pkgUrl = new URL('../../package.json', import.meta.url);
|
|
@@ -30,6 +36,20 @@ export const showcaseCommand = {
|
|
|
30
36
|
description: 'List available showcases',
|
|
31
37
|
type: 'boolean',
|
|
32
38
|
default: false,
|
|
39
|
+
})
|
|
40
|
+
.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.
|
|
48
|
+
type: 'string',
|
|
49
|
+
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.',
|
|
33
53
|
}),
|
|
34
54
|
handler: async (args) => {
|
|
35
55
|
// List mode: no name given, or --list flag
|
|
@@ -67,7 +87,12 @@ export const showcaseCommand = {
|
|
|
67
87
|
if (!showcase) {
|
|
68
88
|
console.error(`Unknown showcase: "${args.name}"`);
|
|
69
89
|
console.error('Run "gjsify showcase" to list available showcases.');
|
|
70
|
-
process.exit(
|
|
90
|
+
// `return process.exit()`: a bare `process.exit()` does NOT halt
|
|
91
|
+
// synchronously under GJS (no atexit, GLib loop still armed), so
|
|
92
|
+
// execution would fall through to `showcase.packageName` below on an
|
|
93
|
+
// undefined `showcase` → throw racing the scheduled exit → the
|
|
94
|
+
// `m_should_exit` core dump. Returning halts the handler here.
|
|
95
|
+
return process.exit(1);
|
|
71
96
|
}
|
|
72
97
|
// System dependency check before delegating — only system libs (gjs,
|
|
73
98
|
// gtk4, …). The showcase's npm deps (incl. `@gjsify/webgl` with the
|
|
@@ -88,11 +113,25 @@ export const showcaseCommand = {
|
|
|
88
113
|
if (cmd) {
|
|
89
114
|
console.error(`\nInstall with:\n ${cmd}`);
|
|
90
115
|
}
|
|
91
|
-
process.exit(
|
|
116
|
+
// `return` — bare `process.exit` falls through under GJS (see above).
|
|
117
|
+
return process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
const runtime = (args.runtime ?? hostRuntime());
|
|
120
|
+
if (!isExampleRuntime(runtime)) {
|
|
121
|
+
console.error(`Unknown --runtime "${runtime}" (expected: ${EXAMPLE_RUNTIMES.join(', ')}).`);
|
|
122
|
+
// `return` — bare `process.exit` falls through under GJS (see above).
|
|
123
|
+
return process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
const cliVersion = readCliVersion();
|
|
126
|
+
// Non-gjs runtime: resolve the showcase's `--app node` bundle and run it
|
|
127
|
+
// on node/bun/deno. Handled in-process (not via the GJS-only `dlx`).
|
|
128
|
+
if (runtime !== 'gjs') {
|
|
129
|
+
await runShowcaseOnRuntime(showcase, runtime, cliVersion);
|
|
130
|
+
return;
|
|
92
131
|
}
|
|
93
|
-
//
|
|
94
|
-
// same atomic symlink-swap, same `gjsify.main` resolution.
|
|
95
|
-
//
|
|
132
|
+
// gjs runtime: delegate to `gjsify dlx <package>@<cli-version>` — same
|
|
133
|
+
// npm-cache, same atomic symlink-swap, same `gjsify.main` resolution.
|
|
134
|
+
// Dispatched in-process to keep the dlx logic in one place.
|
|
96
135
|
//
|
|
97
136
|
// Pinning to the CLI's own version is load-bearing: showcases ship in
|
|
98
137
|
// lockstep with the CLI, so users running `npx @gjsify/cli@X showcase
|
|
@@ -102,26 +141,87 @@ export const showcaseCommand = {
|
|
|
102
141
|
// and the user gets a stale showcase that may be missing deps the
|
|
103
142
|
// newer CLI assumes (the `@gjsify/http-soup-bridge` regression
|
|
104
143
|
// reported against `@gjsify/cli@0.3.17`).
|
|
105
|
-
const cliVersion = readCliVersion();
|
|
106
144
|
const dlxSpec = cliVersion ? `${showcase.packageName}@${cliVersion}` : showcase.packageName;
|
|
107
145
|
console.log(`Running showcase: ${showcase.name} (via gjsify dlx ${dlxSpec})\n`);
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
child.on('error', reject);
|
|
122
|
-
}).catch((err) => {
|
|
146
|
+
// Dispatch `dlx` IN-PROCESS (not `spawn(process.execPath, [cliBin, …])`):
|
|
147
|
+
// under the committed GJS bundle `process.execPath` is `gjs` and
|
|
148
|
+
// `../index.js` is the Node entry, so the spawn ran `gjs cli.gjs.mjs
|
|
149
|
+
// index.js dlx …` (yargs saw `index.js` as the command) — the
|
|
150
|
+
// CLAUDE.md-flagged trap. In-process runCli works identically under Node
|
|
151
|
+
// and GJS and stays node-free (same fix `gjsify storybook` uses).
|
|
152
|
+
try {
|
|
153
|
+
const { runCli } = await import('../cli-app.js');
|
|
154
|
+
await runCli(['dlx', dlxSpec]);
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
123
157
|
console.error(err.message);
|
|
124
158
|
process.exit(1);
|
|
125
|
-
}
|
|
159
|
+
}
|
|
126
160
|
},
|
|
127
161
|
};
|
|
162
|
+
/**
|
|
163
|
+
* Resolve a showcase package's on-disk directory. Prefers a LOCAL install (a
|
|
164
|
+
* workspace member symlinked in node_modules — no network, and it runs the
|
|
165
|
+
* in-tree build), falling back to `gjsify dlx`'s install-into-cache for a
|
|
166
|
+
* published showcase. Returns the directory containing the package.json.
|
|
167
|
+
*/
|
|
168
|
+
async function resolveShowcaseDir(showcase, cliVersion) {
|
|
169
|
+
// Local-first: a workspace showcase resolves via its own package name.
|
|
170
|
+
try {
|
|
171
|
+
const req = createRequire(import.meta.url);
|
|
172
|
+
const localPkgJson = req.resolve(`${showcase.packageName}/package.json`);
|
|
173
|
+
if (existsSync(localPkgJson))
|
|
174
|
+
return dirname(localPkgJson);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
// Not resolvable locally — fall through to dlx install.
|
|
178
|
+
}
|
|
179
|
+
const spec = cliVersion ? `${showcase.packageName}@${cliVersion}` : showcase.packageName;
|
|
180
|
+
const { pkgDir } = await ensurePkgDir(parseSpec(spec), {
|
|
181
|
+
verbose: false,
|
|
182
|
+
cacheMaxAge: 60 * 24 * 7,
|
|
183
|
+
frozen: false,
|
|
184
|
+
});
|
|
185
|
+
return pkgDir;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Run a showcase on a non-gjs runtime (node/bun/deno). Resolves the package,
|
|
189
|
+
* validates it against the showcase's `gjsify.example.runtimes` declaration
|
|
190
|
+
* (clean error if the runtime is unsupported — e.g. a GTK/Adw showcase under
|
|
191
|
+
* node), resolves its `--app node` bundle and launches it via the shared runner.
|
|
192
|
+
*/
|
|
193
|
+
async function runShowcaseOnRuntime(showcase, runtime, cliVersion) {
|
|
194
|
+
let pkgDir;
|
|
195
|
+
try {
|
|
196
|
+
pkgDir = await resolveShowcaseDir(showcase, cliVersion);
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
console.error(`Could not resolve showcase "${showcase.name}": ${err.message}`);
|
|
200
|
+
return process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
// Declaration check — clear, actionable error for an unsupported runtime.
|
|
203
|
+
let pkg = {};
|
|
204
|
+
try {
|
|
205
|
+
pkg = JSON.parse(readFileSync(`${pkgDir}/package.json`, 'utf-8'));
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// Fall through — no declaration means permissive.
|
|
209
|
+
}
|
|
210
|
+
const declared = readDeclaredRuntimes(pkg);
|
|
211
|
+
const support = checkRuntimeSupported(runtime, declared, showcase.name);
|
|
212
|
+
if (!support.ok) {
|
|
213
|
+
console.error(support.message);
|
|
214
|
+
return process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
let entry;
|
|
217
|
+
try {
|
|
218
|
+
entry = resolveNodeEntry(pkgDir);
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
console.error(`Cannot run showcase "${showcase.name}" on ${runtime}: ${err.message}`);
|
|
222
|
+
return process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
console.log(`Running showcase: ${showcase.name} on ${runtime} (${entry})\n`);
|
|
225
|
+
// Terminal call — exit on success.
|
|
226
|
+
await runRuntimeBundle(runtime, entry, [], { exitOnSuccess: true });
|
|
227
|
+
}
|