@gjsify/cli 0.29.0 → 0.31.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 +197 -189
- package/lib/actions/build.d.ts +14 -0
- package/lib/actions/build.js +78 -3
- package/lib/actions/index.d.ts +2 -0
- package/lib/actions/index.js +2 -0
- package/lib/affected-entry.d.ts +1 -0
- package/lib/affected-entry.js +49 -0
- package/lib/bundler-pick.js +84 -19
- package/lib/cli-app.js +20 -0
- package/lib/cli-fail.d.ts +22 -0
- package/lib/cli-fail.js +67 -0
- package/lib/commands/affected.d.ts +2 -2
- package/lib/commands/affected.js +16 -0
- package/lib/commands/install.d.ts +55 -0
- package/lib/commands/install.js +38 -21
- package/lib/commands/run.js +25 -6
- package/lib/commands/test.js +4 -4
- package/lib/commands/tsc.js +59 -11
- package/lib/commands/workspace.js +13 -0
- package/lib/config.js +6 -2
- package/lib/index.js +18 -1
- package/lib/templates/install.mjs.tmpl +27 -2
- package/lib/types/command.d.ts +18 -0
- package/lib/utils/bin-shim.d.ts +39 -0
- package/lib/utils/bin-shim.js +89 -13
- package/lib/utils/check-system-deps.d.ts +88 -1
- package/lib/utils/check-system-deps.js +177 -6
- package/lib/utils/detect-native-packages.js +12 -1
- package/lib/utils/discover-showcases.d.ts +0 -2
- package/lib/utils/discover-showcases.js +0 -1
- package/lib/utils/dlx-cache.d.ts +22 -2
- package/lib/utils/dlx-cache.js +62 -6
- package/lib/utils/force-exit.d.ts +10 -0
- package/lib/utils/force-exit.js +43 -0
- package/lib/utils/gi-search-path.d.ts +36 -0
- package/lib/utils/gi-search-path.js +146 -0
- package/lib/utils/gjs-source-escape.d.ts +10 -0
- package/lib/utils/gjs-source-escape.js +49 -0
- package/lib/utils/gjsify-shim.d.ts +29 -2
- package/lib/utils/gjsify-shim.js +131 -22
- package/lib/utils/install-backend-native.d.ts +8 -0
- package/lib/utils/install-backend-native.js +8 -2
- package/lib/utils/install-global.d.ts +30 -2
- package/lib/utils/install-global.js +51 -5
- package/lib/utils/oxc-resolve.d.ts +1 -1
- package/lib/utils/oxc-resolve.js +10 -6
- package/lib/utils/run-lifecycle-script.js +41 -25
- package/lib/utils/spawn.d.ts +17 -1
- package/lib/utils/spawn.js +23 -6
- package/lib/utils/stdin.d.ts +3 -13
- package/lib/utils/stdin.js +21 -100
- package/lib/utils/system-gi.js +10 -1
- package/package.json +22 -17
- package/showcases.json +8 -16
package/lib/actions/build.d.ts
CHANGED
|
@@ -48,6 +48,20 @@ export declare class BuildAction {
|
|
|
48
48
|
* there is no in-bundle plugin, so this hook is the sole injection point.
|
|
49
49
|
*/
|
|
50
50
|
private applyShebang;
|
|
51
|
+
/**
|
|
52
|
+
* Post-bundle rewrite for `--app gjs`: replace raw U+0000 bytes with `\x00`.
|
|
53
|
+
*
|
|
54
|
+
* GJS hands module source to SpiderMonkey as a NUL-terminated C string, so one raw NUL
|
|
55
|
+
* truncates the file and the loader reports whatever construct was open — typically
|
|
56
|
+
* "`` literal not terminated before end of script", which names neither the NUL nor its
|
|
57
|
+
* location. The minifier produces them by inlining a `'\u0000'` constant into a template
|
|
58
|
+
* literal, so a bundle can build and run unminified and fail under the default `--minify`.
|
|
59
|
+
*
|
|
60
|
+
* Rewrites the files on disk rather than the in-memory chunks: rolldown has already
|
|
61
|
+
* written them by this point, and the same treatment has to reach every chunk of an
|
|
62
|
+
* `--outdir` build, not just a single `--outfile`.
|
|
63
|
+
*/
|
|
64
|
+
private escapeRawNul;
|
|
51
65
|
/**
|
|
52
66
|
* Post-bundle gate for `--app gjs`: throw when the emitted bundle still
|
|
53
67
|
* STATICALLY imports Node builtins, which stock GJS cannot resolve (see
|
package/lib/actions/build.js
CHANGED
|
@@ -6,11 +6,12 @@ import { resolveGlobalsList, writeRegisterInjectFile, detectAutoGlobals, detectN
|
|
|
6
6
|
import { pnpPlugin } from '@gjsify/rolldown-plugin-pnp';
|
|
7
7
|
import { dirname, extname, join, resolve } from 'node:path';
|
|
8
8
|
import { pathToFileURL } from 'node:url';
|
|
9
|
-
import { chmod, mkdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
9
|
+
import { chmod, mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
10
10
|
import { normalizeBundlerOptions, mergeBundlerOptions } from '../utils/normalize-bundler-options.js';
|
|
11
11
|
import { inputSourceDirs, isOutdirInsideSource, libraryOutputLeakError } from '../utils/library-output.js';
|
|
12
12
|
import { detectHtmlEntry, parseHtmlEntry, emitBrowserHtml, htmlOutPathFor } from '../utils/html-entry.js';
|
|
13
13
|
import { assertGjsBundleLoadable } from '../utils/gjs-bundle-guard.js';
|
|
14
|
+
import { escapeRawNulForGjs } from '../utils/gjs-source-escape.js';
|
|
14
15
|
import { assertNodeBundleGlobalsShimmed } from '../utils/node-bundle-guard.js';
|
|
15
16
|
const DEFAULT_GJS_SHEBANG = '#!/usr/bin/env -S gjs -m';
|
|
16
17
|
/**
|
|
@@ -277,6 +278,68 @@ export class BuildAction {
|
|
|
277
278
|
if (verbose)
|
|
278
279
|
console.debug(`[gjsify] --shebang: wrote ${line} + chmod 0o755 to ${outfile}`);
|
|
279
280
|
}
|
|
281
|
+
/**
|
|
282
|
+
* Post-bundle rewrite for `--app gjs`: replace raw U+0000 bytes with `\x00`.
|
|
283
|
+
*
|
|
284
|
+
* GJS hands module source to SpiderMonkey as a NUL-terminated C string, so one raw NUL
|
|
285
|
+
* truncates the file and the loader reports whatever construct was open — typically
|
|
286
|
+
* "`` literal not terminated before end of script", which names neither the NUL nor its
|
|
287
|
+
* location. The minifier produces them by inlining a `'\u0000'` constant into a template
|
|
288
|
+
* literal, so a bundle can build and run unminified and fail under the default `--minify`.
|
|
289
|
+
*
|
|
290
|
+
* Rewrites the files on disk rather than the in-memory chunks: rolldown has already
|
|
291
|
+
* written them by this point, and the same treatment has to reach every chunk of an
|
|
292
|
+
* `--outdir` build, not just a single `--outfile`.
|
|
293
|
+
*/
|
|
294
|
+
async escapeRawNul(outfile, outdir, verbose) {
|
|
295
|
+
// Deliberately NOT driven by the chunk graph: `--watch`'s BUNDLE_END carries the bundle
|
|
296
|
+
// rather than the generated output, and re-running generate() just to list chunks would
|
|
297
|
+
// double every rebuild. Reading the directory needs neither, so the same hook serves the
|
|
298
|
+
// one-shot build and the watch loop — and a watch rebuild that quietly reintroduced the
|
|
299
|
+
// raw NUL would fail exactly like the bug this exists to prevent, only intermittently.
|
|
300
|
+
const targets = [];
|
|
301
|
+
if (outfile) {
|
|
302
|
+
targets.push(outfile);
|
|
303
|
+
}
|
|
304
|
+
else if (outdir) {
|
|
305
|
+
try {
|
|
306
|
+
for (const name of await readdir(outdir)) {
|
|
307
|
+
if (name.endsWith('.mjs') || name.endsWith('.js'))
|
|
308
|
+
targets.push(resolve(outdir, name));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
// ENOENT alone is benign — a watch pass that failed before writing anything has
|
|
313
|
+
// no outdir yet, and there is nothing to escape. Anything ELSE (a permissions
|
|
314
|
+
// error, an I/O error, a wrong outdir) would silently skip the escape and let
|
|
315
|
+
// the build report success while emitting a bundle GJS cannot load, which is
|
|
316
|
+
// precisely the failure this hook exists to prevent.
|
|
317
|
+
if (err?.code === 'ENOENT')
|
|
318
|
+
return;
|
|
319
|
+
throw err;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
for (const target of targets) {
|
|
323
|
+
let original;
|
|
324
|
+
try {
|
|
325
|
+
original = await readFile(target, 'utf-8');
|
|
326
|
+
}
|
|
327
|
+
catch (err) {
|
|
328
|
+
// Same split: a listed-but-unwritten chunk is not this hook's problem; a real
|
|
329
|
+
// read failure must not pass for "nothing to do".
|
|
330
|
+
if (err?.code === 'ENOENT')
|
|
331
|
+
continue;
|
|
332
|
+
throw err;
|
|
333
|
+
}
|
|
334
|
+
const { code, replaced } = escapeRawNulForGjs(original);
|
|
335
|
+
if (replaced === 0)
|
|
336
|
+
continue; // the overwhelmingly common case — touch nothing
|
|
337
|
+
await writeFile(target, code);
|
|
338
|
+
if (verbose) {
|
|
339
|
+
console.debug(`[gjsify] escaped ${replaced} raw NUL byte(s) in ${target} so GJS can load it`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
280
343
|
/**
|
|
281
344
|
* Post-bundle gate for `--app gjs`: throw when the emitted bundle still
|
|
282
345
|
* STATICALLY imports Node builtins, which stock GJS cannot resolve (see
|
|
@@ -624,10 +687,16 @@ export class BuildAction {
|
|
|
624
687
|
plugins: [...pnpPlugins, ...userPlugins, ...cfg.plugins],
|
|
625
688
|
};
|
|
626
689
|
if (opts.watch) {
|
|
627
|
-
await this.runWatchLoop(finalOpts, app, outfile, verbose);
|
|
690
|
+
await this.runWatchLoop(finalOpts, app, outfile, outdir, verbose);
|
|
628
691
|
return [];
|
|
629
692
|
}
|
|
630
693
|
const writeResult = await runBundle(finalOpts);
|
|
694
|
+
// GJS truncates module source at a raw U+0000 (it is handed to SpiderMonkey as a
|
|
695
|
+
// NUL-terminated C string), so escape any the minifier emitted before anything else
|
|
696
|
+
// touches the file. See utils/gjs-source-escape.ts.
|
|
697
|
+
if (app === 'gjs') {
|
|
698
|
+
await this.escapeRawNul(outfile, outdir, verbose);
|
|
699
|
+
}
|
|
631
700
|
if ((app === 'gjs' || app === 'node') && this.configData.shebang) {
|
|
632
701
|
await this.applyShebang(app, outfile, verbose);
|
|
633
702
|
}
|
|
@@ -657,7 +726,7 @@ export class BuildAction {
|
|
|
657
726
|
* without exiting, clean up on SIGINT/SIGTERM. Resolves only when the
|
|
658
727
|
* watcher closes — keeps the CLI process alive across rebuilds.
|
|
659
728
|
*/
|
|
660
|
-
async runWatchLoop(finalOpts, app, outfile, verbose) {
|
|
729
|
+
async runWatchLoop(finalOpts, app, outfile, outdir, verbose) {
|
|
661
730
|
const watcher = await runWatch(finalOpts);
|
|
662
731
|
const closed = new Promise((resolve) => {
|
|
663
732
|
watcher.on('close', () => resolve());
|
|
@@ -689,6 +758,12 @@ export class BuildAction {
|
|
|
689
758
|
case 'BUNDLE_END':
|
|
690
759
|
console.log(`[gjsify build --watch] built in ${event.duration}ms`);
|
|
691
760
|
try {
|
|
761
|
+
// Before the shebang, which re-reads the file: a rebuild that dropped a
|
|
762
|
+
// raw NUL back in would produce a bundle GJS cannot load, and on the
|
|
763
|
+
// watch loop that reads as "it broke when I saved", not as a build bug.
|
|
764
|
+
if (app === 'gjs') {
|
|
765
|
+
await this.escapeRawNul(outfile, outdir, verbose);
|
|
766
|
+
}
|
|
692
767
|
if ((app === 'gjs' || app === 'node') && this.configData.shebang) {
|
|
693
768
|
await this.applyShebang(app, outfile, verbose);
|
|
694
769
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Standalone entry for the affected classifier — bundled to
|
|
3
|
+
// `dist/affected.gjs.mjs` and run by CI's `changes` job (`gjs -m …`) on a plain
|
|
4
|
+
// ubuntu-latest host that only `apt install`s gjs.
|
|
5
|
+
//
|
|
6
|
+
// CRITICAL — ZERO gi:// TYPELIB DEPENDENCIES (beyond GLib/Gio, which ship with
|
|
7
|
+
// gjs): this entry imports ONLY the `affected` command (→ `@gjsify/workspace`
|
|
8
|
+
// + `@gjsify/fs`/`@gjsify/child_process` for the git diff). It MUST NOT import
|
|
9
|
+
// the full CLI, whose network commands (login/publish/trust/whoami/self-update/
|
|
10
|
+
// upgrade) pull `@gjsify/npm-registry` → `@gjsify/fetch` → `gi://Soup?version=3.0`.
|
|
11
|
+
// The classifier host has no Soup typelib, so a bundle that eagerly requires
|
|
12
|
+
// Soup throws at module load → the classifier crashes → main.yml fails open to
|
|
13
|
+
// a full run on every PR (the regression this entry fixes). Keep the import
|
|
14
|
+
// graph below Soup-free; `tests/e2e/affected-classifier-bundle` guards it.
|
|
15
|
+
import { affectedCommand } from './commands/affected.js';
|
|
16
|
+
function parseArgs(argv) {
|
|
17
|
+
const args = { head: 'HEAD', format: 'text', 'changed-from-stdin': false };
|
|
18
|
+
for (let i = 0; i < argv.length; i++) {
|
|
19
|
+
const a = argv[i];
|
|
20
|
+
if (a === '--changed-from-stdin')
|
|
21
|
+
args['changed-from-stdin'] = true;
|
|
22
|
+
else if (a === '--base')
|
|
23
|
+
args.base = argv[++i];
|
|
24
|
+
else if (a === '--head')
|
|
25
|
+
args.head = argv[++i] ?? 'HEAD';
|
|
26
|
+
else if (a === '--cwd')
|
|
27
|
+
args.cwd = argv[++i];
|
|
28
|
+
else if (a.startsWith('--base='))
|
|
29
|
+
args.base = a.slice('--base='.length);
|
|
30
|
+
else if (a.startsWith('--head='))
|
|
31
|
+
args.head = a.slice('--head='.length);
|
|
32
|
+
else if (a.startsWith('--cwd='))
|
|
33
|
+
args.cwd = a.slice('--cwd='.length);
|
|
34
|
+
else if (a.startsWith('--format='))
|
|
35
|
+
args.format = a.slice('--format='.length);
|
|
36
|
+
else if (a === '--format')
|
|
37
|
+
args.format = argv[++i] ?? 'text';
|
|
38
|
+
}
|
|
39
|
+
return args;
|
|
40
|
+
}
|
|
41
|
+
const argv = process.argv.slice(2);
|
|
42
|
+
// Tolerate an optional leading `affected` token so the bundle can be invoked
|
|
43
|
+
// either as `gjs -m affected.gjs.mjs --base …` or `… affected --base …`.
|
|
44
|
+
const rest = argv[0] === 'affected' ? argv.slice(1) : argv;
|
|
45
|
+
// The cast is only about the ARGUMENT shape: `parseArgs` returns the flags this
|
|
46
|
+
// entry accepts, while the handler wants yargs' `ArgumentsCamelCase` wrapper
|
|
47
|
+
// around them. It is no longer also papering over a possibly-undefined handler —
|
|
48
|
+
// `affectedCommand` is a `LeafCommand`, so its handler is required by type.
|
|
49
|
+
await affectedCommand.handler(parseArgs(rest));
|
package/lib/bundler-pick.js
CHANGED
|
@@ -32,6 +32,12 @@ import * as path from 'node:path';
|
|
|
32
32
|
import { createRequire } from 'node:module';
|
|
33
33
|
import { pathToFileURL } from 'node:url';
|
|
34
34
|
import { resolveNpmPackage } from './utils/resolve-npm-package.js';
|
|
35
|
+
// Static, not dynamic: `diagnoseNativeEngine()` is synchronous and is called
|
|
36
|
+
// inside a `throw` expression. Safe — check-system-deps imports only `node:`
|
|
37
|
+
// builtins, so there is no cycle, and every one of them is already in the GJS
|
|
38
|
+
// bundle via `commands/install.ts`.
|
|
39
|
+
import { buildInstallCommand, detectPackageManager, missingSystemDepsFor } from './utils/check-system-deps.js';
|
|
40
|
+
import { activateNativePrebuilds } from './utils/gi-search-path.js';
|
|
35
41
|
import { isGjs } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
36
42
|
// npm `rolldown` is a Rust crate with platform-specific prebuilds; loading
|
|
37
43
|
// it eagerly at module init pulls musl-detection code that does
|
|
@@ -63,17 +69,24 @@ async function loadNpmRolldown() {
|
|
|
63
69
|
* Say WHY the native engine could not be loaded, read off disk rather than
|
|
64
70
|
* guessed.
|
|
65
71
|
*
|
|
66
|
-
* The
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* hit exactly that and it blocked releases.
|
|
72
|
+
* The causes need different fixes, and this message used to assert the rarest
|
|
73
|
+
* of them ("the CLI bundle was invoked DIRECTLY … bypasses the `gjsify`
|
|
74
|
+
* launcher") as the "most likely cause". On the by-far most common one — a
|
|
75
|
+
* freshly cloned + freshly installed tree, where the engine's JS facade has
|
|
76
|
+
* simply never been built — that reading is not merely unhelpful, it points at
|
|
77
|
+
* a launcher that was in fact used correctly, so the reader goes looking for an
|
|
78
|
+
* env-var problem that does not exist. `scripts/verify-committed-bundles.mjs`
|
|
79
|
+
* hit exactly that and it blocked releases. ADR 0021 then removed the bypassed
|
|
80
|
+
* launcher as a CAUSE outright, so it is no longer among the candidates at all.
|
|
74
81
|
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
82
|
+
* NOTHING here may throw or recurse into the failure being explained. Branches 1
|
|
83
|
+
* and 2 are `existsSync`-only (no import, no resolution). Branch 3 additionally
|
|
84
|
+
* asks `missingSystemDepsFor()` and reads the memoized `activateNativePrebuilds()`
|
|
85
|
+
* result — both safe by construction rather than by a catch: the former routes
|
|
86
|
+
* every probe through `tryExecFile`, which returns null on any failure, and the
|
|
87
|
+
* latter is an array the failing load attempt already computed. Keep that
|
|
88
|
+
* property when editing: a probe that dies here replaces the diagnosis with its
|
|
89
|
+
* own stack.
|
|
77
90
|
*/
|
|
78
91
|
function diagnoseNativeEngine() {
|
|
79
92
|
let pkgDir = null;
|
|
@@ -109,15 +122,60 @@ function diagnoseNativeEngine() {
|
|
|
109
122
|
'that is closed, seed `lib/` from the published package — which ships it — or from a host that has ' +
|
|
110
123
|
'Node. Tracked in `status/open-todos.md` ("A Node-less host cannot bootstrap a fresh CLONE").');
|
|
111
124
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
125
|
+
// Branch 3: installed AND built, so the typelib/prebuild LOOKUP failed.
|
|
126
|
+
//
|
|
127
|
+
// This branch used to assert "Most likely the CLI bundle was invoked
|
|
128
|
+
// DIRECTLY … bypasses the launcher" — repeating, one branch later, the exact
|
|
129
|
+
// mistake this function's docblock records fixing in branch 2: naming the
|
|
130
|
+
// rarest cause as the most likely one. After #994 measured `ldd` over every
|
|
131
|
+
// committed linux-x64 prebuild on a consumer-baseline host, a missing
|
|
132
|
+
// `libjson-glib-1.0.so.0` is at least as common, and it is what ts-for-gir#437
|
|
133
|
+
// actually hit — with this message on screen.
|
|
134
|
+
//
|
|
135
|
+
// So both candidates are now MEASURED, cheaply and without any call that can
|
|
136
|
+
// throw while explaining a failure, and the measured one is printed first.
|
|
137
|
+
//
|
|
138
|
+
// ADR 0021 RETIRED THE THIRD CANDIDATE. "The launcher was bypassed, export
|
|
139
|
+
// these two variables yourself" is no longer a cause of failure at all:
|
|
140
|
+
// `tryLoadNative()` calls `activateNativePrebuilds()` before the engine is
|
|
141
|
+
// loaded, which puts every detected prebuild dir on girepository's typelib
|
|
142
|
+
// AND library search paths in-process. Do not reintroduce it — besides now
|
|
143
|
+
// being false, the path it printed (`<facade>/prebuilds/<target>`) had been
|
|
144
|
+
// wrong since ADR 0017 moved every artifact into a per-target SIBLING
|
|
145
|
+
// package, so it told readers to export a directory that does not exist.
|
|
146
|
+
const parts = ['The JS facade is built, so this is the typelib/prebuild lookup failing.'];
|
|
147
|
+
// (1) A declared system library that is not on this host. `missingSystemDepsFor`
|
|
148
|
+
// reads the same tables `gjsify system-check` does, so the answer cannot drift
|
|
149
|
+
// from the declaration.
|
|
150
|
+
const missing = missingSystemDepsFor('@gjsify/rolldown-native');
|
|
151
|
+
if (missing.length > 0) {
|
|
152
|
+
const names = missing.map((d) => d.name).join(', ');
|
|
153
|
+
parts.push(`\nMEASURED CAUSE — a system library the engine's prebuild loads is MISSING: ${names}.\n` +
|
|
154
|
+
'The prebuild is present but its typelib cannot open its backing library, which GJS reports as ' +
|
|
155
|
+
'`Unsupported type void, deriving from fundamental void` — a message that names nothing. ' +
|
|
156
|
+
'This is a system package, not an npm one.');
|
|
157
|
+
const cmd = buildInstallCommand(detectPackageManager(), missing);
|
|
158
|
+
if (cmd)
|
|
159
|
+
parts.push(`Install it:\n ${cmd}`);
|
|
160
|
+
else
|
|
161
|
+
parts.push(`Install ${names} with your system package manager (this host's manager was not recognised, so no command is suggested rather than a wrong one).`);
|
|
162
|
+
}
|
|
163
|
+
// (2) The remaining cause, last because it is the least actionable: no
|
|
164
|
+
// prebuild for this architecture. Read off the set `activateNativePrebuilds()`
|
|
165
|
+
// ACTUALLY put on the search paths — memoized, so this is a cached-array read
|
|
166
|
+
// that cannot throw, and unlike the old hand-composed path it names the
|
|
167
|
+
// per-target sibling package that really holds the artifact.
|
|
168
|
+
if (missing.length === 0) {
|
|
169
|
+
const engine = activateNativePrebuilds().find((p) => p.name.startsWith('@gjsify/rolldown-native'));
|
|
170
|
+
parts.push(engine
|
|
171
|
+
? `\nEvery declared system library resolves and ${engine.name}'s prebuild (${engine.prebuildsDir}) IS on ` +
|
|
172
|
+
"girepository's search paths, so it is present but did not load."
|
|
173
|
+
: `\nEvery declared system library resolves, so most likely no prebuild exists for ` +
|
|
174
|
+
`${process.platform}/${process.arch} — no @gjsify/rolldown-native prebuild directory was found ` +
|
|
175
|
+
`walking up from ${process.cwd()}.`);
|
|
176
|
+
}
|
|
177
|
+
parts.push('\nRunning the build under Node also works — there the npm `rolldown` crate is used instead.');
|
|
178
|
+
return parts.join('\n');
|
|
121
179
|
}
|
|
122
180
|
/**
|
|
123
181
|
* Convert a bare npm specifier into something dynamic `import(...)` can
|
|
@@ -296,6 +354,13 @@ async function tryLoadNative() {
|
|
|
296
354
|
if (!isGjs())
|
|
297
355
|
return null;
|
|
298
356
|
try {
|
|
357
|
+
// Make the engine's prebuild resolvable to THIS process before the
|
|
358
|
+
// import that needs it. Without this the engine loads only when the
|
|
359
|
+
// `gjsify` launcher exported GI_TYPELIB_PATH + the library-path var
|
|
360
|
+
// before exec, so a direct `gjs -m …/dist/cli.gjs.mjs build …` — what
|
|
361
|
+
// several e2e suites do — died with "no usable bundler engine under
|
|
362
|
+
// GJS" on a tree where the engine was installed and loadable. ADR 0021.
|
|
363
|
+
activateNativePrebuilds();
|
|
299
364
|
// Under GJS the ESM loader has no node_modules resolver — a bare
|
|
300
365
|
// `import('@gjsify/rolldown-native')` would throw `Module not
|
|
301
366
|
// found`. Resolve via createRequire (PnP+node_modules-aware) to
|
package/lib/cli-app.js
CHANGED
|
@@ -19,6 +19,7 @@ import { readFileSync } from 'node:fs';
|
|
|
19
19
|
import { dirname, join } from 'node:path';
|
|
20
20
|
import { fileURLToPath } from 'node:url';
|
|
21
21
|
import yargs from 'yargs';
|
|
22
|
+
import { classifyCliFailure } from './cli-fail.js';
|
|
22
23
|
import { buildCommand as build, clearCommand as clear, copyCommand as copy, 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
24
|
import { APP_NAME } from './constants.js';
|
|
24
25
|
import { isBun, isDeno, isNode, gjsSystemVersion } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
@@ -84,6 +85,25 @@ export async function runCli(argv) {
|
|
|
84
85
|
.scriptName(APP_NAME)
|
|
85
86
|
.version(readBundleVersion())
|
|
86
87
|
.strict()
|
|
88
|
+
// Sits beside `.strict()` because that is what produces most of what it
|
|
89
|
+
// classifies. See `cli-fail.ts` for the four yargs measurements this
|
|
90
|
+
// depends on; the short version: `msg` is the discriminator (a handler
|
|
91
|
+
// rejection is the ONE call site that passes null), registering a handler
|
|
92
|
+
// removes yargs' own showHelp+exit, and the incoming error must never be
|
|
93
|
+
// re-thrown as-is because yargs routes `YError` back into `fail()`.
|
|
94
|
+
.fail((msg, err) => {
|
|
95
|
+
const failure = classifyCliFailure(msg, err);
|
|
96
|
+
if (failure.kind === 'usage') {
|
|
97
|
+
cli.showHelp('error');
|
|
98
|
+
// Plain Error, not `err`: re-throwing a YError re-enters fail()
|
|
99
|
+
// and prints the help twice. `index.ts` prints this message.
|
|
100
|
+
throw new Error(failure.message, { cause: err });
|
|
101
|
+
}
|
|
102
|
+
// A handler threw. Return WITHOUT printing or exiting — that is the
|
|
103
|
+
// entire fix. The rejection propagates out of `parseAsync()` and
|
|
104
|
+
// `index.ts`'s catch is the one printer (message, optional
|
|
105
|
+
// GJSIFY_DEBUG stack, `process.exitCode = 1`, `gjsExit(1)`).
|
|
106
|
+
})
|
|
87
107
|
// Use the full terminal width for help (yargs caps at 80 by default).
|
|
88
108
|
.wrap(cli.terminalWidth())
|
|
89
109
|
.command(create.command, create.description, create.builder, create.handler)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** What kind of failure yargs is reporting, and the message when it has one. */
|
|
2
|
+
export type CliFailure =
|
|
3
|
+
/** A usage/validation/parse error: the caller's argv was wrong, so help helps. */
|
|
4
|
+
{
|
|
5
|
+
kind: 'usage';
|
|
6
|
+
message: string;
|
|
7
|
+
}
|
|
8
|
+
/** A command handler threw or rejected: help is noise, `index.ts` prints it. */
|
|
9
|
+
| {
|
|
10
|
+
kind: 'runtime';
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Classify a yargs `fail(msg, err)` callback into the two cases that need
|
|
14
|
+
* different output.
|
|
15
|
+
*
|
|
16
|
+
* Pure on purpose — the whole point is that the rule is testable without
|
|
17
|
+
* driving a process to its death. `cli-fail.spec.ts` additionally pins the
|
|
18
|
+
* yargs-side half (which call site passes `null`) against a real instance, so a
|
|
19
|
+
* yargs upgrade that changes the routing fails a test instead of silently
|
|
20
|
+
* turning every runtime error back into a help dump.
|
|
21
|
+
*/
|
|
22
|
+
export declare function classifyCliFailure(msg: string | null | undefined, _err: unknown): CliFailure;
|
package/lib/cli-fail.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// How the CLI ends when something goes wrong — the one place that decides
|
|
2
|
+
// whether a failure is a USAGE error (help is the right output) or a RUNTIME
|
|
3
|
+
// error (help is noise that buries the answer).
|
|
4
|
+
//
|
|
5
|
+
// The defect this exists for, measured from outside the repo: `gjsify build`
|
|
6
|
+
// under GJS without `@gjsify/rolldown-native` prints a CORRECT and actionable
|
|
7
|
+
// diagnosis — `diagnoseNativeEngine()` names the package and the exact install
|
|
8
|
+
// command — and then yargs prints ~60 lines of `gjsify build` help on top of it
|
|
9
|
+
// and exits 1. In a CI log the actionable line scrolls out of view and the
|
|
10
|
+
// visible artifact is a help dump, which reads as "bad arguments". That is what
|
|
11
|
+
// ts-for-gir's `test-e2e` leg looked like (#1005). A missing engine is not a
|
|
12
|
+
// usage error and must not leave through the usage path.
|
|
13
|
+
//
|
|
14
|
+
// Four things about yargs make this module's shape non-obvious. All four were
|
|
15
|
+
// measured against the pinned `yargs@18.0.0` (note `@types/yargs` is still 17,
|
|
16
|
+
// one major behind, so the types lie in one place — see below):
|
|
17
|
+
//
|
|
18
|
+
// 1. `.fail()` IS reached for an async handler rejection. `command.js:253-260`
|
|
19
|
+
// does `if (isPromise(innerArgv) && !hasParseCallback()) innerArgv.catch(e =>
|
|
20
|
+
// usage.fail(null, e))`, and `hasParseCallback()` is false because
|
|
21
|
+
// `cli-app.ts` calls `parseAsync()` with no function argument. So no
|
|
22
|
+
// try/catch around `parseAsync` is needed to see handler errors here.
|
|
23
|
+
//
|
|
24
|
+
// 2. THE DISCRIMINATOR IS `msg`, NOT `err`. Every usage/validation/parse
|
|
25
|
+
// failure passes a STRING: all ten `validation.js` sites call
|
|
26
|
+
// `usage.fail(msg)`, `command.js:370` calls `fail(parsed.error.message,
|
|
27
|
+
// parsed.error)`, and `yargs-factory.js:188/192/1473` call `fail(msg, err)`.
|
|
28
|
+
// Only `command.js:257` — the handler rejection — passes `null`. Branching on
|
|
29
|
+
// `err` instead would suppress help for genuine parser errors, which is
|
|
30
|
+
// exactly backwards.
|
|
31
|
+
//
|
|
32
|
+
// 3. REGISTERING A HANDLER REMOVES YARGS' OWN EXIT. With no `.fail()` yargs does
|
|
33
|
+
// `showHelp('error')` + `logger.error(msg || err)` + `exit(1)` itself
|
|
34
|
+
// (`usage.js:27-70`). With one, it does none of that — so the usage branch has
|
|
35
|
+
// to terminate on its own. `showHelp()` happens to set `hasOutput`
|
|
36
|
+
// (`yargs-factory.js:834`), which blocks the handler while the chain is still
|
|
37
|
+
// synchronous, but that is a side effect, not a guarantee: `throw` is what
|
|
38
|
+
// survives a future async builder or middleware.
|
|
39
|
+
//
|
|
40
|
+
// 4. NEVER RE-THROW THE INCOMING `YError`. `yargs-factory.js:1471-1475` routes
|
|
41
|
+
// that class back into `usage.fail()`, so re-throwing it prints the help a
|
|
42
|
+
// second time. The usage branch throws a plain `Error` with the original
|
|
43
|
+
// attached as `cause`.
|
|
44
|
+
//
|
|
45
|
+
// The runtime branch deliberately prints NOTHING and exits NOTHING: the rejection
|
|
46
|
+
// propagates out of `parseAsync()` on its own, and `index.ts` is the single
|
|
47
|
+
// printer (it also handles `GJSIFY_DEBUG` stacks and the GJS-side `gjsExit(1)`,
|
|
48
|
+
// which a second printer here would duplicate). Suppressing yargs'
|
|
49
|
+
// help-and-exit IS this branch's whole job — it only looks like a no-op.
|
|
50
|
+
/**
|
|
51
|
+
* Classify a yargs `fail(msg, err)` callback into the two cases that need
|
|
52
|
+
* different output.
|
|
53
|
+
*
|
|
54
|
+
* Pure on purpose — the whole point is that the rule is testable without
|
|
55
|
+
* driving a process to its death. `cli-fail.spec.ts` additionally pins the
|
|
56
|
+
* yargs-side half (which call site passes `null`) against a real instance, so a
|
|
57
|
+
* yargs upgrade that changes the routing fails a test instead of silently
|
|
58
|
+
* turning every runtime error back into a help dump.
|
|
59
|
+
*/
|
|
60
|
+
export function classifyCliFailure(msg, _err) {
|
|
61
|
+
// A non-empty string means yargs itself diagnosed the argv. An empty string
|
|
62
|
+
// is treated as "no message" rather than as an empty usage error: it carries
|
|
63
|
+
// nothing to print, and printing help with no reason is the worst of both.
|
|
64
|
+
if (typeof msg === 'string' && msg.length > 0)
|
|
65
|
+
return { kind: 'usage', message: msg };
|
|
66
|
+
return { kind: 'runtime' };
|
|
67
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { LeafCommand } from '../types/index.js';
|
|
2
2
|
interface AffectedOptions {
|
|
3
3
|
base?: string;
|
|
4
4
|
head?: string;
|
|
@@ -6,5 +6,5 @@ interface AffectedOptions {
|
|
|
6
6
|
'changed-from-stdin': boolean;
|
|
7
7
|
cwd?: string;
|
|
8
8
|
}
|
|
9
|
-
export declare const affectedCommand:
|
|
9
|
+
export declare const affectedCommand: LeafCommand<unknown, AffectedOptions>;
|
|
10
10
|
export {};
|
package/lib/commands/affected.js
CHANGED
|
@@ -223,6 +223,22 @@ const OTHER_WORKFLOW_INPUTS = [
|
|
|
223
223
|
// other. Only the `scripts/`-side repo-scoped half is ignored, and that
|
|
224
224
|
// half is precisely the half `main.yml` does not load.
|
|
225
225
|
/^scripts\/manifest-conformance\//,
|
|
226
|
+
// The authored status data (ADR 0016) and the generator that renders it.
|
|
227
|
+
// Same two halves as the entry above, and both hold: `main.yml` reads
|
|
228
|
+
// NEITHER — it never runs `npm run status:generate` and never opens
|
|
229
|
+
// `status/`, which the grep for it confirms (the only hits are comments
|
|
230
|
+
// citing `open-todos.md` as a ledger) — while `audit-runtimes.yml` runs the
|
|
231
|
+
// `status-data` rule over exactly this data on EVERY pull_request with no
|
|
232
|
+
// `paths:` filter.
|
|
233
|
+
//
|
|
234
|
+
// WHAT IT COSTS TODAY: `status/*.md` already fell into `IGNORE` through the
|
|
235
|
+
// generic `/\.md$/i`, so the drift was invisible for the prose files and
|
|
236
|
+
// showed up only on `status/status.json` — which matched nothing, landed in
|
|
237
|
+
// `unmatched`, and forced a FULL CI run for a file `main.yml` cannot read.
|
|
238
|
+
// Editing one package's status prose paid for the whole matrix. Naming the
|
|
239
|
+
// directory makes the `.md` case intentional rather than incidental.
|
|
240
|
+
/^status\//,
|
|
241
|
+
/^scripts\/generate-status\.mjs$/,
|
|
226
242
|
];
|
|
227
243
|
/** Patterns that contribute no seed and don't force a full run. */
|
|
228
244
|
const IGNORE = [
|
|
@@ -61,4 +61,59 @@ export declare function buildWorkspaceBinShimsForWindows(wsLocation: string, nod
|
|
|
61
61
|
cmd: string;
|
|
62
62
|
ps1: string;
|
|
63
63
|
};
|
|
64
|
+
/**
|
|
65
|
+
* Lay the GJS bundler engine down after a PROJECT install, when the host will
|
|
66
|
+
* need it and does not have it. Closes #1005.
|
|
67
|
+
*
|
|
68
|
+
* The engine is an OPTIONAL PEER of `@gjsify/cli`, deliberately: a plain
|
|
69
|
+
* `npm install @gjsify/cli` on Node/macOS/Windows must not fetch Linux
|
|
70
|
+
* prebuilds. But nobody resolves it for a project — npm 7+ skips optional peers,
|
|
71
|
+
* and the native backend does not resolve peerDependencies at all
|
|
72
|
+
* (`install-backend-native.ts:76`). Under GJS there is no npm `rolldown`
|
|
73
|
+
* fallback, so `gjsify build` simply cannot work. Every consumer in the
|
|
74
|
+
* workspace independently discovered this and hand-declared the engine.
|
|
75
|
+
*
|
|
76
|
+
* Four conditions, and the second is the one worth reading:
|
|
77
|
+
*
|
|
78
|
+
* (a) `@gjsify/cli` is in this tree — otherwise the engine has no caller.
|
|
79
|
+
* (b) THE HOST CAN RUN GJS, which is NOT the same question as `isGjs()`.
|
|
80
|
+
* `isGjs()` asks which runtime is executing the installer; the launcher
|
|
81
|
+
* (`bin-shim.ts:616`) prefers the GJS bundle whenever `command -v gjs`
|
|
82
|
+
* succeeds, so a tree installed BY NODE is routinely built BY GJS. Gating
|
|
83
|
+
* on the interpreter would reproduce #1005 for every contributor who
|
|
84
|
+
* installs on Node and builds through the shim. `runMinimalChecks()` above
|
|
85
|
+
* already probed for gjs, so this costs no extra spawn — and it goes
|
|
86
|
+
* nowhere near `which`.
|
|
87
|
+
* (c) No engine is already reachable — asked with the same walk the BUILD uses.
|
|
88
|
+
* (d) Not `--immutable`. A frozen install cannot acquire something its
|
|
89
|
+
* lockfile does not name without breaking the contract `--immutable`
|
|
90
|
+
* exists to hold, so that case warns and installs nothing.
|
|
91
|
+
*
|
|
92
|
+
* Deliberately NOT gated on `gjsify.app` or on "declares a build script":
|
|
93
|
+
* neither of the failing consumers declares the former, and the latter would
|
|
94
|
+
* mean pattern-matching script bodies — the hand-maintained judgement that
|
|
95
|
+
* drifts, which ADR 0017 rejects by name.
|
|
96
|
+
*
|
|
97
|
+
* NOT recorded in the lockfile. `installPackages` writes one only under
|
|
98
|
+
* `if (resolved && opts.lockfile)` and the default is falsy, so a consumer's
|
|
99
|
+
* `gjsify-lock.json` is never rewritten by this. An e2e row pins that.
|
|
100
|
+
*
|
|
101
|
+
* Ordering is not a constraint here, unlike the global path (which must precede
|
|
102
|
+
* `linkGlobalBins`): `buildNativeEnvPreamble` scans the prefix at LAUNCH time
|
|
103
|
+
* (`bin-shim.ts:432-444`), so a shim written before the engine arrives still
|
|
104
|
+
* exports its dir. `tests/e2e/global-install-engine/run.mjs:361-374` asserts
|
|
105
|
+
* exactly that case.
|
|
106
|
+
*/
|
|
107
|
+
export interface EnsureProjectGjsEngineDeps {
|
|
108
|
+
/** Project root. Defaults to `process.cwd()`. */
|
|
109
|
+
cwd?: string;
|
|
110
|
+
/** Injected so the decision is testable without a registry — the shape
|
|
111
|
+
* `installGjsEnginePackages` already uses for the same reason. */
|
|
112
|
+
installFn?: (prefix: string, version: string, opts: {
|
|
113
|
+
verbose?: boolean;
|
|
114
|
+
}) => Promise<void>;
|
|
115
|
+
/** Injected so a host that HAS an engine can still exercise the missing branch. */
|
|
116
|
+
hasEngineFn?: (dir: string) => boolean;
|
|
117
|
+
}
|
|
118
|
+
export declare function ensureProjectGjsEngine(args: InstallOptions, gjsFound: boolean, deps?: EnsureProjectGjsEngineDeps): Promise<void>;
|
|
64
119
|
export {};
|