@gjsify/cli 0.22.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 +28 -1
- package/dist/cli.gjs.mjs +265 -184
- package/lib/actions/build.d.ts +12 -0
- package/lib/actions/build.js +26 -0
- package/lib/bundler-pick.js +56 -7
- package/lib/commands/affected.js +84 -14
- package/lib/commands/check.js +38 -29
- package/lib/commands/flatpak/build.js +19 -18
- package/lib/commands/foreach.js +119 -42
- package/lib/commands/info.js +11 -5
- package/lib/commands/install.d.ts +16 -0
- package/lib/commands/install.js +123 -36
- package/lib/commands/onboard.js +6 -1
- package/lib/commands/pack.js +69 -4
- package/lib/commands/run.js +26 -14
- package/lib/commands/showcase.js +17 -13
- package/lib/commands/storybook.js +5 -2
- package/lib/commands/trust.js +4 -62
- package/lib/commands/workspace.js +49 -30
- package/lib/utils/bin-shim.d.ts +66 -0
- package/lib/utils/bin-shim.js +246 -0
- package/lib/utils/build-cache.d.ts +42 -6
- package/lib/utils/build-cache.js +114 -42
- package/lib/utils/detect-native-packages.d.ts +118 -7
- package/lib/utils/detect-native-packages.js +213 -29
- package/lib/utils/gjs-bundle-guard.d.ts +32 -0
- package/lib/utils/gjs-bundle-guard.js +103 -0
- package/lib/utils/install-backend-native.d.ts +24 -0
- package/lib/utils/install-backend-native.js +89 -16
- package/lib/utils/install-global.d.ts +16 -0
- package/lib/utils/install-global.js +58 -8
- package/lib/utils/oxc-resolve.js +25 -57
- package/lib/utils/publish-headers.d.ts +2 -3
- package/lib/utils/publish-headers.js +64 -3
- package/lib/utils/publishable-packages.d.ts +22 -0
- package/lib/utils/publishable-packages.js +93 -0
- package/lib/utils/run-gjs.d.ts +6 -7
- package/lib/utils/run-gjs.js +4 -3
- 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/lib/utils/spawn.d.ts +82 -0
- package/lib/utils/spawn.js +139 -0
- package/lib/utils/stdin.d.ts +25 -0
- package/lib/utils/stdin.js +132 -0
- package/package.json +23 -18
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
|
@@ -48,10 +48,9 @@ async function loadNpmRolldown() {
|
|
|
48
48
|
// `@gjsify/rolldown-native`'s lib/ is not built yet, issue #497).
|
|
49
49
|
// Fail with the actionable diagnosis instead.
|
|
50
50
|
if (isGjs()) {
|
|
51
|
-
throw new Error('gjsify build: no usable bundler engine under GJS — `@gjsify/rolldown-native` is not loadable ' +
|
|
52
|
-
'
|
|
53
|
-
|
|
54
|
-
'Build the facade first (`gjsify workspace @gjsify/rolldown-native build` under Node) or run this build under Node.');
|
|
51
|
+
throw new Error('gjsify build: no usable bundler engine under GJS — `@gjsify/rolldown-native` is not loadable, ' +
|
|
52
|
+
'and the npm `rolldown` engine is a Rust napi crate that cannot run under GJS.\n' +
|
|
53
|
+
diagnoseNativeEngine());
|
|
55
54
|
}
|
|
56
55
|
// Indirect specifier so Rolldown's static-analysis doesn't try to
|
|
57
56
|
// bundle the npm crate into a GJS target build.
|
|
@@ -60,6 +59,58 @@ async function loadNpmRolldown() {
|
|
|
60
59
|
const mod = (await import(/* @vite-ignore */ target));
|
|
61
60
|
return mod.rolldown;
|
|
62
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
|
+
}
|
|
63
114
|
/**
|
|
64
115
|
* Convert a bare npm specifier into something dynamic `import(...)` can
|
|
65
116
|
* load regardless of host runtime:
|
|
@@ -287,9 +338,7 @@ async function tryLoadNative() {
|
|
|
287
338
|
return null;
|
|
288
339
|
}
|
|
289
340
|
})();
|
|
290
|
-
const resolved = resolvedFromNpm ??
|
|
291
|
-
resolvedFromFs ??
|
|
292
|
-
createRequire(import.meta.url).resolve(specifier);
|
|
341
|
+
const resolved = resolvedFromNpm ?? resolvedFromFs ?? createRequire(import.meta.url).resolve(specifier);
|
|
293
342
|
target = pathToFileURL(resolved).href;
|
|
294
343
|
}
|
|
295
344
|
const mod = (await import(/* @vite-ignore */ target));
|
package/lib/commands/affected.js
CHANGED
|
@@ -45,11 +45,55 @@
|
|
|
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
|
-
// for the spec suite.
|
|
82
|
+
// for the spec suite. Stdin is slurped via `utils/stdin.ts`, NOT
|
|
83
|
+
// `readFileSync(0)` — `@gjsify/fs` has no numeric-fd path, so the Node
|
|
84
|
+
// idiom opens the relative file `./0` and the committed GJS classifier
|
|
85
|
+
// bundle died with `ENOENT … read '0'` on every `--changed-from-stdin` run.
|
|
51
86
|
import { spawnSync } from 'node:child_process';
|
|
52
|
-
import
|
|
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';
|
|
96
|
+
import { readStdinLines } from '../utils/stdin.js';
|
|
53
97
|
import { discoverWorkspaces, buildDependencyGraph, buildReverseDependencyGraph, affectedClosure, workspacesForChangedFiles, } from '@gjsify/workspace';
|
|
54
98
|
import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
55
99
|
export const affectedCommand = {
|
|
@@ -85,7 +129,7 @@ export const affectedCommand = {
|
|
|
85
129
|
const changedFiles = args['changed-from-stdin']
|
|
86
130
|
? readStdinLines()
|
|
87
131
|
: runGitDiff(rootDir, args.base ?? 'origin/main', args.head ?? 'HEAD');
|
|
88
|
-
const result = classifyAndExpand(workspaces,
|
|
132
|
+
const result = classifyAndExpand(workspaces, changedFiles);
|
|
89
133
|
emit(args.format, result);
|
|
90
134
|
},
|
|
91
135
|
};
|
|
@@ -103,6 +147,11 @@ const GLOBAL_TRIGGERS = [
|
|
|
103
147
|
// can break assertions anywhere. Treat it as a global trigger so a
|
|
104
148
|
// `@gjsify/unit` change re-runs the full suite.
|
|
105
149
|
/^packages\/gjs\/unit\//,
|
|
150
|
+
// The composite action every CI job runs to set itself up. A change here
|
|
151
|
+
// alters the environment of the whole matrix, so it must force a full run
|
|
152
|
+
// EXPLICITLY — it already did, but only by falling through to the
|
|
153
|
+
// conservative "unmatched files" path, which is luck rather than intent.
|
|
154
|
+
/^\.github\/actions\//,
|
|
106
155
|
// Cross-cutting dep + lockfile + root config.
|
|
107
156
|
/^gjsify-lock\.json$/,
|
|
108
157
|
/^package\.json$/,
|
|
@@ -119,7 +168,7 @@ const IGNORE = [
|
|
|
119
168
|
/^refs\//,
|
|
120
169
|
/^website\//,
|
|
121
170
|
/^docs\//,
|
|
122
|
-
/^\.github\/workflows\/(deploy-docs|commitlint|release|audit-runtimes|prebuilds|node-gi)\.yml$/,
|
|
171
|
+
/^\.github\/workflows\/(deploy-docs|commitlint|release|audit-runtimes|prebuilds|node-gi|napi|cli-cross-platform|build-ci-image)\.yml$/,
|
|
123
172
|
/^\.githooks\//,
|
|
124
173
|
// @gjsify/node-gi (the Node-native GObject-Introspection engine, Axis 5) is
|
|
125
174
|
// NOT a gjsify workspace member — the GJS-first install/foreach tooling can't
|
|
@@ -128,6 +177,14 @@ const IGNORE = [
|
|
|
128
177
|
// tested on Node). Without this carve-out its files map to no workspace and
|
|
129
178
|
// land in `unmatched`, forcing a conservative full run on every node-gi PR.
|
|
130
179
|
/^packages\/node-gi\//,
|
|
180
|
+
// @gjsify/napi (the N-API host over GJS's SpiderMonkey, the forward mirror of
|
|
181
|
+
// node-gi) is likewise NOT a gjsify workspace member — the GJS-first
|
|
182
|
+
// install/foreach tooling can't build its Vala+C++/meson shim or its node-gyp
|
|
183
|
+
// test addons, so the main workflow neither builds nor tests it. Its own
|
|
184
|
+
// `.github/workflows/napi.yml` is the source of truth. Without this carve-out
|
|
185
|
+
// its files map to no workspace and force a conservative full run on every
|
|
186
|
+
// napi PR.
|
|
187
|
+
/^packages\/napi\//,
|
|
131
188
|
// Flatpak build/distribution tooling (SDK-extension manifest + metainfo).
|
|
132
189
|
// Like `.githooks/`, it has no package-test consumers; its own
|
|
133
190
|
// `tests/e2e/flatpak-sdk-extension` runs on `tests/e2e/**` / global triggers.
|
|
@@ -143,7 +200,7 @@ const IGNORE = [
|
|
|
143
200
|
];
|
|
144
201
|
/** Patterns that suggest a test-only change. */
|
|
145
202
|
const TEST_PATHS = [/\.spec\.[mc]?[tj]sx?$/, /^tests\/(e2e|integration)\//];
|
|
146
|
-
function classifyAndExpand(workspaces,
|
|
203
|
+
function classifyAndExpand(workspaces, changedFiles) {
|
|
147
204
|
const files = changedFiles.map((f) => f.replace(/\\/g, '/')).filter((f) => f.length > 0);
|
|
148
205
|
if (files.length === 0) {
|
|
149
206
|
return {
|
|
@@ -192,7 +249,7 @@ function classifyAndExpand(workspaces, rootDir, changedFiles) {
|
|
|
192
249
|
}
|
|
193
250
|
}
|
|
194
251
|
// Map files → workspaces. Files outside any workspace stay in `unmatched`.
|
|
195
|
-
const { matched, unmatched } = workspacesForChangedFiles(workspaces,
|
|
252
|
+
const { matched, unmatched } = workspacesForChangedFiles(workspaces, remaining);
|
|
196
253
|
// Unmatched-but-not-ignored files are suspicious enough to fall back to
|
|
197
254
|
// the conservative "full run" path. Examples: a new top-level dotfile,
|
|
198
255
|
// a script in `scripts/` we haven't carved out, a refs/-adjacent file.
|
|
@@ -290,10 +347,6 @@ function runGitDiff(cwd, base, head) {
|
|
|
290
347
|
}
|
|
291
348
|
return r.stdout.split('\n').filter(Boolean);
|
|
292
349
|
}
|
|
293
|
-
function readStdinLines() {
|
|
294
|
-
const data = readFileSync(0, 'utf8');
|
|
295
|
-
return data.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
296
|
-
}
|
|
297
350
|
// ─── Output ────────────────────────────────────────────────────────────────
|
|
298
351
|
function emit(format, r) {
|
|
299
352
|
if (format === 'json') {
|
|
@@ -306,9 +359,11 @@ function emit(format, r) {
|
|
|
306
359
|
return;
|
|
307
360
|
}
|
|
308
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.
|
|
309
364
|
const includeArgs = r.global
|
|
310
365
|
? ''
|
|
311
|
-
: r.workspaces.map((n) => `--include
|
|
366
|
+
: r.workspaces.map((n) => `--include ${assertShellSafeWorkspaceName(n)}`).join(' ');
|
|
312
367
|
const out = process.env.GITHUB_OUTPUT;
|
|
313
368
|
const lines = [
|
|
314
369
|
`skip-all=${r.skipAll}`,
|
|
@@ -320,7 +375,6 @@ function emit(format, r) {
|
|
|
320
375
|
];
|
|
321
376
|
if (out) {
|
|
322
377
|
// GitHub Actions: append to $GITHUB_OUTPUT.
|
|
323
|
-
const { appendFileSync } = require('node:fs');
|
|
324
378
|
for (const l of lines)
|
|
325
379
|
appendFileSync(out, `${l}\n`);
|
|
326
380
|
}
|
|
@@ -341,6 +395,22 @@ function emit(format, r) {
|
|
|
341
395
|
for (const name of r.workspaces)
|
|
342
396
|
process.stdout.write(` - ${name}\n`);
|
|
343
397
|
}
|
|
344
|
-
|
|
345
|
-
|
|
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}`);
|
|
346
416
|
}
|
package/lib/commands/check.js
CHANGED
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
// — if both `system-check`-style flags (`--json`) and `check`-style scripts
|
|
15
15
|
// are reachable here, the `--json` flag routes through this command first
|
|
16
16
|
// (since the typescript-check binding is positional `[paths..]`).
|
|
17
|
-
import {
|
|
17
|
+
import { spawnSync } from 'node:child_process';
|
|
18
18
|
import { prefixLines } from '../utils/prefixed-output.js';
|
|
19
|
+
import { spawnToCompletion } from '../utils/spawn.js';
|
|
19
20
|
import { readPackageJson } from '../utils/pkg-json.js';
|
|
20
21
|
import { join } from 'node:path';
|
|
21
22
|
import { cpus } from 'node:os';
|
|
@@ -26,36 +27,44 @@ import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
|
26
27
|
* indicates failure. Output is forwarded to the parent's stdout/stderr,
|
|
27
28
|
* line-prefixed with the workspace name when `prefix` is set.
|
|
28
29
|
*/
|
|
29
|
-
function runCheck(ws, prefix) {
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
async function runCheck(ws, prefix) {
|
|
31
|
+
// `completion: 'exit'` — every path out of this command's handler ends in
|
|
32
|
+
// `process.exit(…)`, which quits the GLib main loop `spawn()` arms under
|
|
33
|
+
// GJS, so the STREAMING async path is safe here (and required: the
|
|
34
|
+
// blocking one would serialize the parallel workers). See utils/spawn.ts.
|
|
35
|
+
const flushers = [];
|
|
36
|
+
try {
|
|
37
|
+
const { code } = await spawnToCompletion('npm', ['run', 'check', '--if-present'], {
|
|
38
|
+
completion: 'exit',
|
|
32
39
|
cwd: ws.location,
|
|
33
|
-
stdio: prefix === null ? 'inherit' :
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
for (const flush of flushers)
|
|
50
|
-
flush();
|
|
51
|
-
resolve(code ?? 1);
|
|
52
|
-
});
|
|
53
|
-
child.on('error', () => {
|
|
54
|
-
for (const flush of flushers)
|
|
55
|
-
flush();
|
|
56
|
-
resolve(1);
|
|
40
|
+
stdio: prefix === null ? 'inherit' : 'pipe',
|
|
41
|
+
onSpawn: (child) => {
|
|
42
|
+
// Shared with `foreach`: live line-prefixing on a tty, buffered
|
|
43
|
+
// flush-on-close on a non-tty sink. The previous inline
|
|
44
|
+
// forwarder wrote every line live — under GJS
|
|
45
|
+
// `process.stdout.write` is a blocking `Gio.write_all`, so a
|
|
46
|
+
// parallel check into a backpressuring CI pipe could stall the
|
|
47
|
+
// GLib loop the same way the foreach examples build did. See
|
|
48
|
+
// utils/prefixed-output.ts.
|
|
49
|
+
const buffered = !process.stdout.isTTY;
|
|
50
|
+
if (prefix !== null && child.stdout && child.stderr) {
|
|
51
|
+
const tag = `[${prefix}] `;
|
|
52
|
+
flushers.push(prefixLines(child.stdout, process.stdout, tag, buffered));
|
|
53
|
+
flushers.push(prefixLines(child.stderr, process.stderr, tag, buffered));
|
|
54
|
+
}
|
|
55
|
+
},
|
|
57
56
|
});
|
|
58
|
-
|
|
57
|
+
for (const flush of flushers)
|
|
58
|
+
flush();
|
|
59
|
+
return code ?? 1;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Spawn failure (npm not on PATH, …) reports as a failed check —
|
|
63
|
+
// `gjsify check` aggregates codes rather than throwing per workspace.
|
|
64
|
+
for (const flush of flushers)
|
|
65
|
+
flush();
|
|
66
|
+
return 1;
|
|
67
|
+
}
|
|
59
68
|
}
|
|
60
69
|
export const checkCommand = {
|
|
61
70
|
command: 'check',
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
// Replaces the project-local `build-flatpak.sh`-style scripts: same flag
|
|
4
4
|
// shape (manifest, build-dir, install, repo, bundle), plus a `--tarball`
|
|
5
5
|
// helper for Flathub submissions.
|
|
6
|
-
import { spawn } from 'node:child_process';
|
|
7
6
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'node:fs';
|
|
8
7
|
import { dirname, resolve } from 'node:path';
|
|
8
|
+
import { spawnToCompletion } from '../../utils/spawn.js';
|
|
9
9
|
export const flatpakBuildCommand = {
|
|
10
10
|
command: 'build [manifest]',
|
|
11
11
|
description: 'Build the Flatpak via `flatpak-builder`. Wraps a typical install + export + bundle + tarball pipeline.',
|
|
@@ -115,6 +115,12 @@ export const flatpakBuildCommand = {
|
|
|
115
115
|
await runTar(['-czf', tarballPath, '-C', buildDir, '.'], { verbose: args.verbose });
|
|
116
116
|
}
|
|
117
117
|
console.log(`[gjsify flatpak build] done (${buildDir})`);
|
|
118
|
+
// Terminal command: exit explicitly. Under GJS the async `spawn` used
|
|
119
|
+
// above arms a GLib main loop (`ensureMainLoop`) that only
|
|
120
|
+
// `process.exit()` quits — returning here would park the CLI at 0% CPU
|
|
121
|
+
// after flatpak-builder had already finished. Same reason `foreach` /
|
|
122
|
+
// `workspace` / `check` end this way; see utils/spawn.ts.
|
|
123
|
+
return process.exit(0);
|
|
118
124
|
},
|
|
119
125
|
};
|
|
120
126
|
/** Pick the first JSON file in cwd that looks like a Flatpak manifest. */
|
|
@@ -167,25 +173,20 @@ async function runFlatpak(args, opts) {
|
|
|
167
173
|
async function runTar(args, opts) {
|
|
168
174
|
return runProc('tar', args, opts, { notFoundHint: 'tar not found.' });
|
|
169
175
|
}
|
|
170
|
-
function runProc(cmd, args, opts, extra) {
|
|
176
|
+
async function runProc(cmd, args, opts, extra) {
|
|
171
177
|
if (opts.verbose) {
|
|
172
178
|
console.log(`[gjsify flatpak] ${cmd} ${args.join(' ')}`);
|
|
173
179
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
child.on('exit', (code) => {
|
|
185
|
-
if (code === 0)
|
|
186
|
-
res();
|
|
187
|
-
else
|
|
188
|
-
rej(new Error(`gjsify flatpak: ${cmd} exited with status ${code}`));
|
|
189
|
-
});
|
|
180
|
+
// `completion: 'exit'` — the handler ends in `process.exit(0)` (added for
|
|
181
|
+
// exactly this reason), so the GLib main loop `spawn()` arms under GJS is
|
|
182
|
+
// quit on the way out and the STREAMING path stays available. That matters
|
|
183
|
+
// here more than anywhere: `flatpak-builder` runs for minutes and its live
|
|
184
|
+
// progress is the whole point of `stdio: 'inherit'`. See utils/spawn.ts.
|
|
185
|
+
const { code } = await spawnToCompletion(cmd, args, {
|
|
186
|
+
completion: 'exit',
|
|
187
|
+
notFound: () => new Error(`gjsify flatpak: ${extra.notFoundHint}`),
|
|
190
188
|
});
|
|
189
|
+
if (code !== 0) {
|
|
190
|
+
throw new Error(`gjsify flatpak: ${cmd} exited with status ${code}`);
|
|
191
|
+
}
|
|
191
192
|
}
|