@gjsify/cli 0.13.1 → 0.14.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 +78 -71
- package/lib/actions/build.js +31 -1
- package/lib/commands/affected.js +8 -1
- package/lib/commands/install.js +6 -0
- package/lib/commands/storybook.d.ts +1 -0
- package/lib/commands/storybook.js +51 -9
- package/lib/utils/install-backend-native.js +35 -2
- package/lib/utils/install-backend.d.ts +13 -0
- package/lib/utils/node-version.d.ts +21 -0
- package/lib/utils/node-version.js +56 -0
- package/lib/utils/run-node.d.ts +20 -0
- package/lib/utils/run-node.js +85 -0
- package/package.json +18 -17
package/lib/actions/build.js
CHANGED
|
@@ -2,7 +2,7 @@ import { runBundle, runWatch, bundleToChunks } from '../bundler-pick.js';
|
|
|
2
2
|
import { gjsifyPlugin, textLoaderPlugin, resolveShebangLine, NODE_SHEBANG } from '@gjsify/rolldown-plugin-gjsify';
|
|
3
3
|
import { isGjs } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
4
4
|
import { resolveUserPlugins } from '../utils/resolve-plugin-by-name.js';
|
|
5
|
-
import { resolveGlobalsList, writeRegisterInjectFile, detectAutoGlobals } from '@gjsify/rolldown-plugin-gjsify/globals';
|
|
5
|
+
import { resolveGlobalsList, writeRegisterInjectFile, detectAutoGlobals, detectNodeGiGlobals, } from '@gjsify/rolldown-plugin-gjsify/globals';
|
|
6
6
|
import { pnpPlugin } from '@gjsify/rolldown-plugin-pnp';
|
|
7
7
|
import { dirname, extname, join } from 'node:path';
|
|
8
8
|
import { pathToFileURL } from 'node:url';
|
|
@@ -402,6 +402,36 @@ export class BuildAction {
|
|
|
402
402
|
{ extraGlobalsList: extras, excludeGlobals, cwd: process.cwd() }, bundleToChunks);
|
|
403
403
|
pluginOpts.autoGlobalsInject = injectPath;
|
|
404
404
|
}
|
|
405
|
+
else if (app === 'node' && autoMode) {
|
|
406
|
+
// `--app node` reverse direction: detect GJS ambient globals
|
|
407
|
+
// (`print`/`imports`/…) in the bundled, tree-shaken output and
|
|
408
|
+
// inject `@gjsify/node-gi/globals` only when present. A single
|
|
409
|
+
// in-memory analysis pass (the shim is external — no convergence
|
|
410
|
+
// loop). Gated on `autoMode` so `--globals none` opts out. The
|
|
411
|
+
// detection must run BEFORE the final build so the entry can be
|
|
412
|
+
// wrapped; nothing is injected when no globals are referenced (the
|
|
413
|
+
// eager-native-load regression guard).
|
|
414
|
+
const gjsifyPluginFactory = async (opts) => {
|
|
415
|
+
const cfg = await gjsifyPlugin({
|
|
416
|
+
input: userBundler.input,
|
|
417
|
+
output: { file: outfile, dir: outdir },
|
|
418
|
+
userExternal,
|
|
419
|
+
userBanner,
|
|
420
|
+
userAliases: aliases,
|
|
421
|
+
shebang: this.configData.shebang,
|
|
422
|
+
}, opts);
|
|
423
|
+
return { options: cfg.options, plugins: cfg.plugins };
|
|
424
|
+
};
|
|
425
|
+
const needsGiGlobals = await detectNodeGiGlobals({
|
|
426
|
+
input: userBundler.input,
|
|
427
|
+
plugins: [...pnpPlugins, ...userPlugins],
|
|
428
|
+
external: userBundler.external,
|
|
429
|
+
transform: userBundler.transform,
|
|
430
|
+
format,
|
|
431
|
+
}, pluginOpts, gjsifyPluginFactory, bundleToChunks);
|
|
432
|
+
if (needsGiGlobals)
|
|
433
|
+
pluginOpts.nodeGiGlobalsInject = true;
|
|
434
|
+
}
|
|
405
435
|
else if (extras) {
|
|
406
436
|
pluginOpts.autoGlobalsInject = await this.resolveGlobalsInject(app, extras, verbose);
|
|
407
437
|
}
|
package/lib/commands/affected.js
CHANGED
|
@@ -119,8 +119,15 @@ const IGNORE = [
|
|
|
119
119
|
/^refs\//,
|
|
120
120
|
/^website\//,
|
|
121
121
|
/^docs\//,
|
|
122
|
-
/^\.github\/workflows\/(deploy-docs|commitlint|release|audit-runtimes|prebuilds)\.yml$/,
|
|
122
|
+
/^\.github\/workflows\/(deploy-docs|commitlint|release|audit-runtimes|prebuilds|node-gi)\.yml$/,
|
|
123
123
|
/^\.githooks\//,
|
|
124
|
+
// @gjsify/node-gi (the Node-native GObject-Introspection engine, Axis 5) is
|
|
125
|
+
// NOT a gjsify workspace member — the GJS-first install/foreach tooling can't
|
|
126
|
+
// build its node-gyp addon, so the main workflow neither builds nor tests it.
|
|
127
|
+
// Its own `.github/workflows/node-gi.yml` is the source of truth (built +
|
|
128
|
+
// tested on Node). Without this carve-out its files map to no workspace and
|
|
129
|
+
// land in `unmatched`, forcing a conservative full run on every node-gi PR.
|
|
130
|
+
/^packages\/node-gi\//,
|
|
124
131
|
// Flatpak build/distribution tooling (SDK-extension manifest + metainfo).
|
|
125
132
|
// Like `.githooks/`, it has no package-test consumers; its own
|
|
126
133
|
// `tests/e2e/flatpak-sdk-extension` runs on `tests/e2e/**` / global triggers.
|
package/lib/commands/install.js
CHANGED
|
@@ -619,6 +619,11 @@ async function workspaceInstall(cwd, args, signal) {
|
|
|
619
619
|
overrides,
|
|
620
620
|
signal,
|
|
621
621
|
progress,
|
|
622
|
+
// Workspace-named packages are symlinked above — the native backend
|
|
623
|
+
// must never fetch a same-named published version over those
|
|
624
|
+
// symlinks (e.g. a `types-dev/<lib>` workspace that also exists on
|
|
625
|
+
// npm and is pinned transitively in the lockfile).
|
|
626
|
+
workspaceNames: new Set(byName.keys()),
|
|
622
627
|
});
|
|
623
628
|
}
|
|
624
629
|
else if (args.verbose) {
|
|
@@ -644,6 +649,7 @@ async function workspaceInstall(cwd, args, signal) {
|
|
|
644
649
|
lockfile: !args.immutable,
|
|
645
650
|
frozen: args.immutable,
|
|
646
651
|
signal,
|
|
652
|
+
workspaceNames: new Set(byName.keys()),
|
|
647
653
|
});
|
|
648
654
|
}
|
|
649
655
|
// Re-wire workspace symlinks once more now that the external + scoped
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
// `gjsify storybook` — discover every *.story.ts in a project, generate a tiny
|
|
2
|
-
// entry that registers them, then build
|
|
2
|
+
// entry that registers them, then build and launch the generic
|
|
3
3
|
// @gjsify/storybook component browser. No per-project storybook app needed.
|
|
4
4
|
//
|
|
5
|
+
// `--runtime gjs` (default) builds `--app gjs` and runs it with `gjs -m`.
|
|
6
|
+
// `--runtime node` builds `--app node` (gi:// → @gjsify/node-gi reverse bridge)
|
|
7
|
+
// and runs it with `node` — the SAME storybook source rendering on Node.js. The
|
|
8
|
+
// node path wires GI_TYPELIB_PATH/LD_LIBRARY_PATH like `gjsify run` (so any
|
|
9
|
+
// native @gjsify bridge a story uses resolves) and requires `@gjsify/node-gi`
|
|
10
|
+
// installed in the project (kept external by the bundler, a native addon).
|
|
11
|
+
//
|
|
5
12
|
// Dispatch is IN-PROCESS (runCli + runGjsBundle), not a re-spawn of the CLI:
|
|
6
13
|
// the showcase-style `spawn(process.execPath, [cliBin, …])` pattern assumes
|
|
7
14
|
// Node, but under the committed GJS bundle `process.execPath` is `gjs` and the
|
|
@@ -82,6 +89,13 @@ export const storybookCommand = {
|
|
|
82
89
|
.option('out', {
|
|
83
90
|
description: 'Output bundle path (default: node_modules/.cache/gjsify-storybook)',
|
|
84
91
|
type: 'string',
|
|
92
|
+
})
|
|
93
|
+
.option('runtime', {
|
|
94
|
+
description: 'Runtime to build for and launch on: gjs (default) or node (via @gjsify/node-gi). ' +
|
|
95
|
+
'node requires @gjsify/node-gi installed in the project.',
|
|
96
|
+
type: 'string',
|
|
97
|
+
choices: ['gjs', 'node'],
|
|
98
|
+
default: 'gjs',
|
|
85
99
|
})
|
|
86
100
|
.option('watch', {
|
|
87
101
|
description: 'Rebuild and relaunch when a story file changes',
|
|
@@ -97,10 +111,16 @@ export const storybookCommand = {
|
|
|
97
111
|
const applicationId = args.appId ?? config.applicationId ?? deriveAppId(pkg.name);
|
|
98
112
|
const title = args.title ?? config.title;
|
|
99
113
|
const globals = args.globals ?? config.globals ?? 'auto';
|
|
114
|
+
const runtime = (args.runtime ?? config.runtime ?? 'gjs');
|
|
115
|
+
// 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` builds an `--app node` bundle (gi:// → @gjsify/node-gi) run by
|
|
118
|
+
// `node`. Same generated entry + `--globals auto` for both.
|
|
119
|
+
const app = runtime === 'node' ? 'node' : 'gjs';
|
|
100
120
|
const cacheDir = join(cwd, 'node_modules', '.cache', 'gjsify-storybook');
|
|
101
121
|
mkdirSync(cacheDir, { recursive: true });
|
|
102
122
|
const entryPath = join(cacheDir, 'entry.ts');
|
|
103
|
-
const outPath = args.out ? resolve(cwd, args.out) : join(cacheDir,
|
|
123
|
+
const outPath = args.out ? resolve(cwd, args.out) : join(cacheDir, `storybook.${app}.mjs`);
|
|
104
124
|
const writeEntry = () => {
|
|
105
125
|
const files = findStoryFiles(storiesDir).sort();
|
|
106
126
|
writeFileSync(entryPath, generateEntry(files, applicationId, title));
|
|
@@ -111,24 +131,46 @@ export const storybookCommand = {
|
|
|
111
131
|
console.error(`No *.story.ts files found under ${relative(cwd, storiesDir) || storiesDir}`);
|
|
112
132
|
process.exit(1);
|
|
113
133
|
}
|
|
114
|
-
console.log(`Found ${initialCount} story file(s) under ${relative(cwd, storiesDir) || '.'}`);
|
|
134
|
+
console.log(`Found ${initialCount} story file(s) under ${relative(cwd, storiesDir) || '.'} (runtime: ${runtime})`);
|
|
115
135
|
// In-process build — runtime-agnostic (Node uses npm rolldown, GJS uses
|
|
116
136
|
// the @gjsify/rolldown-native facade), no re-spawn of the CLI binary.
|
|
117
137
|
const { runCli } = await import('../cli-app.js');
|
|
118
|
-
const buildBundle = () => runCli(['build', entryPath, '--app',
|
|
138
|
+
const buildBundle = () => runCli(['build', entryPath, '--app', app, '--globals', globals, '--outfile', outPath]);
|
|
139
|
+
const launch = async () => {
|
|
140
|
+
if (runtime === 'node') {
|
|
141
|
+
const { runNodeBundle } = await import('../utils/run-node.js');
|
|
142
|
+
await runNodeBundle(outPath, [], { exitOnSuccess: true });
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
const { runGjsBundle } = await import('../utils/run-gjs.js');
|
|
146
|
+
await runGjsBundle(outPath, [], { exitOnSuccess: true });
|
|
147
|
+
}
|
|
148
|
+
};
|
|
119
149
|
if (!args.watch) {
|
|
120
150
|
await buildBundle();
|
|
121
151
|
if (args.buildOnly)
|
|
122
152
|
return;
|
|
123
|
-
|
|
124
|
-
await runGjsBundle(outPath, [], { exitOnSuccess: true });
|
|
153
|
+
await launch();
|
|
125
154
|
return;
|
|
126
155
|
}
|
|
127
156
|
// Watch mode: rebuild + relaunch on any change under the stories dir.
|
|
128
157
|
// The storybook bundle only needs system Gtk/Adw typelibs at runtime
|
|
129
|
-
// (no @gjsify native bridges), so a plain `gjs -m` child is
|
|
130
|
-
// stays killable for relaunch.
|
|
158
|
+
// (no @gjsify native bridges), so a plain `gjs -m` / `node` child is
|
|
159
|
+
// enough and stays killable for relaunch.
|
|
131
160
|
const { watch } = await import('node:fs');
|
|
161
|
+
const spawnChild = async () => {
|
|
162
|
+
if (runtime === 'node') {
|
|
163
|
+
const { computeNativeEnvForBundle } = await import('../utils/run-gjs.js');
|
|
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 } });
|
|
171
|
+
}
|
|
172
|
+
return spawn('gjs', ['-m', outPath], { stdio: 'inherit' });
|
|
173
|
+
};
|
|
132
174
|
let child = null;
|
|
133
175
|
let rebuilding = false;
|
|
134
176
|
let pending = false;
|
|
@@ -146,7 +188,7 @@ export const storybookCommand = {
|
|
|
146
188
|
}
|
|
147
189
|
await buildBundle();
|
|
148
190
|
if (!args.buildOnly) {
|
|
149
|
-
child =
|
|
191
|
+
child = await spawnChild();
|
|
150
192
|
}
|
|
151
193
|
}
|
|
152
194
|
catch (err) {
|
|
@@ -21,6 +21,7 @@ import { DEFAULT_REGISTRY, fetchPackument, fetchPackumentConditional, fetchTarba
|
|
|
21
21
|
import { extractTarball } from '@gjsify/tar';
|
|
22
22
|
import { getCachedTarball, getForeignCachedTarball, putCachedTarball, } from './install-tarball-cache.js';
|
|
23
23
|
import { getCachedPackument, putCachedPackument } from './install-packument-cache.js';
|
|
24
|
+
import { assertNativeBackendNodeVersion } from './node-version.js';
|
|
24
25
|
// 16-wide download pool. Matched to the shared Soup.Session's lifted
|
|
25
26
|
// `max-conns-per-host` (see @gjsify/fetch `getSharedSession`) — a higher pool
|
|
26
27
|
// here only translates into real concurrency because that cap was raised from
|
|
@@ -30,6 +31,9 @@ const DEFAULT_CONCURRENCY = Number(process.env.GJSIFY_INSTALL_CONCURRENCY ?? '16
|
|
|
30
31
|
const LOCKFILE_NAME = 'gjsify-lock.json';
|
|
31
32
|
const LOCKFILE_VERSION = 2;
|
|
32
33
|
export async function installPackagesNative(opts) {
|
|
34
|
+
// Fail clearly on an unsupported Node major BEFORE touching the ABI-locked
|
|
35
|
+
// native deps — otherwise they SIGSEGV mid-extract with no actionable message.
|
|
36
|
+
assertNativeBackendNodeVersion();
|
|
33
37
|
if (opts.specs.length === 0) {
|
|
34
38
|
throw new Error('installPackagesNative: empty specs list');
|
|
35
39
|
}
|
|
@@ -72,12 +76,29 @@ export async function installPackagesNative(opts) {
|
|
|
72
76
|
// (and silently bumping transitive deps) on a one-package add.
|
|
73
77
|
const preferred = !opts.refreshLockfile && existingLock ? buildPreferredVersions(existingLock) : undefined;
|
|
74
78
|
log('install: resolving %d top-level spec(s) → %s%s', opts.specs.length, opts.prefix, preferred ? ` (preserving ${preferred.size} locked name(s))` : '');
|
|
75
|
-
nodes = await resolveDeps(opts.specs, npmrc, log, opts.overrides, opts.skipDeps, opts.signal, progress, preferred);
|
|
79
|
+
nodes = await resolveDeps(opts.specs, npmrc, log, opts.overrides, opts.skipDeps, opts.signal, progress, preferred, opts.workspaceNames);
|
|
76
80
|
if (opts.lockfile) {
|
|
77
81
|
writeLockfile(lockfilePath, opts.specs, nodes);
|
|
78
82
|
log('install: wrote %s (%d entries)', LOCKFILE_NAME, nodes.length);
|
|
79
83
|
}
|
|
80
84
|
}
|
|
85
|
+
// A package whose name is one of the monorepo's own workspaces is provided
|
|
86
|
+
// by a workspace symlink (wired by `workspaceInstall`), NOT by a registry
|
|
87
|
+
// tarball — even when the lockfile or a transitive edge pins a same-named
|
|
88
|
+
// published version. Drop those nodes from the fetch/extract set so
|
|
89
|
+
// `extractOne` never `rm`s + overwrites the workspace source symlink (its
|
|
90
|
+
// data-loss guard would otherwise abort the whole install). This keeps
|
|
91
|
+
// `--immutable` robust against a committed lockfile that still carries such
|
|
92
|
+
// registry entries (it is built from the lockfile verbatim and never runs
|
|
93
|
+
// `resolveDeps`); a fresh resolve additionally skips them at the source.
|
|
94
|
+
if (opts.workspaceNames && opts.workspaceNames.size > 0) {
|
|
95
|
+
const before = nodes.length;
|
|
96
|
+
nodes = nodes.filter((n) => !opts.workspaceNames.has(n.name));
|
|
97
|
+
const dropped = before - nodes.length;
|
|
98
|
+
if (dropped > 0) {
|
|
99
|
+
log('install: %d workspace-provided package(s) symlinked, not fetched', dropped);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
81
102
|
log('install: downloading %d tarball(s)', nodes.length);
|
|
82
103
|
await downloadAndExtractAll(nodes, opts.prefix, npmrc, log, opts.signal, progress);
|
|
83
104
|
await linkBins(nodes, opts.prefix, log);
|
|
@@ -143,7 +164,14 @@ async function resolveDeps(specs, npmrc, log, overrides, skipDeps, signal, progr
|
|
|
143
164
|
* Undefined ⇒ a fresh resolve that always picks the newest in-range version
|
|
144
165
|
* (first install, or an explicit `--refresh-lockfile`).
|
|
145
166
|
*/
|
|
146
|
-
preferredVersions
|
|
167
|
+
preferredVersions,
|
|
168
|
+
/**
|
|
169
|
+
* Names of the monorepo's own workspace packages. An edge whose name is in
|
|
170
|
+
* this set is satisfied by the workspace symlink, so it is skipped here —
|
|
171
|
+
* the published version (and its subtree) never enters the resolved tree or
|
|
172
|
+
* the lockfile.
|
|
173
|
+
*/
|
|
174
|
+
workspaceNames) {
|
|
147
175
|
progress?.beginPhase('resolve', specs.length);
|
|
148
176
|
const applyOverride = (name, range) => {
|
|
149
177
|
if (!overrides)
|
|
@@ -209,6 +237,11 @@ preferredVersions) {
|
|
|
209
237
|
// cache instead of blocking on a fresh round-trip.
|
|
210
238
|
for (let wi = 0; wi < wave.length; wi++) {
|
|
211
239
|
const edge = wave[wi];
|
|
240
|
+
// A workspace member is satisfied by its workspace symlink, never by
|
|
241
|
+
// a registry tarball — skip the edge so the published version (and
|
|
242
|
+
// its subtree) never enters the resolved tree or the lockfile.
|
|
243
|
+
if (workspaceNames?.has(edge.name))
|
|
244
|
+
continue;
|
|
212
245
|
// Walk the ancestor chain to see whether a satisfying placement is
|
|
213
246
|
// already visible from the requester's `node_modules` lookup. npm's
|
|
214
247
|
// resolver does this — each level of nesting acts as a fallback.
|
|
@@ -70,6 +70,19 @@ export interface InstallOptions {
|
|
|
70
70
|
* to override.
|
|
71
71
|
*/
|
|
72
72
|
progress?: ProgressReporter;
|
|
73
|
+
/**
|
|
74
|
+
* Native backend only: the names of the monorepo's own workspace packages.
|
|
75
|
+
* A package whose name is in this set is materialised as a symlink to its
|
|
76
|
+
* workspace source by `workspaceInstall`, never fetched from the registry —
|
|
77
|
+
* even when the lockfile or a transitive edge references a same-named
|
|
78
|
+
* published version (e.g. a `types-dev/<lib>` workspace that ALSO exists on
|
|
79
|
+
* npm). The native backend drops such names from `resolveDeps` AND from the
|
|
80
|
+
* fetch/extract set, so `extractOne` never tries to `rm` + overwrite a
|
|
81
|
+
* workspace source symlink (its data-loss guard would otherwise abort the
|
|
82
|
+
* whole install). The `npm` backend ignores this — npm resolves workspaces
|
|
83
|
+
* itself.
|
|
84
|
+
*/
|
|
85
|
+
workspaceNames?: Set<string>;
|
|
73
86
|
}
|
|
74
87
|
export interface InstallResult {
|
|
75
88
|
/** Top-level packages that were requested, with the version each
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimum Node.js major the native install backend supports — its prebuilt native
|
|
3
|
+
* modules are ABI-locked to this line (kept in sync with `.nvmrc`).
|
|
4
|
+
*/
|
|
5
|
+
export declare const MIN_NODE_MAJOR = 24;
|
|
6
|
+
/** Parse the major from a `process.versions.node`-style string (`'24.18.0'` → 24). */
|
|
7
|
+
export declare function parseNodeMajor(version: string): number;
|
|
8
|
+
/**
|
|
9
|
+
* The preflight verdict for the native install backend, factored out so it can be
|
|
10
|
+
* unit-tested without spawning a foreign Node. Returns an error message when the
|
|
11
|
+
* runtime is real Node older than {@link MIN_NODE_MAJOR}, else `null` — i.e. on a
|
|
12
|
+
* supported Node, on GJS (which fakes `process.versions.node` and does not use
|
|
13
|
+
* these prebuilds), or on a version string that does not parse.
|
|
14
|
+
*/
|
|
15
|
+
export declare function nativeBackendNodeError(version: string, onRealNode: boolean): string | null;
|
|
16
|
+
/**
|
|
17
|
+
* Guard the native install backend: throw a clear error on an unsupported Node
|
|
18
|
+
* major instead of letting the ABI-mismatched prebuilds SIGSEGV mid-install.
|
|
19
|
+
* No-op under GJS and on supported Node.
|
|
20
|
+
*/
|
|
21
|
+
export declare function assertNativeBackendNodeVersion(): void;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Node-version preflight for the native install backend.
|
|
3
|
+
//
|
|
4
|
+
// The native backend composes native deps whose prebuilt binaries are ABI-locked
|
|
5
|
+
// to the Node line gjsify targets (see `.nvmrc`). On an older Node major they load
|
|
6
|
+
// a mismatched ABI and SIGSEGV mid-install instead of failing cleanly — a
|
|
7
|
+
// confusing crash with no actionable message (observed: Node 22 in a Fedora
|
|
8
|
+
// container segfaults `gjsify install --backend native` during download/extract).
|
|
9
|
+
// This preflight turns that into a clear, actionable error.
|
|
10
|
+
import { isNode } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
11
|
+
/**
|
|
12
|
+
* Minimum Node.js major the native install backend supports — its prebuilt native
|
|
13
|
+
* modules are ABI-locked to this line (kept in sync with `.nvmrc`).
|
|
14
|
+
*/
|
|
15
|
+
export const MIN_NODE_MAJOR = 24;
|
|
16
|
+
/** Parse the major from a `process.versions.node`-style string (`'24.18.0'` → 24). */
|
|
17
|
+
export function parseNodeMajor(version) {
|
|
18
|
+
return Number.parseInt(String(version).split('.', 1)[0] ?? '', 10);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The preflight verdict for the native install backend, factored out so it can be
|
|
22
|
+
* unit-tested without spawning a foreign Node. Returns an error message when the
|
|
23
|
+
* runtime is real Node older than {@link MIN_NODE_MAJOR}, else `null` — i.e. on a
|
|
24
|
+
* supported Node, on GJS (which fakes `process.versions.node` and does not use
|
|
25
|
+
* these prebuilds), or on a version string that does not parse.
|
|
26
|
+
*/
|
|
27
|
+
export function nativeBackendNodeError(version, onRealNode) {
|
|
28
|
+
if (!onRealNode)
|
|
29
|
+
return null;
|
|
30
|
+
const major = parseNodeMajor(version);
|
|
31
|
+
if (!Number.isFinite(major) || major >= MIN_NODE_MAJOR)
|
|
32
|
+
return null;
|
|
33
|
+
return (`gjsify install (native backend) requires Node ≥ ${MIN_NODE_MAJOR}, but this is Node ${version}. ` +
|
|
34
|
+
`Its prebuilt native modules are ABI-locked to Node ${MIN_NODE_MAJOR} and crash on older majors. ` +
|
|
35
|
+
`Use Node ${MIN_NODE_MAJOR} (nvm/asdf locally, actions/setup-node in CI), or run with --backend npm.`);
|
|
36
|
+
}
|
|
37
|
+
/** True when the CLI runs on real Node — NOT GJS, which fakes `process.versions.node`. */
|
|
38
|
+
function onRealNode() {
|
|
39
|
+
// GJS exposes `imports.system.version`; @gjsify/process fakes
|
|
40
|
+
// `process.versions.node = '20.0.0'` for compat, so check GJS FIRST — a plain
|
|
41
|
+
// `process.versions.node` read would be a false Node positive under GJS.
|
|
42
|
+
const sysVersion = globalThis.imports?.system?.version;
|
|
43
|
+
if (sysVersion !== undefined)
|
|
44
|
+
return false;
|
|
45
|
+
return isNode();
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Guard the native install backend: throw a clear error on an unsupported Node
|
|
49
|
+
* major instead of letting the ABI-mismatched prebuilds SIGSEGV mid-install.
|
|
50
|
+
* No-op under GJS and on supported Node.
|
|
51
|
+
*/
|
|
52
|
+
export function assertNativeBackendNodeVersion() {
|
|
53
|
+
const message = nativeBackendNodeError(process.versions.node, onRealNode());
|
|
54
|
+
if (message)
|
|
55
|
+
throw new Error(message);
|
|
56
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Walk up from `startDir` looking for `<node_modules>/@gjsify/node-gi`. Returns
|
|
3
|
+
* the package directory, or null when it is not installed anywhere on the chain.
|
|
4
|
+
* A plain fs walk (works identically on Node and GJS) — Node's own resolver is
|
|
5
|
+
* not available under the GJS-bundled CLI.
|
|
6
|
+
*/
|
|
7
|
+
export declare function resolveNodeGi(startDir: string): string | null;
|
|
8
|
+
/** Pick the node executable: the current one when on Node, else PATH `node`. */
|
|
9
|
+
export declare function nodeBinary(): string;
|
|
10
|
+
export interface RunNodeBundleOptions {
|
|
11
|
+
/** Exit this process with code 0 once the child succeeds (terminal callers). */
|
|
12
|
+
exitOnSuccess?: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Run a `--app node` bundle on Node.js, auto-setting LD_LIBRARY_PATH and
|
|
16
|
+
* GI_TYPELIB_PATH for any installed native gjsify packages (same detection as
|
|
17
|
+
* {@link runGjsBundle}). Verifies `@gjsify/node-gi` is resolvable first and
|
|
18
|
+
* throws a clear, actionable error otherwise.
|
|
19
|
+
*/
|
|
20
|
+
export declare function runNodeBundle(bundlePath: string, extraArgs?: string[], options?: RunNodeBundleOptions): Promise<void>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Shared utility for running a `--app node` bundle on Node.js with the native
|
|
2
|
+
// typelib env wired the same way `runGjsBundle` does.
|
|
3
|
+
//
|
|
4
|
+
// A `gjsify build --app node` bundle of a `gi://` source runs the GObject stack
|
|
5
|
+
// through `@gjsify/node-gi` (the Axis-5 reverse bridge). Two things have to be
|
|
6
|
+
// true at runtime:
|
|
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.
|
|
12
|
+
// 2. `gi://Gtk` / `gi://Adw` etc. must resolve. System typelibs come from the
|
|
13
|
+
// default GI search path, but any `@gjsify/*` Vala bridge a story pulls in
|
|
14
|
+
// ships its typelib in `prebuilds/` — so we reuse the SAME
|
|
15
|
+
// `computeNativeEnvForBundle()` walker `runGjsBundle` uses to prepend
|
|
16
|
+
// `GI_TYPELIB_PATH` / `LD_LIBRARY_PATH`. It is a no-op when no native
|
|
17
|
+
// gjsify packages are present (the storybook's common case).
|
|
18
|
+
//
|
|
19
|
+
// The node binary is `process.execPath` when the CLI itself runs under Node, and
|
|
20
|
+
// the PATH `node` otherwise (under the committed GJS bundle `process.execPath` is
|
|
21
|
+
// `gjs`, which cannot run a `--app node` bundle).
|
|
22
|
+
import { spawn } from 'node:child_process';
|
|
23
|
+
import { existsSync } from 'node:fs';
|
|
24
|
+
import { join, resolve } from 'node:path';
|
|
25
|
+
import { isNode } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
26
|
+
import { computeNativeEnvForBundle } from './run-gjs.js';
|
|
27
|
+
/**
|
|
28
|
+
* Walk up from `startDir` looking for `<node_modules>/@gjsify/node-gi`. Returns
|
|
29
|
+
* the package directory, or null when it is not installed anywhere on the chain.
|
|
30
|
+
* A plain fs walk (works identically on Node and GJS) — Node's own resolver is
|
|
31
|
+
* not available under the GJS-bundled CLI.
|
|
32
|
+
*/
|
|
33
|
+
export function resolveNodeGi(startDir) {
|
|
34
|
+
let dir = resolve(startDir);
|
|
35
|
+
while (true) {
|
|
36
|
+
const candidate = join(dir, 'node_modules', '@gjsify', 'node-gi', 'package.json');
|
|
37
|
+
if (existsSync(candidate))
|
|
38
|
+
return join(dir, 'node_modules', '@gjsify', 'node-gi');
|
|
39
|
+
const parent = resolve(dir, '..');
|
|
40
|
+
if (parent === dir)
|
|
41
|
+
return null;
|
|
42
|
+
dir = parent;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Pick the node executable: the current one when on Node, else PATH `node`. */
|
|
46
|
+
export function nodeBinary() {
|
|
47
|
+
return isNode() && process.execPath ? process.execPath : 'node';
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Run a `--app node` bundle on Node.js, auto-setting LD_LIBRARY_PATH and
|
|
51
|
+
* GI_TYPELIB_PATH for any installed native gjsify packages (same detection as
|
|
52
|
+
* {@link runGjsBundle}). Verifies `@gjsify/node-gi` is resolvable first and
|
|
53
|
+
* throws a clear, actionable error otherwise.
|
|
54
|
+
*/
|
|
55
|
+
export async function runNodeBundle(bundlePath, extraArgs = [], options = {}) {
|
|
56
|
+
const cwd = process.cwd();
|
|
57
|
+
if (!resolveNodeGi(cwd)) {
|
|
58
|
+
throw new Error('Cannot run the storybook on Node: `@gjsify/node-gi` is not installed.\n' +
|
|
59
|
+
'Add @gjsify/node-gi as a dependency to run the storybook on Node, e.g.:\n' +
|
|
60
|
+
' "devDependencies": { "@gjsify/node-gi": "^0.13.0" }');
|
|
61
|
+
}
|
|
62
|
+
const { env: nativeEnv, envPrefix } = computeNativeEnvForBundle(bundlePath, cwd);
|
|
63
|
+
const env = { ...process.env, ...nativeEnv };
|
|
64
|
+
const nodeBin = nodeBinary();
|
|
65
|
+
const nodeArgs = [bundlePath, ...extraArgs];
|
|
66
|
+
// Echo the exact command (stderr, so a child speaking a protocol on stdout
|
|
67
|
+
// stays uncontaminated) so users can reproduce the run without the wrapper.
|
|
68
|
+
const cmd = [nodeBin, ...nodeArgs.map((a) => (a.includes(' ') ? `"${a}"` : a))].join(' ');
|
|
69
|
+
console.error(`$ ${envPrefix ? `${envPrefix} ` : ''}${cmd}`);
|
|
70
|
+
const child = spawn(nodeBin, nodeArgs, { env, stdio: 'inherit' });
|
|
71
|
+
await new Promise((resolvePromise, reject) => {
|
|
72
|
+
child.on('close', (code) => {
|
|
73
|
+
if (code !== 0)
|
|
74
|
+
reject(new Error(`node exited with code ${code}`));
|
|
75
|
+
else
|
|
76
|
+
resolvePromise();
|
|
77
|
+
});
|
|
78
|
+
child.on('error', reject);
|
|
79
|
+
}).catch((err) => {
|
|
80
|
+
console.error(err.message);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
});
|
|
83
|
+
if (options.exitOnSuccess)
|
|
84
|
+
process.exit(0);
|
|
85
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "CLI for Gjsify",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -108,6 +108,7 @@
|
|
|
108
108
|
"start": "node lib/index.js",
|
|
109
109
|
"build": "tsc && mkdir -p lib/templates/flatpak && cp -L src/templates/install.mjs.tmpl lib/templates/install.mjs.tmpl && cp src/templates/flatpak/*.tmpl lib/templates/flatpak/ && cp src/templates/oxlintrc.json.tmpl lib/templates/oxlintrc.json.tmpl && cp src/templates/oxfmtrc.tmpl lib/templates/oxfmtrc.tmpl && gjsify run chmod",
|
|
110
110
|
"build:gjs-bundle": "node lib/index.js build src/index.ts --app gjs --outfile dist/cli.gjs.mjs --shebang",
|
|
111
|
+
"build:affected-bundle": "node lib/index.js build src/affected-entry.ts --app gjs --outfile dist/affected.gjs.mjs",
|
|
111
112
|
"chmod": "chmod +x ./lib/index.js",
|
|
112
113
|
"build:test:node": "node lib/index.js build src/test.mts --app node --outfile dist/test.node.mjs",
|
|
113
114
|
"test:node": "node dist/test.node.mjs",
|
|
@@ -120,19 +121,19 @@
|
|
|
120
121
|
"cli"
|
|
121
122
|
],
|
|
122
123
|
"dependencies": {
|
|
123
|
-
"@gjsify/buffer": "^0.
|
|
124
|
-
"@gjsify/create-app": "^0.
|
|
125
|
-
"@gjsify/node-globals": "^0.
|
|
126
|
-
"@gjsify/node-polyfills": "^0.
|
|
127
|
-
"@gjsify/npm-registry": "^0.
|
|
128
|
-
"@gjsify/resolve-npm": "^0.
|
|
129
|
-
"@gjsify/rolldown-plugin-gjsify": "^0.
|
|
130
|
-
"@gjsify/rolldown-plugin-pnp": "^0.
|
|
131
|
-
"@gjsify/semver": "^0.
|
|
132
|
-
"@gjsify/tar": "^0.
|
|
133
|
-
"@gjsify/tsc": "^0.
|
|
134
|
-
"@gjsify/web-polyfills": "^0.
|
|
135
|
-
"@gjsify/workspace": "^0.
|
|
124
|
+
"@gjsify/buffer": "^0.14.0",
|
|
125
|
+
"@gjsify/create-app": "^0.14.0",
|
|
126
|
+
"@gjsify/node-globals": "^0.14.0",
|
|
127
|
+
"@gjsify/node-polyfills": "^0.14.0",
|
|
128
|
+
"@gjsify/npm-registry": "^0.14.0",
|
|
129
|
+
"@gjsify/resolve-npm": "^0.14.0",
|
|
130
|
+
"@gjsify/rolldown-plugin-gjsify": "^0.14.0",
|
|
131
|
+
"@gjsify/rolldown-plugin-pnp": "^0.14.0",
|
|
132
|
+
"@gjsify/semver": "^0.14.0",
|
|
133
|
+
"@gjsify/tar": "^0.14.0",
|
|
134
|
+
"@gjsify/tsc": "^0.14.0",
|
|
135
|
+
"@gjsify/web-polyfills": "^0.14.0",
|
|
136
|
+
"@gjsify/workspace": "^0.14.0",
|
|
136
137
|
"cosmiconfig": "^9.0.2",
|
|
137
138
|
"get-tsconfig": "^4.14.0",
|
|
138
139
|
"pkg-types": "^2.3.1",
|
|
@@ -140,13 +141,13 @@
|
|
|
140
141
|
"yargs": "^18.0.0"
|
|
141
142
|
},
|
|
142
143
|
"devDependencies": {
|
|
143
|
-
"@gjsify/unit": "^0.
|
|
144
|
+
"@gjsify/unit": "^0.14.0",
|
|
144
145
|
"@types/yargs": "^17.0.35",
|
|
145
146
|
"typescript": "^6.0.3"
|
|
146
147
|
},
|
|
147
148
|
"peerDependencies": {
|
|
148
|
-
"@gjsify/oxfmt-native": "^0.
|
|
149
|
-
"@gjsify/rolldown-native": "^0.
|
|
149
|
+
"@gjsify/oxfmt-native": "^0.14.0",
|
|
150
|
+
"@gjsify/rolldown-native": "^0.14.0"
|
|
150
151
|
},
|
|
151
152
|
"peerDependenciesMeta": {
|
|
152
153
|
"@gjsify/oxfmt-native": {
|