@gjsify/cli 0.23.0 → 0.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -5
- package/dist/cli.gjs.mjs +93 -85
- package/lib/actions/build.d.ts +12 -0
- package/lib/actions/build.js +26 -0
- package/lib/bundler-pick.js +54 -13
- package/lib/commands/affected.js +62 -4
- package/lib/commands/foreach.js +71 -1
- package/lib/commands/install.js +20 -4
- package/lib/commands/showcase.js +17 -13
- package/lib/commands/storybook.js +5 -2
- package/lib/utils/detect-native-packages.d.ts +26 -21
- package/lib/utils/detect-native-packages.js +53 -41
- package/lib/utils/gjs-bundle-guard.d.ts +32 -0
- package/lib/utils/gjs-bundle-guard.js +103 -0
- package/lib/utils/oxc-resolve.js +7 -2
- package/lib/utils/run-node.d.ts +16 -0
- package/lib/utils/run-node.js +28 -7
- package/lib/utils/runtimes.d.ts +21 -0
- package/lib/utils/runtimes.js +23 -0
- package/package.json +17 -17
package/lib/actions/build.d.ts
CHANGED
|
@@ -48,6 +48,18 @@ 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 gate for `--app gjs`: throw when the emitted bundle still
|
|
53
|
+
* STATICALLY imports Node builtins, which stock GJS cannot resolve (see
|
|
54
|
+
* `utils/gjs-bundle-guard.ts` for the full rationale).
|
|
55
|
+
*
|
|
56
|
+
* The oracle is the bundler's own module graph — each chunk's `imports`
|
|
57
|
+
* list — not the emitted text. Both engines report it (npm `rolldown`
|
|
58
|
+
* natively, `@gjsify/rolldown-native` via `synthRolldownOutput`), and it
|
|
59
|
+
* cannot be fooled by a `node:` specifier quoted inside a string, which a
|
|
60
|
+
* text scan cannot tell from a statement.
|
|
61
|
+
*/
|
|
62
|
+
private assertGjsOutputLoadable;
|
|
51
63
|
/**
|
|
52
64
|
* Post-bundle step for a `--app browser` HTML entry: write the processed
|
|
53
65
|
* `index.html` beside the JS bundle, with the entry `<script>`'s `src`
|
package/lib/actions/build.js
CHANGED
|
@@ -10,6 +10,7 @@ import { chmod, mkdir, 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
|
+
import { assertGjsBundleLoadable } from '../utils/gjs-bundle-guard.js';
|
|
13
14
|
const DEFAULT_GJS_SHEBANG = '#!/usr/bin/env -S gjs -m';
|
|
14
15
|
/**
|
|
15
16
|
* `true` when `path` points at a location that's unsafe to use as a build
|
|
@@ -275,6 +276,21 @@ export class BuildAction {
|
|
|
275
276
|
if (verbose)
|
|
276
277
|
console.debug(`[gjsify] --shebang: wrote ${line} + chmod 0o755 to ${outfile}`);
|
|
277
278
|
}
|
|
279
|
+
/**
|
|
280
|
+
* Post-bundle gate for `--app gjs`: throw when the emitted bundle still
|
|
281
|
+
* STATICALLY imports Node builtins, which stock GJS cannot resolve (see
|
|
282
|
+
* `utils/gjs-bundle-guard.ts` for the full rationale).
|
|
283
|
+
*
|
|
284
|
+
* The oracle is the bundler's own module graph — each chunk's `imports`
|
|
285
|
+
* list — not the emitted text. Both engines report it (npm `rolldown`
|
|
286
|
+
* natively, `@gjsify/rolldown-native` via `synthRolldownOutput`), and it
|
|
287
|
+
* cannot be fooled by a `node:` specifier quoted inside a string, which a
|
|
288
|
+
* text scan cannot tell from a statement.
|
|
289
|
+
*/
|
|
290
|
+
assertGjsOutputLoadable(result, outfile, outdir) {
|
|
291
|
+
const chunks = (result.output ?? []).filter((item) => item.type === 'chunk');
|
|
292
|
+
assertGjsBundleLoadable(chunks, outfile ?? outdir ?? 'the GJS bundle');
|
|
293
|
+
}
|
|
278
294
|
/**
|
|
279
295
|
* Post-bundle step for a `--app browser` HTML entry: write the processed
|
|
280
296
|
* `index.html` beside the JS bundle, with the entry `<script>`'s `src`
|
|
@@ -576,6 +592,10 @@ export class BuildAction {
|
|
|
576
592
|
if ((app === 'gjs' || app === 'node') && this.configData.shebang) {
|
|
577
593
|
await this.applyShebang(app, outfile, verbose);
|
|
578
594
|
}
|
|
595
|
+
// Refuse to hand back a `--app gjs` bundle stock GJS cannot even load.
|
|
596
|
+
if (app === 'gjs') {
|
|
597
|
+
this.assertGjsOutputLoadable(writeResult, outfile, outdir);
|
|
598
|
+
}
|
|
579
599
|
// Browser HTML entry: emit the processed `index.html` beside the bundle
|
|
580
600
|
// (engine-portable post-step — the native rolldown engine has no
|
|
581
601
|
// `emitFile`, so this can't live in a plugin). `outfile` is guaranteed
|
|
@@ -626,6 +646,12 @@ export class BuildAction {
|
|
|
626
646
|
if ((app === 'gjs' || app === 'node') && this.configData.shebang) {
|
|
627
647
|
await this.applyShebang(app, outfile, verbose);
|
|
628
648
|
}
|
|
649
|
+
// No `--app gjs` loadability gate here: the watcher's
|
|
650
|
+
// BUNDLE_END carries the bundle, not the generated
|
|
651
|
+
// output, and re-running `generate()` just to read the
|
|
652
|
+
// chunk graph would double every rebuild. The one-shot
|
|
653
|
+
// build is the gate that CI and the pre-commit hook go
|
|
654
|
+
// through; `--watch` is a dev loop.
|
|
629
655
|
}
|
|
630
656
|
finally {
|
|
631
657
|
await event.result.close();
|
package/lib/bundler-pick.js
CHANGED
|
@@ -50,16 +50,7 @@ async function loadNpmRolldown() {
|
|
|
50
50
|
if (isGjs()) {
|
|
51
51
|
throw new Error('gjsify build: no usable bundler engine under GJS — `@gjsify/rolldown-native` is not loadable, ' +
|
|
52
52
|
'and the npm `rolldown` engine is a Rust napi crate that cannot run under GJS.\n' +
|
|
53
|
-
|
|
54
|
-
'dist/cli.gjs.mjs), which bypasses the `gjsify` launcher that exports GI_TYPELIB_PATH / ' +
|
|
55
|
-
'LD_LIBRARY_PATH for the prebuilds. The typelib lookup happens inside the GJS runtime, so those ' +
|
|
56
|
-
'must be set BEFORE the process starts — the CLI cannot repair it from the inside. ' +
|
|
57
|
-
'Run through the `gjsify` bin instead (`node_modules/.bin/gjsify …`, or the globally installed shim), ' +
|
|
58
|
-
'or export the env yourself:\n' +
|
|
59
|
-
' P=<repo>/packages/infra/rolldown-native/prebuilds/linux-x86_64\n' +
|
|
60
|
-
' GI_TYPELIB_PATH=$P LD_LIBRARY_PATH=$P gjs -m …/cli.gjs.mjs build …\n' +
|
|
61
|
-
'Other causes: the JS facade `lib/` is not built (`gjsify workspace @gjsify/rolldown-native build` ' +
|
|
62
|
-
'under Node), or no prebuild exists for this architecture. Running the build under Node also works.');
|
|
53
|
+
diagnoseNativeEngine());
|
|
63
54
|
}
|
|
64
55
|
// Indirect specifier so Rolldown's static-analysis doesn't try to
|
|
65
56
|
// bundle the npm crate into a GJS target build.
|
|
@@ -68,6 +59,58 @@ async function loadNpmRolldown() {
|
|
|
68
59
|
const mod = (await import(/* @vite-ignore */ target));
|
|
69
60
|
return mod.rolldown;
|
|
70
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Say WHY the native engine could not be loaded, read off disk rather than
|
|
64
|
+
* guessed.
|
|
65
|
+
*
|
|
66
|
+
* The three causes need three different fixes, and this message used to assert
|
|
67
|
+
* the rarest of them ("the CLI bundle was invoked DIRECTLY … bypasses the
|
|
68
|
+
* `gjsify` launcher") as the "most likely cause". On the by-far most common
|
|
69
|
+
* one — a freshly cloned + freshly installed tree, where the engine's JS facade
|
|
70
|
+
* has simply never been built — that reading is not merely unhelpful, it points
|
|
71
|
+
* at a launcher that was in fact used correctly, so the reader goes looking for
|
|
72
|
+
* an env-var problem that does not exist. `scripts/verify-committed-bundles.mjs`
|
|
73
|
+
* hit exactly that and it blocked releases.
|
|
74
|
+
*
|
|
75
|
+
* The probe is `existsSync`-only (no import, no resolution) so it cannot itself
|
|
76
|
+
* throw or recurse into the failure being explained.
|
|
77
|
+
*/
|
|
78
|
+
function diagnoseNativeEngine() {
|
|
79
|
+
let pkgDir = null;
|
|
80
|
+
try {
|
|
81
|
+
const bundleDir = path.dirname(new URL(import.meta.url).pathname);
|
|
82
|
+
pkgDir = findRolldownNativeDir(process.cwd(), bundleDir);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
pkgDir = null;
|
|
86
|
+
}
|
|
87
|
+
if (pkgDir === null) {
|
|
88
|
+
return ('`@gjsify/rolldown-native` is NOT INSTALLED — no node_modules/@gjsify/rolldown-native was found ' +
|
|
89
|
+
`walking up from ${process.cwd()} or from the CLI bundle's own directory.\n` +
|
|
90
|
+
'Install it: `gjsify install @gjsify/rolldown-native` (in the gjsify repo: `gjsify install`). ' +
|
|
91
|
+
'Running the build under Node also works — there the npm `rolldown` crate is used instead.');
|
|
92
|
+
}
|
|
93
|
+
const facade = path.join(pkgDir, 'lib', 'esm', 'index.js');
|
|
94
|
+
if (!existsSync(facade)) {
|
|
95
|
+
return (`The engine's JS facade is NOT BUILT: ${facade} does not exist.\n` +
|
|
96
|
+
'It is a BUILD OUTPUT, not something `gjsify install` produces — so a freshly cloned or freshly ' +
|
|
97
|
+
'installed tree never has it, and every `gjsify build` under GJS fails here until it is built once. ' +
|
|
98
|
+
'Building it needs a bundler, which is why it cannot bootstrap itself under GJS; the Node CLI entry ' +
|
|
99
|
+
'does it instead. From the gjsify repo root:\n' +
|
|
100
|
+
' gjsify run build:infra # the full cold-tree chain (recommended)\n' +
|
|
101
|
+
' node scripts/bootstrap-native-facades.mjs # just the facades, if the rest is already built\n' +
|
|
102
|
+
'Both are idempotent and skip already-built facades in seconds.');
|
|
103
|
+
}
|
|
104
|
+
return ('The JS facade is built, so this is the typelib/prebuild lookup failing. Most likely the CLI bundle was ' +
|
|
105
|
+
'invoked DIRECTLY (`gjs -m …/cli.gjs.mjs`, or by executing dist/cli.gjs.mjs), which bypasses the `gjsify` ' +
|
|
106
|
+
'launcher that exports GI_TYPELIB_PATH / LD_LIBRARY_PATH for the prebuilds. The typelib lookup happens ' +
|
|
107
|
+
'inside the GJS runtime, so those must be set BEFORE the process starts — the CLI cannot repair it from ' +
|
|
108
|
+
'the inside. Run through the `gjsify` bin instead (`node_modules/.bin/gjsify …`, or the globally installed ' +
|
|
109
|
+
'shim), or export the env yourself:\n' +
|
|
110
|
+
` P=${path.join(pkgDir, 'prebuilds', `${process.platform ?? 'linux'}-${process.arch ?? 'x64'}`)}\n` +
|
|
111
|
+
' GI_TYPELIB_PATH=$P LD_LIBRARY_PATH=$P gjs -m …/cli.gjs.mjs build …\n' +
|
|
112
|
+
'Otherwise: no prebuild exists for this architecture. Running the build under Node also works.');
|
|
113
|
+
}
|
|
71
114
|
/**
|
|
72
115
|
* Convert a bare npm specifier into something dynamic `import(...)` can
|
|
73
116
|
* load regardless of host runtime:
|
|
@@ -295,9 +338,7 @@ async function tryLoadNative() {
|
|
|
295
338
|
return null;
|
|
296
339
|
}
|
|
297
340
|
})();
|
|
298
|
-
const resolved = resolvedFromNpm ??
|
|
299
|
-
resolvedFromFs ??
|
|
300
|
-
createRequire(import.meta.url).resolve(specifier);
|
|
341
|
+
const resolved = resolvedFromNpm ?? resolvedFromFs ?? createRequire(import.meta.url).resolve(specifier);
|
|
301
342
|
target = pathToFileURL(resolved).href;
|
|
302
343
|
}
|
|
303
344
|
const mod = (await import(/* @vite-ignore */ target));
|
package/lib/commands/affected.js
CHANGED
|
@@ -45,6 +45,38 @@
|
|
|
45
45
|
// --format=globs one `@gjsify/<name>` per line
|
|
46
46
|
// --format=github-actions $GITHUB_OUTPUT key=value lines
|
|
47
47
|
//
|
|
48
|
+
// `include-args` CONTRACT (github-actions format) — read this before changing
|
|
49
|
+
// its shape. The value is a SPACE-SEPARATED list of ALREADY-SHELL-SAFE tokens:
|
|
50
|
+
//
|
|
51
|
+
// include-args=--include @gjsify/fs --include @gjsify/stream
|
|
52
|
+
//
|
|
53
|
+
// and its only supported consumption is an UNQUOTED `$INCLUDE_ARGS` expansion
|
|
54
|
+
// in a POSIX shell, where word splitting is exactly the intent. It carries NO
|
|
55
|
+
// quoting of its own, deliberately:
|
|
56
|
+
//
|
|
57
|
+
// * `main.yml` splices it into a `su testuser -c "… sh -c '…'"` nesting, and
|
|
58
|
+
// the result of a parameter expansion is NEVER re-scanned for quotes. When
|
|
59
|
+
// this emitted `--include '@gjsify/fs'`, the apostrophes became part of the
|
|
60
|
+
// glob, every pattern matched zero workspaces, and `foreach` exited 0 —
|
|
61
|
+
// CI "built" nothing on every selective PR run in 0.65 s (repo task #75).
|
|
62
|
+
// Pre-quoting a value for an unknown number of nesting levels cannot work;
|
|
63
|
+
// emitting tokens that need no quoting at any level can.
|
|
64
|
+
// * There are FIVE consumers of this one output — the build / browser-bundle
|
|
65
|
+
// / check / test steps splice it into a command line, and thirteen
|
|
66
|
+
// `contains(needs.changes.outputs.include-args, '@gjsify/…')` GitHub
|
|
67
|
+
// expressions substring-match it. A format that requires per-consumer
|
|
68
|
+
// unquoting would have to be repeated five times in the very nesting that
|
|
69
|
+
// produced the bug; `contains()` is unaffected either way (it matched the
|
|
70
|
+
// quoted spelling too, which is why the gates kept working while the build
|
|
71
|
+
// silently did nothing).
|
|
72
|
+
//
|
|
73
|
+
// Shell-safety is ENFORCED, not assumed: `assertShellSafeWorkspaceName` rejects
|
|
74
|
+
// any workspace name outside `[A-Za-z0-9@._/-]` before it is emitted. npm names
|
|
75
|
+
// cannot contain those characters, so this never fires in practice — and if it
|
|
76
|
+
// somehow does, the command exits non-zero, the classify step's
|
|
77
|
+
// `continue-on-error` leaves the outputs unset, and CI falls back to a FULL
|
|
78
|
+
// run. Failing towards "run everything" is the safe direction.
|
|
79
|
+
//
|
|
48
80
|
// `--changed-from-stdin` skips `git diff` entirely and reads a newline-
|
|
49
81
|
// separated list of paths from stdin. Useful for local debugging and
|
|
50
82
|
// for the spec suite. Stdin is slurped via `utils/stdin.ts`, NOT
|
|
@@ -52,6 +84,15 @@
|
|
|
52
84
|
// idiom opens the relative file `./0` and the committed GJS classifier
|
|
53
85
|
// bundle died with `ENOENT … read '0'` on every `--changed-from-stdin` run.
|
|
54
86
|
import { spawnSync } from 'node:child_process';
|
|
87
|
+
// STATIC import, never a lazy `require('node:fs')`. This package is ESM
|
|
88
|
+
// (`"type": "module"`), so a bare `require` is a ReferenceError the moment the
|
|
89
|
+
// line executes — and the only line that executes it sits behind
|
|
90
|
+
// `if (process.env.GITHUB_OUTPUT)`, i.e. exclusively inside a GitHub Actions
|
|
91
|
+
// step. That is why it survived from the command's first commit: every local
|
|
92
|
+
// run and every spec took the stdout branch. There is no reason for laziness
|
|
93
|
+
// here — the command already statically imports `node:child_process`, and the
|
|
94
|
+
// classifier bundle resolves `node:fs` through `@gjsify/fs` like any other.
|
|
95
|
+
import { appendFileSync } from 'node:fs';
|
|
55
96
|
import { readStdinLines } from '../utils/stdin.js';
|
|
56
97
|
import { discoverWorkspaces, buildDependencyGraph, buildReverseDependencyGraph, affectedClosure, workspacesForChangedFiles, } from '@gjsify/workspace';
|
|
57
98
|
import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
@@ -318,9 +359,11 @@ function emit(format, r) {
|
|
|
318
359
|
return;
|
|
319
360
|
}
|
|
320
361
|
if (format === 'github-actions') {
|
|
362
|
+
// Unquoted, space-separated tokens — see the `include-args` CONTRACT
|
|
363
|
+
// block at the top of this file. Do NOT re-add quoting here.
|
|
321
364
|
const includeArgs = r.global
|
|
322
365
|
? ''
|
|
323
|
-
: r.workspaces.map((n) => `--include
|
|
366
|
+
: r.workspaces.map((n) => `--include ${assertShellSafeWorkspaceName(n)}`).join(' ');
|
|
324
367
|
const out = process.env.GITHUB_OUTPUT;
|
|
325
368
|
const lines = [
|
|
326
369
|
`skip-all=${r.skipAll}`,
|
|
@@ -332,7 +375,6 @@ function emit(format, r) {
|
|
|
332
375
|
];
|
|
333
376
|
if (out) {
|
|
334
377
|
// GitHub Actions: append to $GITHUB_OUTPUT.
|
|
335
|
-
const { appendFileSync } = require('node:fs');
|
|
336
378
|
for (const l of lines)
|
|
337
379
|
appendFileSync(out, `${l}\n`);
|
|
338
380
|
}
|
|
@@ -353,6 +395,22 @@ function emit(format, r) {
|
|
|
353
395
|
for (const name of r.workspaces)
|
|
354
396
|
process.stdout.write(` - ${name}\n`);
|
|
355
397
|
}
|
|
356
|
-
|
|
357
|
-
|
|
398
|
+
// A workspace name that survives an UNQUOTED shell expansion unchanged: no
|
|
399
|
+
// whitespace, no quote/escape characters, no glob metacharacters, no `$`, and
|
|
400
|
+
// nothing the shell would treat as an operator. npm package names are a strict
|
|
401
|
+
// subset of this, so the guard is a machine-checked restatement of the
|
|
402
|
+
// `include-args` contract rather than a real runtime branch. Returns the name
|
|
403
|
+
// so it can be used inline at the emit site.
|
|
404
|
+
const SHELL_SAFE_WORKSPACE_NAME = /^[A-Za-z0-9@][A-Za-z0-9@._/-]*$/;
|
|
405
|
+
function assertShellSafeWorkspaceName(name) {
|
|
406
|
+
if (SHELL_SAFE_WORKSPACE_NAME.test(name))
|
|
407
|
+
return name;
|
|
408
|
+
process.stderr.write(`gjsify affected: workspace name ${JSON.stringify(name)} is not shell-safe, so it cannot be emitted as an\n` +
|
|
409
|
+
` \`include-args\` token (the value is consumed by an UNQUOTED $INCLUDE_ARGS expansion — see the\n` +
|
|
410
|
+
` contract at the top of commands/affected.ts). Rename the workspace, or teach this command a\n` +
|
|
411
|
+
` transport the consumers can unquote. Refusing to emit a filter that would silently mis-match.\n`);
|
|
412
|
+
process.exit(2);
|
|
413
|
+
// Unreachable — `process.exit` terminates. Present so this function has no
|
|
414
|
+
// fall-through path that could ever return an unsafe name.
|
|
415
|
+
throw new Error(`gjsify affected: unsafe workspace name ${name}`);
|
|
358
416
|
}
|
package/lib/commands/foreach.js
CHANGED
|
@@ -208,7 +208,7 @@ export const foreachCommand = {
|
|
|
208
208
|
default: false,
|
|
209
209
|
})
|
|
210
210
|
.option('include', {
|
|
211
|
-
description: 'Glob pattern to include workspaces by name (repeatable).',
|
|
211
|
+
description: 'Glob pattern to include workspaces by name (repeatable). A pattern that matches NO discovered workspace is a hard error — a filter selecting nothing is a typo or a caller-side quoting bug, never an intent.',
|
|
212
212
|
type: 'string',
|
|
213
213
|
array: true,
|
|
214
214
|
})
|
|
@@ -295,6 +295,7 @@ export const foreachCommand = {
|
|
|
295
295
|
process.exit(1);
|
|
296
296
|
}
|
|
297
297
|
}
|
|
298
|
+
assertEveryIncludeMatches(allWorkspaces, args.include);
|
|
298
299
|
let selected = filterWorkspaces(allWorkspaces, {
|
|
299
300
|
include: args.include,
|
|
300
301
|
exclude: args.exclude,
|
|
@@ -344,6 +345,12 @@ export const foreachCommand = {
|
|
|
344
345
|
}
|
|
345
346
|
}
|
|
346
347
|
if (selected.length === 0) {
|
|
348
|
+
// Reaching here means every --include pattern DID match something
|
|
349
|
+
// (assertEveryIncludeMatches above is fatal otherwise) and the set
|
|
350
|
+
// was then narrowed to nothing by --exclude, --no-private, the
|
|
351
|
+
// script-presence filter, or --shard. All four are legitimate, so
|
|
352
|
+
// this stays a clean exit-0 no-op — see the assert's doc comment
|
|
353
|
+
// for why the two cases are not the same condition.
|
|
347
354
|
console.log(`gjsify foreach: no workspaces match (${exec ? 'exec' : 'script'}="${cmd}", include=${JSON.stringify(args.include ?? [])}, exclude=${JSON.stringify(args.exclude ?? [])})`);
|
|
348
355
|
return;
|
|
349
356
|
}
|
|
@@ -410,6 +417,69 @@ export const foreachCommand = {
|
|
|
410
417
|
process.exit(0);
|
|
411
418
|
},
|
|
412
419
|
};
|
|
420
|
+
/**
|
|
421
|
+
* A `--include` pattern that matches ZERO of the discovered workspaces is a
|
|
422
|
+
* hard error. This is the load-bearing half of the fix for repo task #75: the
|
|
423
|
+
* CI classifier emitted its filter pre-quoted (`--include '@gjsify/fs'`), the
|
|
424
|
+
* workflow expanded it unquoted inside a `su … -c "… sh -c '…'"` nesting, the
|
|
425
|
+
* apostrophes became part of every glob, and this command matched nothing,
|
|
426
|
+
* printed one line, and exited 0. A build that did nothing was indistinguishable
|
|
427
|
+
* from a build that succeeded, so the bug lived through every selective PR run
|
|
428
|
+
* — the Build step took 0.65 s and every later step inspected whatever the
|
|
429
|
+
* cache had restored. The quoting was the defect; the SILENCE is why nobody saw
|
|
430
|
+
* it.
|
|
431
|
+
*
|
|
432
|
+
* Scope is deliberately "an include PATTERN matched no workspace", NOT "the
|
|
433
|
+
* final selection is empty". The final selection is legitimately empty in
|
|
434
|
+
* several supported situations and must stay exit-0:
|
|
435
|
+
* * `--shard 3/4` over a 2-workspace closure — most shards run nothing;
|
|
436
|
+
* * `--exclude '@gjsify/integration-*'` removing everything the include kept;
|
|
437
|
+
* * script mode dropping workspaces that don't declare `<script>` (yarn does
|
|
438
|
+
* this too, and it is why `foreach check` over a closure of script-less
|
|
439
|
+
* packages is a no-op rather than a failure).
|
|
440
|
+
* All three describe a real, existing set being narrowed to nothing. A pattern
|
|
441
|
+
* that names something the workspace tree does not contain is different in
|
|
442
|
+
* kind: it is a typo, a stale package name, or a caller-side quoting bug, and
|
|
443
|
+
* it can never be what the caller meant. There is no opt-out flag — an escape
|
|
444
|
+
* hatch here would be re-arming exactly the silence this exists to remove.
|
|
445
|
+
*/
|
|
446
|
+
function assertEveryIncludeMatches(allWorkspaces, include) {
|
|
447
|
+
if (!include || include.length === 0)
|
|
448
|
+
return;
|
|
449
|
+
const unmatched = include.filter((pattern) => filterWorkspaces(allWorkspaces, { include: [pattern] }).length === 0);
|
|
450
|
+
if (unmatched.length === 0)
|
|
451
|
+
return;
|
|
452
|
+
const lines = [
|
|
453
|
+
`gjsify foreach: --include matched no workspace (${unmatched.length} of ${include.length} pattern(s)):`,
|
|
454
|
+
];
|
|
455
|
+
for (const pattern of unmatched) {
|
|
456
|
+
lines.push(` ${JSON.stringify(pattern)}`);
|
|
457
|
+
// The task-#75 signature: the pattern only fails because the caller's
|
|
458
|
+
// shell quotes ended up INSIDE the value. Name it outright — that
|
|
459
|
+
// failure reads as "package not found" and sends people hunting in the
|
|
460
|
+
// wrong place.
|
|
461
|
+
const unquoted = stripSurroundingQuotes(pattern);
|
|
462
|
+
if (unquoted !== pattern && filterWorkspaces(allWorkspaces, { include: [unquoted] }).length > 0) {
|
|
463
|
+
lines.push(` → matches as ${JSON.stringify(unquoted)}: the QUOTES are part of the value. A shell expansion`, ` is not re-scanned for quoting, so a pre-quoted filter spliced through \`sh -c\` arrives with`, ` the quotes attached. Pass the bare name and let word splitting do the work.`);
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
const near = allWorkspaces
|
|
467
|
+
.map((w) => w.name)
|
|
468
|
+
.filter((name) => name.toLowerCase().includes(unquoted.replace(/\*/g, '').toLowerCase()))
|
|
469
|
+
.slice(0, 3);
|
|
470
|
+
if (near.length > 0)
|
|
471
|
+
lines.push(` → did you mean: ${near.join(', ')}?`);
|
|
472
|
+
}
|
|
473
|
+
lines.push(` ${allWorkspaces.length} workspace(s) discovered. Refusing to run: a filter that selects nothing would`, ` otherwise exit 0 and look exactly like a successful run over the whole set.`);
|
|
474
|
+
console.error(lines.join('\n'));
|
|
475
|
+
process.exit(1);
|
|
476
|
+
}
|
|
477
|
+
/** Drop ONE matching pair of surrounding `'` or `"` — the quoting-bug probe. */
|
|
478
|
+
function stripSurroundingQuotes(s) {
|
|
479
|
+
if (s.length >= 2 && (s[0] === "'" || s[0] === '"') && s[s.length - 1] === s[0])
|
|
480
|
+
return s.slice(1, -1);
|
|
481
|
+
return s;
|
|
482
|
+
}
|
|
413
483
|
async function runSequential(workspaces, script, args, verbose, exec, cache) {
|
|
414
484
|
for (const ws of workspaces) {
|
|
415
485
|
await runOne(ws, script, args, /* prefixOutput */ false, verbose, exec, cache);
|
package/lib/commands/install.js
CHANGED
|
@@ -205,10 +205,26 @@ export const installCommand = {
|
|
|
205
205
|
const hardExitTimerId = overallTimeoutMs > 0
|
|
206
206
|
? setTimeout(() => {
|
|
207
207
|
const secs = Math.round(overallTimeoutMs / 100) / 10;
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
208
|
+
// Do NOT assert a cause here — this timer cannot tell the
|
|
209
|
+
// two shapes apart, and claiming the rarer one sends the
|
|
210
|
+
// next reader after a bug that isn't there. Measured on a
|
|
211
|
+
// cold tree of this workspace: 1597 packages / ~4.8 GB
|
|
212
|
+
// extracted, of which ~3.4 GB are foreign-platform
|
|
213
|
+
// optional deps (`os`/`cpu` excludes the host) that npm
|
|
214
|
+
// would never place. That install was still making steady
|
|
215
|
+
// progress when the 30-min budget elapsed, and a re-run
|
|
216
|
+
// finished the remainder in ~2 min. Extraction also has
|
|
217
|
+
// multi-second SYNCHRONOUS stretches (tar parse + file
|
|
218
|
+
// writes) and 16 run concurrently, so the clean abort path
|
|
219
|
+
// can easily miss the grace window with nothing wedged.
|
|
220
|
+
console.error(`gjsify install: the ${secs}s budget elapsed and the abort did not unwind within ` +
|
|
221
|
+
`${Math.round(HARD_EXIT_GRACE_MS / 1000)}s — forcing exit. That means one of two ` +
|
|
222
|
+
`things, and this timer cannot distinguish them: the install was still WORKING ` +
|
|
223
|
+
`(a cold tree here extracts several GB, and extraction blocks the loop in ` +
|
|
224
|
+
`multi-second synchronous stretches), or a step genuinely wedged. Re-running tells ` +
|
|
225
|
+
`you which, and is safe: extraction is idempotent, every package already on disk ` +
|
|
226
|
+
`is skipped, so a second run finishes the remainder in a fraction of the time. A ` +
|
|
227
|
+
`re-run that makes NO further progress is a real wedge — file an issue.`);
|
|
212
228
|
forceExit(1);
|
|
213
229
|
}, overallTimeoutMs + HARD_EXIT_GRACE_MS)
|
|
214
230
|
: null;
|
package/lib/commands/showcase.js
CHANGED
|
@@ -7,8 +7,7 @@ import { ensurePkgDir } from './dlx.js';
|
|
|
7
7
|
import { parseSpec } from '../utils/parse-spec.js';
|
|
8
8
|
import { resolveNodeEntry } from '../utils/resolve-gjs-entry.js';
|
|
9
9
|
import { runRuntimeBundle } from '../utils/run-node.js';
|
|
10
|
-
import {
|
|
11
|
-
import { EXAMPLE_RUNTIMES, isExampleRuntime, readDeclaredRuntimes, checkRuntimeSupported, } from '../utils/runtimes.js';
|
|
10
|
+
import { EXAMPLE_RUNTIMES, isExampleRuntime, readDeclaredRuntimes, checkRuntimeSupported, defaultExampleRuntime, } from '../utils/runtimes.js';
|
|
12
11
|
function readCliVersion() {
|
|
13
12
|
try {
|
|
14
13
|
const pkgUrl = new URL('../../package.json', import.meta.url);
|
|
@@ -38,18 +37,23 @@ export const showcaseCommand = {
|
|
|
38
37
|
default: false,
|
|
39
38
|
})
|
|
40
39
|
.option('runtime', {
|
|
41
|
-
// Runtime selector, mirroring `gjsify storybook --runtime`.
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
40
|
+
// Runtime selector, mirroring `gjsify storybook --runtime`. gjs
|
|
41
|
+
// runs the showcase's GJS bundle via `gjsify dlx`; node/bun/deno
|
|
42
|
+
// run its `--app node` bundle on that runtime. Validated against
|
|
43
|
+
// the showcase's declared `gjsify.example.runtimes` — a GTK-only
|
|
44
|
+
// showcase requested under node fails with a clear message, not
|
|
45
|
+
// a crash.
|
|
46
|
+
//
|
|
47
|
+
// NO eager yargs `default:` — it is resolved in the handler via
|
|
48
|
+
// `defaultExampleRuntime()` (gjs when gjs is installed, else the
|
|
49
|
+
// host runtime). Two reasons: yargs evaluates a `default:` while
|
|
50
|
+
// BUILDING the command, before we know a showcase was even
|
|
51
|
+
// named, and resolving it probes PATH — work that must not
|
|
52
|
+
// happen for `gjsify showcase` in list mode.
|
|
48
53
|
type: 'string',
|
|
49
54
|
choices: EXAMPLE_RUNTIMES,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
description: 'Runtime to run the showcase on: gjs | node | bun | deno (default: the host runtime). node/bun/deno run its `--app node` bundle.',
|
|
55
|
+
defaultDescription: 'gjs when gjs is installed, else the host runtime',
|
|
56
|
+
description: 'Runtime to run the showcase on: gjs | node | bun | deno (default: gjs when installed, else the host runtime). node/bun/deno run its `--app node` bundle.',
|
|
53
57
|
}),
|
|
54
58
|
handler: async (args) => {
|
|
55
59
|
// List mode: no name given, or --list flag
|
|
@@ -116,7 +120,7 @@ export const showcaseCommand = {
|
|
|
116
120
|
// `return` — bare `process.exit` falls through under GJS (see above).
|
|
117
121
|
return process.exit(1);
|
|
118
122
|
}
|
|
119
|
-
const runtime = (args.runtime ??
|
|
123
|
+
const runtime = (args.runtime ?? defaultExampleRuntime());
|
|
120
124
|
if (!isExampleRuntime(runtime)) {
|
|
121
125
|
console.error(`Unknown --runtime "${runtime}" (expected: ${EXAMPLE_RUNTIMES.join(', ')}).`);
|
|
122
126
|
// `return` — bare `process.exit` falls through under GJS (see above).
|
|
@@ -181,9 +181,12 @@ export const storybookCommand = {
|
|
|
181
181
|
// typelib env wired like `gjsify run`. All three need @gjsify/node-gi
|
|
182
182
|
// (the storybook uses gi://), kept external by the bundler.
|
|
183
183
|
const { computeNativeEnvForBundle } = await import('../utils/run-gjs.js');
|
|
184
|
-
const {
|
|
184
|
+
const { resolveNodeGiForBundle, nodeBinary } = await import('../utils/run-node.js');
|
|
185
185
|
const { RUNTIMES } = await import('../utils/runtimes.js');
|
|
186
|
-
|
|
186
|
+
// Same resolution base as `runRuntimeBundle`: the bundle's own dir
|
|
187
|
+
// first (that is where the runtime resolves the external specifier
|
|
188
|
+
// from), cwd as the fallback.
|
|
189
|
+
if (!resolveNodeGiForBundle(outPath, cwd)) {
|
|
187
190
|
throw new Error(`Cannot run the storybook on ${runtime}: \`@gjsify/node-gi\` is not installed. ` +
|
|
188
191
|
`Add @gjsify/node-gi as a dependency to run the storybook on ${runtime}.`);
|
|
189
192
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export interface NativePackage {
|
|
2
2
|
/** npm package name, e.g. "@gjsify/webgl" */
|
|
3
3
|
name: string;
|
|
4
|
-
/** Absolute path to the arch-specific prebuilds dir, e.g. "/…/@gjsify/webgl/prebuilds/linux-
|
|
4
|
+
/** Absolute path to the arch-specific prebuilds dir, e.g. "/…/@gjsify/webgl/prebuilds/linux-x64" */
|
|
5
5
|
prebuildsDir: string;
|
|
6
6
|
}
|
|
7
7
|
/** The host target a resolution is performed for. Defaults to the running process. */
|
|
@@ -31,34 +31,39 @@ export interface NativeEnv {
|
|
|
31
31
|
[key: string]: string | undefined;
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
|
-
* Canonical `<os>-<arch>` form
|
|
35
|
-
* `
|
|
36
|
-
*
|
|
37
|
-
*
|
|
34
|
+
* Canonical `<os>-<arch>` form — the node spelling — so `linux-x86_64` and
|
|
35
|
+
* `linux-x64` compare equal. Only the *arch* half is normalised; the OS half
|
|
36
|
+
* is always a `process.platform` token (`linux`/`darwin`/`win32`), which has
|
|
37
|
+
* no competing spelling.
|
|
38
38
|
*/
|
|
39
39
|
export declare function canonicalPlatformToken(token: string): string;
|
|
40
40
|
/**
|
|
41
41
|
* The prebuild directory names to probe for a host, most-specific first.
|
|
42
42
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
43
|
+
* The canonical name is `${process.platform}-${process.arch}` — `linux-x64`,
|
|
44
|
+
* `linux-arm64`, `darwin-arm64`, `win32-x64`. Every package in the release
|
|
45
|
+
* train declares it (`gjsify.platforms`), `scripts/stage-prebuild.mjs` is the
|
|
46
|
+
* only thing that creates it, and `scripts/audit-runtimes.mjs --check` rejects
|
|
47
|
+
* any other spelling, so the three can no longer drift.
|
|
45
48
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* * `@gjsify/node-gi` (and `@gjsify/napi`'s prebuild staging) use
|
|
49
|
-
* `prebuilds/${process.platform}-${process.arch}/` — node-style
|
|
50
|
-
* (`linux-x64`, `darwin-arm64`, `win32-x64`).
|
|
49
|
+
* Two probes remain BEHIND that canonical name, and both are backward
|
|
50
|
+
* compatibility with tarballs already on npm — not a second convention:
|
|
51
51
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
52
|
+
* 1. **The package's own declared spelling.** A tarball published before the
|
|
53
|
+
* rename declares (and ships) `linux-x86_64`; probing its declaration
|
|
54
|
+
* first loads it without the CLI having to guess. This is also what makes
|
|
55
|
+
* the rename work in the OTHER direction — an older CLI resolving a newer
|
|
56
|
+
* package — because `canonicalPlatformToken` folds both spellings onto
|
|
57
|
+
* one form at both ends.
|
|
58
|
+
* 2. **The legacy uname spelling.** Only reachable for a package that ships
|
|
59
|
+
* a prebuild dir and declares NO `gjsify.platforms` at all — i.e. a
|
|
60
|
+
* tarball predating the OS-axis audit, or a third-party package using
|
|
61
|
+
* `gjsify.prebuilds` without the declaration. Inside this repo the audit
|
|
62
|
+
* makes that state impossible.
|
|
56
63
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* (`darwin-arm64`, `win32-x64` — on Apple silicon `uname -m` also reports
|
|
61
|
-
* `arm64`, so there node-style *is* the native spelling).
|
|
64
|
+
* Both are pure array entries matched against an already-read directory
|
|
65
|
+
* listing — no extra I/O — so tolerance on the READ side costs nothing, while
|
|
66
|
+
* the single spelling is enforced everywhere a name is WRITTEN.
|
|
62
67
|
*
|
|
63
68
|
* @param platform `process.platform` value.
|
|
64
69
|
* @param arch `process.arch` value.
|