@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
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { spawn } from 'node:child_process';
|
|
20
20
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
21
21
|
import { join, relative, resolve } from 'node:path';
|
|
22
|
+
import { hostRuntime } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
22
23
|
function readPackage(cwd) {
|
|
23
24
|
try {
|
|
24
25
|
return JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8'));
|
|
@@ -91,11 +92,15 @@ export const storybookCommand = {
|
|
|
91
92
|
type: 'string',
|
|
92
93
|
})
|
|
93
94
|
.option('runtime', {
|
|
94
|
-
description: 'Runtime to build for and launch on: gjs (default
|
|
95
|
-
'node
|
|
95
|
+
description: 'Runtime to build for and launch on: gjs | node | bun | deno (default: the host runtime). ' +
|
|
96
|
+
'node/bun/deno build the same `--app node` bundle (gi:// → @gjsify/node-gi) and require ' +
|
|
97
|
+
'@gjsify/node-gi installed in the project.',
|
|
96
98
|
type: 'string',
|
|
97
|
-
choices: ['gjs', 'node'],
|
|
98
|
-
default: 'gjs'
|
|
99
|
+
choices: ['gjs', 'node', 'bun', 'deno'],
|
|
100
|
+
// No yargs `default: 'gjs'` — a yargs default clobbers the
|
|
101
|
+
// config-file value (`gjsify.storybook.runtime`). The host
|
|
102
|
+
// default is applied in the handler: flag > config > host.
|
|
103
|
+
defaultDescription: 'host runtime (gjs on gjs, else node/bun/deno)',
|
|
99
104
|
})
|
|
100
105
|
.option('watch', {
|
|
101
106
|
description: 'Rebuild and relaunch when a story file changes',
|
|
@@ -111,12 +116,15 @@ export const storybookCommand = {
|
|
|
111
116
|
const applicationId = args.appId ?? config.applicationId ?? deriveAppId(pkg.name);
|
|
112
117
|
const title = args.title ?? config.title;
|
|
113
118
|
const globals = args.globals ?? config.globals ?? 'auto';
|
|
114
|
-
|
|
119
|
+
// Default FOLLOWS the host runtime the CLI executes in. Precedence: CLI
|
|
120
|
+
// flag > `gjsify.storybook.runtime` config > host default (`hostRuntime()`).
|
|
121
|
+
const runtime = (args.runtime ?? config.runtime ?? hostRuntime());
|
|
115
122
|
// The runtime drives BOTH the build target (`gjsify build --app <app>`)
|
|
116
|
-
// and the launcher: `gjs` builds an `--app gjs` bundle run by `gjs -m
|
|
117
|
-
// `node`
|
|
118
|
-
//
|
|
119
|
-
|
|
123
|
+
// and the launcher: `gjs` builds an `--app gjs` bundle run by `gjs -m`;
|
|
124
|
+
// `node`/`bun`/`deno` build the SAME `--app node` bundle (gi:// →
|
|
125
|
+
// @gjsify/node-gi) run on that runtime. Same generated entry +
|
|
126
|
+
// `--globals auto` for all.
|
|
127
|
+
const app = runtime === 'gjs' ? 'gjs' : 'node';
|
|
120
128
|
const cacheDir = join(cwd, 'node_modules', '.cache', 'gjsify-storybook');
|
|
121
129
|
mkdirSync(cacheDir, { recursive: true });
|
|
122
130
|
const entryPath = join(cacheDir, 'entry.ts');
|
|
@@ -137,13 +145,20 @@ export const storybookCommand = {
|
|
|
137
145
|
const { runCli } = await import('../cli-app.js');
|
|
138
146
|
const buildBundle = () => runCli(['build', entryPath, '--app', app, '--globals', globals, '--outfile', outPath]);
|
|
139
147
|
const launch = async () => {
|
|
140
|
-
if (runtime === '
|
|
148
|
+
if (runtime === 'gjs') {
|
|
149
|
+
const { runGjsBundle } = await import('../utils/run-gjs.js');
|
|
150
|
+
await runGjsBundle(outPath, [], { exitOnSuccess: true });
|
|
151
|
+
}
|
|
152
|
+
else if (runtime === 'node') {
|
|
141
153
|
const { runNodeBundle } = await import('../utils/run-node.js');
|
|
142
154
|
await runNodeBundle(outPath, [], { exitOnSuccess: true });
|
|
143
155
|
}
|
|
144
156
|
else {
|
|
145
|
-
|
|
146
|
-
|
|
157
|
+
// bun / deno — run the SAME `--app node` bundle via the shared
|
|
158
|
+
// runner (Node-API is their common ABI). node-gi is required
|
|
159
|
+
// because the storybook uses gi:// (checked in runRuntimeBundle).
|
|
160
|
+
const { runRuntimeBundle } = await import('../utils/run-node.js');
|
|
161
|
+
await runRuntimeBundle(runtime, outPath, [], { exitOnSuccess: true });
|
|
147
162
|
}
|
|
148
163
|
};
|
|
149
164
|
if (!args.watch) {
|
|
@@ -159,17 +174,29 @@ export const storybookCommand = {
|
|
|
159
174
|
// enough and stays killable for relaunch.
|
|
160
175
|
const { watch } = await import('node:fs');
|
|
161
176
|
const spawnChild = async () => {
|
|
177
|
+
if (runtime === 'gjs') {
|
|
178
|
+
return spawn('gjs', ['-m', outPath], { stdio: 'inherit' });
|
|
179
|
+
}
|
|
180
|
+
// node / bun / deno — run the `--app node` bundle with the native
|
|
181
|
+
// typelib env wired like `gjsify run`. All three need @gjsify/node-gi
|
|
182
|
+
// (the storybook uses gi://), kept external by the bundler.
|
|
183
|
+
const { computeNativeEnvForBundle } = await import('../utils/run-gjs.js');
|
|
184
|
+
const { resolveNodeGi, nodeBinary } = await import('../utils/run-node.js');
|
|
185
|
+
const { RUNTIMES } = await import('../utils/runtimes.js');
|
|
186
|
+
if (!resolveNodeGi(cwd)) {
|
|
187
|
+
throw new Error(`Cannot run the storybook on ${runtime}: \`@gjsify/node-gi\` is not installed. ` +
|
|
188
|
+
`Add @gjsify/node-gi as a dependency to run the storybook on ${runtime}.`);
|
|
189
|
+
}
|
|
190
|
+
const { env: nativeEnv } = computeNativeEnvForBundle(outPath, cwd);
|
|
191
|
+
const env = { ...process.env, ...nativeEnv };
|
|
192
|
+
// `node` uses the resolved node binary (under a GJS-hosted CLI
|
|
193
|
+
// `process.execPath` is gjs); bun/deno launch via the shared spec
|
|
194
|
+
// (deno's `--node-modules-dir=manual`, etc.).
|
|
162
195
|
if (runtime === 'node') {
|
|
163
|
-
|
|
164
|
-
const { resolveNodeGi, nodeBinary } = await import('../utils/run-node.js');
|
|
165
|
-
if (!resolveNodeGi(cwd)) {
|
|
166
|
-
throw new Error('Cannot run the storybook on Node: `@gjsify/node-gi` is not installed. ' +
|
|
167
|
-
'Add @gjsify/node-gi as a dependency to run the storybook on Node.');
|
|
168
|
-
}
|
|
169
|
-
const { env: nativeEnv } = computeNativeEnvForBundle(outPath, cwd);
|
|
170
|
-
return spawn(nodeBinary(), [outPath], { stdio: 'inherit', env: { ...process.env, ...nativeEnv } });
|
|
196
|
+
return spawn(nodeBinary(), [outPath], { stdio: 'inherit', env });
|
|
171
197
|
}
|
|
172
|
-
|
|
198
|
+
const [cmd, launchArgs] = RUNTIMES[runtime].launch(outPath);
|
|
199
|
+
return spawn(cmd, launchArgs, { stdio: 'inherit', env });
|
|
173
200
|
};
|
|
174
201
|
let child = null;
|
|
175
202
|
let rebuilding = false;
|
package/lib/config.js
CHANGED
|
@@ -2,7 +2,8 @@ import { APP_NAME } from './constants.js';
|
|
|
2
2
|
import { cosmiconfig } from 'cosmiconfig';
|
|
3
3
|
import { basename } from 'node:path';
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
|
-
import { isGjs } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
5
|
+
import { isGjs, hostRuntime } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
6
|
+
import { buildAppForRuntime } from './utils/runtimes.js';
|
|
6
7
|
/**
|
|
7
8
|
* Does the failed `import()` of a config look like a MODULE-RESOLUTION failure
|
|
8
9
|
* (the config imports something GJS's ESM loader can't resolve for an external
|
|
@@ -227,6 +228,16 @@ export class Config {
|
|
|
227
228
|
configData.exclude = cliArgs.exclude || [];
|
|
228
229
|
if (cliArgs.consoleShim !== undefined)
|
|
229
230
|
configData.consoleShim = cliArgs.consoleShim;
|
|
231
|
+
// Default `--app` FOLLOWS the host runtime the CLI executes in: gjs when
|
|
232
|
+
// run under gjs, node when run under node/bun/deno (both consume the
|
|
233
|
+
// `--app node` bundle). Applied post-merge (not as a yargs `default:`)
|
|
234
|
+
// so a `gjsify.app` declared in package.json#gjsify / `.gjsifyrc.*`
|
|
235
|
+
// survives — a yargs default is indistinguishable from a user value and
|
|
236
|
+
// would clobber the config one. Precedence: CLI flag > config file >
|
|
237
|
+
// host default (same pattern as `globals` / `bundler.input`).
|
|
238
|
+
if (cliArgs.app !== undefined)
|
|
239
|
+
configData.app = cliArgs.app;
|
|
240
|
+
configData.app ??= buildAppForRuntime(hostRuntime());
|
|
230
241
|
if (cliArgs.globals !== undefined)
|
|
231
242
|
configData.globals = cliArgs.globals;
|
|
232
243
|
// Fallback applied post-merge (not as a yargs `default:`) so a
|
package/lib/index.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// This file is just the executable wrapper around `runCli`.
|
|
7
7
|
import { hideBin } from 'yargs/helpers';
|
|
8
8
|
import { runCli } from './cli-app.js';
|
|
9
|
+
import { gjsExit } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
9
10
|
try {
|
|
10
11
|
await runCli(hideBin(process.argv));
|
|
11
12
|
}
|
|
@@ -23,6 +24,5 @@ catch (err) {
|
|
|
23
24
|
// atexit hook and would exit 0 at natural shutdown, so force a non-zero exit
|
|
24
25
|
// via `imports.system.exit` there (no-op on Node, where `imports` is unset).
|
|
25
26
|
process.exitCode = 1;
|
|
26
|
-
|
|
27
|
-
gjs?.system?.exit?.(1);
|
|
27
|
+
gjsExit(1);
|
|
28
28
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RolldownOptions, OutputOptions, RolldownPluginOption } from 'rolldown';
|
|
2
|
+
import type { App } from '@gjsify/rolldown-plugin-gjsify';
|
|
2
3
|
import type { ConfigDataLibrary, ConfigDataTypescript } from './index.js';
|
|
3
4
|
/**
|
|
4
5
|
* Plugin entry resolvable by package name from the project's `node_modules`.
|
|
@@ -64,6 +65,14 @@ export interface LegacyEsbuildOptions {
|
|
|
64
65
|
export interface ConfigData {
|
|
65
66
|
/** Switch on the verbose mode */
|
|
66
67
|
verbose?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* The build target for an application build (`gjs` | `node` | `browser` |
|
|
70
|
+
* `nativescript`). Resolved in `Config.forBuild` as CLI flag (`--app`) >
|
|
71
|
+
* `package.json#gjsify.app` > host-runtime default (`gjs` when the CLI runs
|
|
72
|
+
* under gjs, `node` when it runs under node/bun/deno). Consumed by the build
|
|
73
|
+
* handler and passed to `BuildAction.start`.
|
|
74
|
+
*/
|
|
75
|
+
app?: App;
|
|
67
76
|
/**
|
|
68
77
|
* Bundler-level options forwarded to Rolldown. Replaces the legacy
|
|
69
78
|
* `esbuild` field. The orchestrator applies platform-specific defaults
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
// their full transitive prebuild set picked up automatically. The
|
|
11
11
|
// transitive walk is what makes `gjsify showcase` / `gjsify dlx` work
|
|
12
12
|
// for packages whose Vala typelibs live in *indirect* deps.
|
|
13
|
-
import { readdirSync, existsSync
|
|
13
|
+
import { readdirSync, existsSync } from 'node:fs';
|
|
14
14
|
import { join, resolve } from 'node:path';
|
|
15
|
+
import { readPackageJson } from './pkg-json.js';
|
|
15
16
|
/** Map Node.js process.arch values to the convention used in prebuilds/ directories. */
|
|
16
17
|
function nodeArchToLinuxArch(arch) {
|
|
17
18
|
const map = {
|
|
@@ -22,15 +23,6 @@ function nodeArchToLinuxArch(arch) {
|
|
|
22
23
|
};
|
|
23
24
|
return map[arch] ?? arch;
|
|
24
25
|
}
|
|
25
|
-
/** Read a package.json file and return its parsed content, or null on error. */
|
|
26
|
-
function readPackageJson(pkgJsonPath) {
|
|
27
|
-
try {
|
|
28
|
-
return JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
|
|
29
|
-
}
|
|
30
|
-
catch {
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
26
|
/**
|
|
35
27
|
* Scan all packages in a node_modules directory for gjsify native prebuilds.
|
|
36
28
|
* Handles scoped packages (@scope/name) as well as flat packages.
|
|
@@ -9,8 +9,16 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
9
9
|
import { dirname, join } from 'node:path';
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
11
|
function manifestPath() {
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
// `showcases.json` lives at the CLI package root. Its position relative to
|
|
14
|
+
// this module depends on how the CLI is loaded:
|
|
15
|
+
// - `lib/utils/discover-showcases.js` (Node/Bun/Deno) → `../../showcases.json`.
|
|
16
|
+
// - the committed `dist/cli.gjs.mjs` GJS bundle (`gjsify showcase` under
|
|
17
|
+
// gjs) → `import.meta.url` is `dist/cli.gjs.mjs`, so the manifest is one
|
|
18
|
+
// level up (`../showcases.json`), not two — without this the gjs CLI
|
|
19
|
+
// finds no manifest and `showcase` wrongly reports "No showcases found".
|
|
20
|
+
const candidates = [join(here, '..', '..', 'showcases.json'), join(here, '..', 'showcases.json')];
|
|
21
|
+
return candidates.find((c) => existsSync(c)) ?? candidates[0];
|
|
14
22
|
}
|
|
15
23
|
/**
|
|
16
24
|
* Read the curated showcase list from `showcases.json`. Returns showcases
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// confusing crash with no actionable message (observed: Node 22 in a Fedora
|
|
8
8
|
// container segfaults `gjsify install --backend native` during download/extract).
|
|
9
9
|
// This preflight turns that into a clear, actionable error.
|
|
10
|
-
import { isNode } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
10
|
+
import { isNode, gjsSystemVersion } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
11
11
|
/**
|
|
12
12
|
* Minimum Node.js major the native install backend supports — its prebuilt native
|
|
13
13
|
* modules are ABI-locked to this line (kept in sync with `.nvmrc`).
|
|
@@ -39,7 +39,7 @@ function onRealNode() {
|
|
|
39
39
|
// GJS exposes `imports.system.version`; @gjsify/process fakes
|
|
40
40
|
// `process.versions.node = '20.0.0'` for compat, so check GJS FIRST — a plain
|
|
41
41
|
// `process.versions.node` read would be a false Node positive under GJS.
|
|
42
|
-
const sysVersion =
|
|
42
|
+
const sysVersion = gjsSystemVersion();
|
|
43
43
|
if (sysVersion !== undefined)
|
|
44
44
|
return false;
|
|
45
45
|
return isNode();
|
package/lib/utils/oxc-resolve.js
CHANGED
|
@@ -54,7 +54,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
54
54
|
import { findWorkspaceRoot } from './workspace-root.js';
|
|
55
55
|
import { resolveNpmPackage } from './resolve-npm-package.js';
|
|
56
56
|
import { nodeBinary } from './run-node.js';
|
|
57
|
-
import { isGjs } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
57
|
+
import { isGjs, gjsExit } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
58
58
|
/**
|
|
59
59
|
* Map Node.js `process.platform` + `arch` (+ musl on Linux) to the
|
|
60
60
|
* napi-rs binding-package suffix oxc uses, e.g. `linux-x64-gnu`,
|
|
@@ -408,9 +408,7 @@ export function setOxcExitCode(code) {
|
|
|
408
408
|
process.exitCode = code;
|
|
409
409
|
if (code === 0)
|
|
410
410
|
return;
|
|
411
|
-
|
|
412
|
-
if (gjs?.system?.exit)
|
|
413
|
-
gjs.system.exit(code);
|
|
411
|
+
gjsExit(code);
|
|
414
412
|
}
|
|
415
413
|
/**
|
|
416
414
|
* Has the given oxc tool's npm package (or its companion) been declared in the
|
|
@@ -11,6 +11,8 @@ export interface PackageJson {
|
|
|
11
11
|
devDependencies?: Record<string, string>;
|
|
12
12
|
peerDependencies?: Record<string, string>;
|
|
13
13
|
optionalDependencies?: Record<string, string>;
|
|
14
|
+
scripts?: Record<string, string>;
|
|
15
|
+
private?: boolean;
|
|
14
16
|
[key: string]: unknown;
|
|
15
17
|
}
|
|
16
18
|
export declare function readPackageJson(pkgPath: string): PackageJson | null;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { PackageJson } from './pkg-json-edit.js';
|
|
2
|
+
/**
|
|
3
|
+
* Read + parse a `package.json` at `pkgPath`. Permissive: any error (missing
|
|
4
|
+
* file, unreadable, invalid JSON) yields `null`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function readPackageJson(pkgPath: string): PackageJson | null;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Single permissive reader for `package.json` files, shared by the CLI's
|
|
2
|
+
// workspace- and dependency-discovery paths (`gjsify run`/`workspace`/`foreach`/
|
|
3
|
+
// `check`, native-prebuild scanning). Returns `null` when the file is missing
|
|
4
|
+
// OR unparseable — discovery must degrade gracefully on a stray or corrupt
|
|
5
|
+
// manifest rather than crash the whole command.
|
|
6
|
+
//
|
|
7
|
+
// The install-save path keeps its own STRICTER reader in `pkg-json-edit.ts`
|
|
8
|
+
// (`readPackageJson` there): it returns `null` only when the file is absent but
|
|
9
|
+
// THROWS on invalid JSON, so a corrupt manifest is never silently overwritten
|
|
10
|
+
// during `gjsify install <pkg>`. That contract is intentional and load-bearing
|
|
11
|
+
// — do not fold it into this one.
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
/**
|
|
14
|
+
* Read + parse a `package.json` at `pkgPath`. Permissive: any error (missing
|
|
15
|
+
* file, unreadable, invalid JSON) yields `null`.
|
|
16
|
+
*/
|
|
17
|
+
export function readPackageJson(pkgPath) {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -4,4 +4,15 @@ interface ResolvedEntry {
|
|
|
4
4
|
fromFallback: boolean;
|
|
5
5
|
}
|
|
6
6
|
export declare function resolveGjsEntry(pkgDir: string, binName: string | null): ResolvedEntry;
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the `--app node` bundle of a package for the non-gjs runtimes
|
|
9
|
+
* (node/bun/deno all consume it). Resolution order:
|
|
10
|
+
* 1. explicit `gjsify.example.node` (a package can name its node bundle)
|
|
11
|
+
* 2. convention: the node twin of the GJS entry — swap the `<name>.gjs.<ext>`
|
|
12
|
+
* infix to `<name>.node.<ext>` and probe `.node.{mjs,js,cjs}` (gjsify's
|
|
13
|
+
* `build:node → dist/<name>.node.mjs` convention; the GJS entry is
|
|
14
|
+
* `dist/<name>.gjs.js`, so the extension can differ).
|
|
15
|
+
* Hard-fails with an actionable hint when neither is found.
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveNodeEntry(pkgDir: string): string;
|
|
7
18
|
export {};
|
|
@@ -63,3 +63,45 @@ export function resolveGjsEntry(pkgDir, binName) {
|
|
|
63
63
|
}
|
|
64
64
|
return { bundlePath, binName: resolvedBin, fromFallback };
|
|
65
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Resolve the `--app node` bundle of a package for the non-gjs runtimes
|
|
68
|
+
* (node/bun/deno all consume it). Resolution order:
|
|
69
|
+
* 1. explicit `gjsify.example.node` (a package can name its node bundle)
|
|
70
|
+
* 2. convention: the node twin of the GJS entry — swap the `<name>.gjs.<ext>`
|
|
71
|
+
* infix to `<name>.node.<ext>` and probe `.node.{mjs,js,cjs}` (gjsify's
|
|
72
|
+
* `build:node → dist/<name>.node.mjs` convention; the GJS entry is
|
|
73
|
+
* `dist/<name>.gjs.js`, so the extension can differ).
|
|
74
|
+
* Hard-fails with an actionable hint when neither is found.
|
|
75
|
+
*/
|
|
76
|
+
export function resolveNodeEntry(pkgDir) {
|
|
77
|
+
const pkgJsonPath = join(pkgDir, 'package.json');
|
|
78
|
+
if (!existsSync(pkgJsonPath)) {
|
|
79
|
+
throw new Error(`no package.json found at ${pkgDir}`);
|
|
80
|
+
}
|
|
81
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
|
|
82
|
+
// 1. Explicit declaration wins.
|
|
83
|
+
const explicit = pkg.gjsify?.example?.node;
|
|
84
|
+
if (explicit) {
|
|
85
|
+
const p = resolve(pkgDir, explicit);
|
|
86
|
+
if (!existsSync(p)) {
|
|
87
|
+
throw new Error(`declared node entry not found: ${p} (gjsify.example.node)`);
|
|
88
|
+
}
|
|
89
|
+
return p;
|
|
90
|
+
}
|
|
91
|
+
// 2. Derive from the GJS entry.
|
|
92
|
+
const gjsEntry = pkg.gjsify?.main ?? (pkg.gjsify?.bin ? Object.values(pkg.gjsify.bin)[0] : undefined) ?? pkg.main;
|
|
93
|
+
if (!gjsEntry) {
|
|
94
|
+
throw new Error(`package "${pkg.name ?? pkgDir}" has no entry to derive a node bundle from — ` +
|
|
95
|
+
'set `gjsify.example.node` to its `--app node` bundle path.');
|
|
96
|
+
}
|
|
97
|
+
const stem = gjsEntry.replace(/\.gjs\.(js|mjs|cjs)$/, '').replace(/\.(js|mjs|cjs)$/, '');
|
|
98
|
+
const candidates = [`${stem}.node.mjs`, `${stem}.node.js`, `${stem}.node.cjs`];
|
|
99
|
+
for (const c of candidates) {
|
|
100
|
+
const p = resolve(pkgDir, c);
|
|
101
|
+
if (existsSync(p))
|
|
102
|
+
return p;
|
|
103
|
+
}
|
|
104
|
+
throw new Error(`package "${pkg.name ?? pkgDir}" has no \`--app node\` bundle — looked for ${candidates.join(', ')}.\n` +
|
|
105
|
+
' Build one (e.g. `gjsify build src/app.ts --app node --outfile dist/app.node.mjs`) ' +
|
|
106
|
+
'or set `gjsify.example.node` to its path.');
|
|
107
|
+
}
|
package/lib/utils/run-gjs.js
CHANGED
|
@@ -53,6 +53,7 @@ export async function runGjsBundle(bundlePath, extraArgs = [], options = {}) {
|
|
|
53
53
|
const gjsCommand = ['gjs', ...gjsArgs.map((a) => (a.includes(' ') ? `"${a}"` : a))].join(' ');
|
|
54
54
|
console.error(`$ ${envPrefix ? `${envPrefix} ` : ''}${gjsCommand}`);
|
|
55
55
|
const child = spawn('gjs', gjsArgs, { env, stdio: 'inherit' });
|
|
56
|
+
let failed = false;
|
|
56
57
|
await new Promise((resolvePromise, reject) => {
|
|
57
58
|
child.on('close', (code) => {
|
|
58
59
|
if (code !== 0) {
|
|
@@ -65,10 +66,22 @@ export async function runGjsBundle(bundlePath, extraArgs = [], options = {}) {
|
|
|
65
66
|
child.on('error', reject);
|
|
66
67
|
}).catch((err) => {
|
|
67
68
|
console.error(err.message);
|
|
68
|
-
|
|
69
|
+
failed = true;
|
|
69
70
|
});
|
|
71
|
+
if (failed) {
|
|
72
|
+
// Set `process.exitCode` synchronously BEFORE exiting: under GJS
|
|
73
|
+
// `process.exit()` is deferred (the GLib main loop is armed), so when
|
|
74
|
+
// this runs via run.ts's in-process script dispatch, execution returns
|
|
75
|
+
// to the caller, which propagates by reading `process.exitCode`. Without
|
|
76
|
+
// this, `gjsify run <script>` whose body is `gjsify run <bundle>`
|
|
77
|
+
// swallowed a non-zero gjs exit and returned 0 — masking a failing
|
|
78
|
+
// `test:gjs` in the `test` script chain (and, via `gjsify foreach test`,
|
|
79
|
+
// in CI). The `return` guards against a fall-through second `exit`.
|
|
80
|
+
process.exitCode = 1;
|
|
81
|
+
return process.exit(1);
|
|
82
|
+
}
|
|
70
83
|
// See RunGjsBundleOptions.exitOnSuccess — terminal callers opt in,
|
|
71
84
|
// mid-flow callers (test.ts's runtime loop) keep the process alive.
|
|
72
85
|
if (options.exitOnSuccess)
|
|
73
|
-
process.exit(0);
|
|
86
|
+
return process.exit(0);
|
|
74
87
|
}
|
package/lib/utils/run-node.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ExampleRuntime } from './runtimes.js';
|
|
1
2
|
/**
|
|
2
3
|
* Walk up from `startDir` looking for `<node_modules>/@gjsify/node-gi`. Returns
|
|
3
4
|
* the package directory, or null when it is not installed anywhere on the chain.
|
|
@@ -18,3 +19,23 @@ export interface RunNodeBundleOptions {
|
|
|
18
19
|
* throws a clear, actionable error otherwise.
|
|
19
20
|
*/
|
|
20
21
|
export declare function runNodeBundle(bundlePath: string, extraArgs?: string[], options?: RunNodeBundleOptions): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Whether a `--app node` bundle actually consumes `@gjsify/node-gi` (the gi://
|
|
24
|
+
* reverse bridge is kept EXTERNAL, so a bundle that uses gi:// imports the shim
|
|
25
|
+
* by name). A plain Node app (express, a pure `node:*` program) does NOT, and
|
|
26
|
+
* must run on node/bun/deno WITHOUT node-gi installed — so the node-gi check is
|
|
27
|
+
* conditional on this, not unconditional like the storybook path.
|
|
28
|
+
*/
|
|
29
|
+
export declare function bundleRequiresNodeGi(bundlePath: string): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Run a built bundle on a non-gjs runtime (node / bun / deno). All three consume
|
|
32
|
+
* the SAME `--app node` bundle (Node-API is their common ABI), so this is the
|
|
33
|
+
* shared launcher `gjsify run --runtime` and `gjsify showcase --runtime` use for
|
|
34
|
+
* node/bun/deno. The native typelib env is wired exactly like {@link runGjsBundle}
|
|
35
|
+
* (so a bundle pulling an `@gjsify/*` Vala bridge resolves its prebuild), and
|
|
36
|
+
* `@gjsify/node-gi` is required ONLY when the bundle actually uses gi://.
|
|
37
|
+
*
|
|
38
|
+
* `gjs` is intentionally NOT handled here — the caller dispatches it to
|
|
39
|
+
* {@link runGjsBundle}.
|
|
40
|
+
*/
|
|
41
|
+
export declare function runRuntimeBundle(runtime: Exclude<ExampleRuntime, 'gjs'>, bundlePath: string, extraArgs?: string[], options?: RunNodeBundleOptions): Promise<void>;
|
package/lib/utils/run-node.js
CHANGED
|
@@ -20,10 +20,11 @@
|
|
|
20
20
|
// the PATH `node` otherwise (under the committed GJS bundle `process.execPath` is
|
|
21
21
|
// `gjs`, which cannot run a `--app node` bundle).
|
|
22
22
|
import { spawn } from 'node:child_process';
|
|
23
|
-
import { existsSync } from 'node:fs';
|
|
23
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
24
24
|
import { join, resolve } from 'node:path';
|
|
25
25
|
import { isNode } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
26
26
|
import { computeNativeEnvForBundle } from './run-gjs.js';
|
|
27
|
+
import { RUNTIMES } from './runtimes.js';
|
|
27
28
|
/**
|
|
28
29
|
* Walk up from `startDir` looking for `<node_modules>/@gjsify/node-gi`. Returns
|
|
29
30
|
* the package directory, or null when it is not installed anywhere on the chain.
|
|
@@ -83,3 +84,63 @@ export async function runNodeBundle(bundlePath, extraArgs = [], options = {}) {
|
|
|
83
84
|
if (options.exitOnSuccess)
|
|
84
85
|
process.exit(0);
|
|
85
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Whether a `--app node` bundle actually consumes `@gjsify/node-gi` (the gi://
|
|
89
|
+
* reverse bridge is kept EXTERNAL, so a bundle that uses gi:// imports the shim
|
|
90
|
+
* by name). A plain Node app (express, a pure `node:*` program) does NOT, and
|
|
91
|
+
* must run on node/bun/deno WITHOUT node-gi installed — so the node-gi check is
|
|
92
|
+
* conditional on this, not unconditional like the storybook path.
|
|
93
|
+
*/
|
|
94
|
+
export function bundleRequiresNodeGi(bundlePath) {
|
|
95
|
+
try {
|
|
96
|
+
return readFileSync(bundlePath, 'utf-8').includes('@gjsify/node-gi');
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Run a built bundle on a non-gjs runtime (node / bun / deno). All three consume
|
|
104
|
+
* the SAME `--app node` bundle (Node-API is their common ABI), so this is the
|
|
105
|
+
* shared launcher `gjsify run --runtime` and `gjsify showcase --runtime` use for
|
|
106
|
+
* node/bun/deno. The native typelib env is wired exactly like {@link runGjsBundle}
|
|
107
|
+
* (so a bundle pulling an `@gjsify/*` Vala bridge resolves its prebuild), and
|
|
108
|
+
* `@gjsify/node-gi` is required ONLY when the bundle actually uses gi://.
|
|
109
|
+
*
|
|
110
|
+
* `gjs` is intentionally NOT handled here — the caller dispatches it to
|
|
111
|
+
* {@link runGjsBundle}.
|
|
112
|
+
*/
|
|
113
|
+
export async function runRuntimeBundle(runtime, bundlePath, extraArgs = [], options = {}) {
|
|
114
|
+
const cwd = process.cwd();
|
|
115
|
+
const resolvedBundle = resolve(bundlePath);
|
|
116
|
+
if (bundleRequiresNodeGi(resolvedBundle) && !resolveNodeGi(cwd)) {
|
|
117
|
+
throw new Error(`Cannot run this bundle on ${runtime}: it uses gi:// via \`@gjsify/node-gi\`, which is not installed.\n` +
|
|
118
|
+
'Add @gjsify/node-gi as a dependency to run gi:// code off GJS, e.g.:\n' +
|
|
119
|
+
' "dependencies": { "@gjsify/node-gi": "^0.13.0" }');
|
|
120
|
+
}
|
|
121
|
+
const { env: nativeEnv, envPrefix } = computeNativeEnvForBundle(resolvedBundle, cwd);
|
|
122
|
+
const env = { ...process.env, ...nativeEnv };
|
|
123
|
+
// Reuse the shared RUNTIMES launch spec for the argv (deno's
|
|
124
|
+
// `--node-modules-dir=manual`, etc.), substituting the resolved node binary
|
|
125
|
+
// for the `node` runtime (under a GJS-hosted CLI `process.execPath` is gjs).
|
|
126
|
+
const [, launchArgs] = RUNTIMES[runtime].launch(resolvedBundle, extraArgs);
|
|
127
|
+
const cmd = runtime === 'node' ? nodeBinary() : RUNTIMES[runtime].probe;
|
|
128
|
+
// Echo on stderr so a child speaking a protocol on stdout stays clean.
|
|
129
|
+
const shown = [cmd, ...launchArgs.map((a) => (a.includes(' ') ? `"${a}"` : a))].join(' ');
|
|
130
|
+
console.error(`$ ${envPrefix ? `${envPrefix} ` : ''}${shown}`);
|
|
131
|
+
const child = spawn(cmd, launchArgs, { env, stdio: 'inherit' });
|
|
132
|
+
await new Promise((resolvePromise, reject) => {
|
|
133
|
+
child.on('close', (code) => {
|
|
134
|
+
if (code !== 0)
|
|
135
|
+
reject(new Error(`${runtime} exited with code ${code}`));
|
|
136
|
+
else
|
|
137
|
+
resolvePromise();
|
|
138
|
+
});
|
|
139
|
+
child.on('error', reject);
|
|
140
|
+
}).catch((err) => {
|
|
141
|
+
console.error(err.message);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
});
|
|
144
|
+
if (options.exitOnSuccess)
|
|
145
|
+
process.exit(0);
|
|
146
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** The runtimes a built gjsify example/showcase can run on. `gjs` is the reference. */
|
|
2
|
+
export type ExampleRuntime = 'gjs' | 'node' | 'bun' | 'deno';
|
|
3
|
+
/** All runtimes, in canonical order (gjs first — it is the reference). */
|
|
4
|
+
export declare const EXAMPLE_RUNTIMES: readonly ExampleRuntime[];
|
|
5
|
+
export interface RuntimeSpec {
|
|
6
|
+
/** Binary probed on PATH to decide availability (`<probe> --version`). */
|
|
7
|
+
readonly probe: string;
|
|
8
|
+
/**
|
|
9
|
+
* The `gjsify build --app <buildApp>` target this runtime consumes. `gjs`
|
|
10
|
+
* runs the native `--app gjs` bundle; node/bun/deno all reuse the SAME
|
|
11
|
+
* `--app node` bundle (gi:// → `@gjsify/node-gi`; Node-API is their common
|
|
12
|
+
* ABI), so bun/deno never need their own build target.
|
|
13
|
+
*/
|
|
14
|
+
readonly buildApp: 'gjs' | 'node';
|
|
15
|
+
/** argv to launch a built `entry` (+ optional extra args) under this runtime. */
|
|
16
|
+
readonly launch: (entry: string, extraArgs?: readonly string[]) => [string, string[]];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The runtime map — the generalization of the node-gi harness's `RUNTIMES`.
|
|
20
|
+
* `deno` uses `--node-modules-dir=manual`: `auto` would re-resolve the example's
|
|
21
|
+
* heavy build-time dep tree (`@gjsify/cli` + platform binaries) and hang, when
|
|
22
|
+
* the app only needs the already-linked runtime dep `@gjsify/node-gi`.
|
|
23
|
+
*/
|
|
24
|
+
export declare const RUNTIMES: Record<ExampleRuntime, RuntimeSpec>;
|
|
25
|
+
/** Type guard: is `value` one of the four known runtimes? */
|
|
26
|
+
export declare function isExampleRuntime(value: string): value is ExampleRuntime;
|
|
27
|
+
/** The `gjsify build --app <target>` a runtime consumes (`gjs`, else `node`). */
|
|
28
|
+
export declare function buildAppForRuntime(runtime: ExampleRuntime): 'gjs' | 'node';
|
|
29
|
+
/** Whether `<probe> --version` succeeds (i.e. the runtime binary is on PATH). */
|
|
30
|
+
export declare function isRuntimeOnPath(probe: string): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Whether a specific runtime is runnable here. The HOST runtime (the one the
|
|
33
|
+
* CLI itself executes in — gjs / node / bun / deno) is always available: we are
|
|
34
|
+
* running on it, so no PATH probe is needed. Every other runtime is probed via
|
|
35
|
+
* `<binary> --version`.
|
|
36
|
+
*/
|
|
37
|
+
export declare function isRuntimeAvailable(runtime: ExampleRuntime): boolean;
|
|
38
|
+
/** Which of gjs/node/bun/deno are runnable on this host. */
|
|
39
|
+
export declare function availableRuntimes(): ExampleRuntime[];
|
|
40
|
+
interface ExampleGjsifyField {
|
|
41
|
+
example?: {
|
|
42
|
+
runtimes?: unknown;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Read a package's declared example runtimes. Returns the (validated) list, or
|
|
47
|
+
* `null` when the package declares none — the "unconstrained / permissive" case.
|
|
48
|
+
* Unknown entries in the list are dropped (so a future runtime name doesn't hard
|
|
49
|
+
* error against an older CLI).
|
|
50
|
+
*/
|
|
51
|
+
export declare function readDeclaredRuntimes(pkg: {
|
|
52
|
+
gjsify?: ExampleGjsifyField;
|
|
53
|
+
} | null | undefined): ExampleRuntime[] | null;
|
|
54
|
+
export interface RuntimeSupportResult {
|
|
55
|
+
ok: boolean;
|
|
56
|
+
/** A clear, actionable message when `ok === false`; undefined otherwise. */
|
|
57
|
+
message?: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Validate a requested runtime against a package's declaration. A `null`
|
|
61
|
+
* declaration is permissive (always ok). When the runtime is not in the
|
|
62
|
+
* declared set, returns a clear, actionable error message (never throws) so
|
|
63
|
+
* callers can print it and exit cleanly.
|
|
64
|
+
*/
|
|
65
|
+
export declare function checkRuntimeSupported(runtime: ExampleRuntime, declared: ExampleRuntime[] | null, name: string): RuntimeSupportResult;
|
|
66
|
+
export {};
|