@gjsify/cli 0.49.0 → 0.50.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 +97 -97
- package/lib/actions/barrels-generate.d.ts +22 -0
- package/lib/actions/barrels-generate.js +36 -1
- package/lib/actions/build.js +8 -5
- package/lib/commands/barrels.js +19 -2
- package/lib/commands/foreach.js +11 -19
- package/lib/commands/test.d.ts +16 -0
- package/lib/commands/test.js +30 -29
- package/lib/commands/workspace.js +47 -44
- package/lib/index.js +10 -0
- package/lib/utils/build-cache.d.ts +15 -4
- package/lib/utils/build-cache.js +62 -67
- package/lib/utils/cli-exit.d.ts +23 -0
- package/lib/utils/cli-exit.js +78 -0
- package/lib/utils/package-inputs.d.ts +81 -0
- package/lib/utils/package-inputs.js +233 -0
- package/lib/utils/ship/changelog.d.ts +15 -14
- package/lib/utils/ship/changelog.js +14 -13
- package/lib/utils/ship/formats.js +23 -2
- package/lib/utils/ship/gzip.d.ts +31 -2
- package/lib/utils/ship/gzip.js +55 -3
- package/lib/utils/ship/plan.d.ts +32 -4
- package/lib/utils/ship/plan.js +61 -10
- package/lib/utils/simple-command.d.ts +17 -2
- package/lib/utils/simple-command.js +9 -4
- package/package.json +19 -19
|
@@ -21,9 +21,31 @@ export interface BarrelsArgs {
|
|
|
21
21
|
}
|
|
22
22
|
export declare const DEFAULT_BARRELS_HEADER = "// Auto-generated by `gjsify barrels` \u2014 do not edit by hand.";
|
|
23
23
|
export declare const DEFAULT_BARRELS_EXCLUDES: readonly string[];
|
|
24
|
+
/**
|
|
25
|
+
* The entries of `paths` that cannot be scanned as a directory, resolved against `baseDir`
|
|
26
|
+
* — each already phrased for printing.
|
|
27
|
+
*
|
|
28
|
+
* A SEPARATE pass rather than a behaviour change inside {@link generateBarrels}, because
|
|
29
|
+
* the two callers want opposite things. A programmatic caller may legitimately hand the
|
|
30
|
+
* generator a directory that is not there yet and still want the others written, which is
|
|
31
|
+
* why it skips; a COMMAND-LINE caller named these directories, so one that cannot be read
|
|
32
|
+
* is a typo or a rename and nothing else. So the command asserts first — the shape
|
|
33
|
+
* `assertEveryIncludeMatches` already has for `gjsify foreach --include`.
|
|
34
|
+
*
|
|
35
|
+
* THE INCIDENT: `gjsify barrels --check` counted a directory it could not read as 0 drift
|
|
36
|
+
* and exited 0, so the guard reported a clean barrel for a directory nothing had looked
|
|
37
|
+
* at, and the only trace was a line printed under `--verbose`. "Skipped" and "in sync"
|
|
38
|
+
* have to be told apart by the exit code — a guard that goes green for work it never did
|
|
39
|
+
* is worse than no guard, because a later step is measuring its output.
|
|
40
|
+
*/
|
|
41
|
+
export declare function unscannableBarrelPaths(baseDir: string, paths: readonly string[]): Promise<string[]>;
|
|
24
42
|
/**
|
|
25
43
|
* Regenerate `index.ts` in every directory in `args.paths`.
|
|
26
44
|
*
|
|
45
|
+
* A path that cannot be read is SKIPPED here, deliberately — see
|
|
46
|
+
* {@link unscannableBarrelPaths} for who refuses it instead, and why that is the command
|
|
47
|
+
* and not this.
|
|
48
|
+
*
|
|
27
49
|
* Returns the number of files that drifted from the canonical output —
|
|
28
50
|
* always 0 when `check` is false (drift is rewritten in-place); non-zero
|
|
29
51
|
* under `check: true` signals CI failure.
|
|
@@ -12,14 +12,49 @@
|
|
|
12
12
|
//
|
|
13
13
|
// Original: Copyright (c) Beabee Community Repo contributors. AGPL-3.0.
|
|
14
14
|
// Reimplemented for gjsify under the project's MIT license.
|
|
15
|
-
import { readdir, readFile, writeFile } from 'node:fs/promises';
|
|
15
|
+
import { readdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
16
16
|
import { basename, extname, join, resolve } from 'node:path';
|
|
17
17
|
export const DEFAULT_BARRELS_HEADER = '// Auto-generated by `gjsify barrels` — do not edit by hand.';
|
|
18
18
|
export const DEFAULT_BARRELS_EXCLUDES = ['\\.test\\.', '\\.spec\\.', '\\.test-data\\.'];
|
|
19
19
|
const SOURCE_FILE_RE = /\.(ts|tsx|mts|cts)$/;
|
|
20
|
+
/**
|
|
21
|
+
* The entries of `paths` that cannot be scanned as a directory, resolved against `baseDir`
|
|
22
|
+
* — each already phrased for printing.
|
|
23
|
+
*
|
|
24
|
+
* A SEPARATE pass rather than a behaviour change inside {@link generateBarrels}, because
|
|
25
|
+
* the two callers want opposite things. A programmatic caller may legitimately hand the
|
|
26
|
+
* generator a directory that is not there yet and still want the others written, which is
|
|
27
|
+
* why it skips; a COMMAND-LINE caller named these directories, so one that cannot be read
|
|
28
|
+
* is a typo or a rename and nothing else. So the command asserts first — the shape
|
|
29
|
+
* `assertEveryIncludeMatches` already has for `gjsify foreach --include`.
|
|
30
|
+
*
|
|
31
|
+
* THE INCIDENT: `gjsify barrels --check` counted a directory it could not read as 0 drift
|
|
32
|
+
* and exited 0, so the guard reported a clean barrel for a directory nothing had looked
|
|
33
|
+
* at, and the only trace was a line printed under `--verbose`. "Skipped" and "in sync"
|
|
34
|
+
* have to be told apart by the exit code — a guard that goes green for work it never did
|
|
35
|
+
* is worse than no guard, because a later step is measuring its output.
|
|
36
|
+
*/
|
|
37
|
+
export async function unscannableBarrelPaths(baseDir, paths) {
|
|
38
|
+
const problems = [];
|
|
39
|
+
for (const p of paths) {
|
|
40
|
+
const dir = resolve(baseDir, p);
|
|
41
|
+
try {
|
|
42
|
+
if (!(await stat(dir)).isDirectory())
|
|
43
|
+
problems.push(`${dir} is not a directory`);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
problems.push(`${dir}: ${err.message}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return problems;
|
|
50
|
+
}
|
|
20
51
|
/**
|
|
21
52
|
* Regenerate `index.ts` in every directory in `args.paths`.
|
|
22
53
|
*
|
|
54
|
+
* A path that cannot be read is SKIPPED here, deliberately — see
|
|
55
|
+
* {@link unscannableBarrelPaths} for who refuses it instead, and why that is the command
|
|
56
|
+
* and not this.
|
|
57
|
+
*
|
|
23
58
|
* Returns the number of files that drifted from the canonical output —
|
|
24
59
|
* always 0 when `check` is false (drift is rewritten in-place); non-zero
|
|
25
60
|
* under `check: true` signals CI failure.
|
package/lib/actions/build.js
CHANGED
|
@@ -15,6 +15,7 @@ import { collectEntryPaths, describeJsxConfig, findJsxEntryPoint, jsxConfigMissi
|
|
|
15
15
|
import { giSystemProbes } from '../utils/gi-runtime-paths.js';
|
|
16
16
|
import { escapeRawNulForGjs } from '../utils/gjs-source-escape.js';
|
|
17
17
|
import { assertNodeBundleGlobalsShimmed } from '../utils/node-bundle-guard.js';
|
|
18
|
+
import { DEP_CHANGE_LOCKFILES } from '../utils/package-inputs.js';
|
|
18
19
|
const DEFAULT_GJS_SHEBANG = '#!/usr/bin/env -S gjs -m';
|
|
19
20
|
/**
|
|
20
21
|
* `true` when `path` points at a location that's unsafe to use as a build
|
|
@@ -59,16 +60,18 @@ function isTruthyEnv(v) {
|
|
|
59
60
|
return v === '1' || v === 'true' || v === 'yes';
|
|
60
61
|
}
|
|
61
62
|
/**
|
|
62
|
-
*
|
|
63
|
+
* Newest mtime (ms) among known lockfiles in `cwd`, or 0 when none exist.
|
|
64
|
+
*
|
|
63
65
|
* A dep bump (`npm/yarn/pnpm/gjsify install`) rewrites one of these but leaves
|
|
64
66
|
* the plugin's own entry-file mtime untouched — so the plugin GJS-bundle cache
|
|
65
|
-
* must invalidate on
|
|
67
|
+
* must invalidate on them too, not only on the entry. The LIST is
|
|
68
|
+
* {@link DEP_CHANGE_LOCKFILES}, shared with the package-input definition: two
|
|
69
|
+
* answers to "has the dependency tree moved" is how they come to disagree
|
|
70
|
+
* about a fifth package manager.
|
|
66
71
|
*/
|
|
67
|
-
const PLUGIN_CACHE_DEP_SIGNALS = ['gjsify-lock.json', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml'];
|
|
68
|
-
/** Newest mtime (ms) among known lockfiles in `cwd`, or 0 when none exist. */
|
|
69
72
|
async function newestLockfileMtime(cwd) {
|
|
70
73
|
let newest = 0;
|
|
71
|
-
for (const name of
|
|
74
|
+
for (const name of DEP_CHANGE_LOCKFILES) {
|
|
72
75
|
try {
|
|
73
76
|
const s = await stat(join(cwd, name));
|
|
74
77
|
if (s.mtimeMs > newest)
|
package/lib/commands/barrels.js
CHANGED
|
@@ -13,7 +13,10 @@
|
|
|
13
13
|
// file as a module.
|
|
14
14
|
// - Output is sorted by file name (locale-compare) — deterministic diffs.
|
|
15
15
|
// - `--check` exits non-zero on drift without writing (pre-commit / CI use).
|
|
16
|
-
|
|
16
|
+
// - A named path that is not a readable directory is REFUSED before anything
|
|
17
|
+
// is generated: skipping it would let `--check` report 0 drift, and exit 0,
|
|
18
|
+
// for a barrel nothing ever looked at.
|
|
19
|
+
import { DEFAULT_BARRELS_EXCLUDES, DEFAULT_BARRELS_HEADER, generateBarrels, unscannableBarrelPaths, } from '../actions/barrels-generate.js';
|
|
17
20
|
export const barrelsCommand = {
|
|
18
21
|
command: 'barrels [paths..]',
|
|
19
22
|
description: 'Regenerate `index.ts` barrel files for the given directories. Drop-in replacement for `barrelsby`.',
|
|
@@ -81,13 +84,27 @@ export const barrelsCommand = {
|
|
|
81
84
|
process.exitCode = 1;
|
|
82
85
|
return;
|
|
83
86
|
}
|
|
87
|
+
const baseDir = args.baseDir ?? process.cwd();
|
|
88
|
+
// A named path this cannot scan is a typo or a rename, and it is refused BEFORE
|
|
89
|
+
// anything is generated. The generator skips such a path and reports no drift for
|
|
90
|
+
// it, which under `--check` is a guard going green for a directory nothing looked
|
|
91
|
+
// at — the one failure mode a guard must not have.
|
|
92
|
+
const unscannable = await unscannableBarrelPaths(baseDir, paths);
|
|
93
|
+
if (unscannable.length > 0) {
|
|
94
|
+
for (const problem of unscannable)
|
|
95
|
+
console.error(`[gjsify barrels] cannot scan ${problem}`);
|
|
96
|
+
console.error(`[gjsify barrels] ${unscannable.length} of ${paths.length} path(s) could not be scanned — ` +
|
|
97
|
+
'refusing to report on the rest as if that were the whole set.');
|
|
98
|
+
process.exitCode = 1;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
84
101
|
const excludePatterns = args.exclude?.length
|
|
85
102
|
? args.exclude
|
|
86
103
|
: [...DEFAULT_BARRELS_EXCLUDES];
|
|
87
104
|
const drift = await generateBarrels({
|
|
88
105
|
paths,
|
|
89
106
|
extension: args.ext ?? 'none',
|
|
90
|
-
baseDir
|
|
107
|
+
baseDir,
|
|
91
108
|
exclude: excludePatterns.map((src) => new RegExp(src)),
|
|
92
109
|
header: args.header ?? DEFAULT_BARRELS_HEADER,
|
|
93
110
|
noSemicolon: args.semicolon === false || args.semicolon === undefined,
|
package/lib/commands/foreach.js
CHANGED
|
@@ -583,25 +583,17 @@ async function runParallel(workspaces, script, args, concurrency, verbose, exec,
|
|
|
583
583
|
await Promise.all(workers);
|
|
584
584
|
}
|
|
585
585
|
async function runTopologicalParallel(workspaces, script, args, concurrency, verbose, includeDev, exec, cache) {
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
if (!spec.startsWith('workspace:'))
|
|
598
|
-
continue;
|
|
599
|
-
if (selectedNames.has(name))
|
|
600
|
-
wsDeps.add(name);
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
remaining.set(ws.name, wsDeps);
|
|
604
|
-
}
|
|
586
|
+
// The SHARED graph, walked once — never a private copy of the walk. `buildDependencyGraph`
|
|
587
|
+
// over the SELECTED set yields exactly these edges: it indexes by name from the list it is
|
|
588
|
+
// handed, so a dependency outside the selection resolves to no member and is not an edge.
|
|
589
|
+
//
|
|
590
|
+
// The copy this replaces followed `workspace:` only. Once #1587 widened the shared rule to
|
|
591
|
+
// include a plain range the local member satisfies, the two topological legs would have
|
|
592
|
+
// DISAGREED — `-t` ordering correctly off the shared graph while `-t -p` saw no edges at
|
|
593
|
+
// all and started every workspace at once, in a monorepo that declares its local deps by
|
|
594
|
+
// plain semver range. Wrong order, exit 0, and a build measured by a later step: the same
|
|
595
|
+
// shape #1587 is about, one flag over.
|
|
596
|
+
const remaining = new Map([...buildDependencyGraph(workspaces, { includeDev }).edges].map(([name, deps]) => [name, new Set(deps)]));
|
|
605
597
|
const byName = new Map(workspaces.map((w) => [w.name, w]));
|
|
606
598
|
const total = workspaces.length;
|
|
607
599
|
const done = new Set();
|
package/lib/commands/test.d.ts
CHANGED
|
@@ -9,4 +9,20 @@ interface TestOptions {
|
|
|
9
9
|
verbose?: boolean;
|
|
10
10
|
}
|
|
11
11
|
export declare const testCommand: Command<unknown, TestOptions>;
|
|
12
|
+
/**
|
|
13
|
+
* True when `outfile` is at least as new as every build input of the package
|
|
14
|
+
* at `cwd`.
|
|
15
|
+
*
|
|
16
|
+
* The set is {@link packageBuildInputs}, the same one the build cache hashes.
|
|
17
|
+
* It used to be `dirname(entry)` — `tests/` for any package whose suite lives
|
|
18
|
+
* there, so `src/**` was never walked: a change to the code UNDER TEST left
|
|
19
|
+
* the bundle "fresh" and `gjsify test` reran the previous build and reported
|
|
20
|
+
* on it. That went both ways inside one dependency bump — a green run
|
|
21
|
+
* measuring a version that was no longer installed, then a red one after the
|
|
22
|
+
* source had already been repaired (#1651).
|
|
23
|
+
*
|
|
24
|
+
* `outdir` is subtracted as this command's own output; `entry` is added
|
|
25
|
+
* because `--entry ../shared/test.mts` may point outside the package.
|
|
26
|
+
*/
|
|
27
|
+
export declare function isFresh(outfile: string, entry: string, cwd: string, outdir: string): boolean;
|
|
12
28
|
export {};
|
package/lib/commands/test.js
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
// just needs `src/test.mts` aggregating its `@gjsify/unit` suites; this
|
|
6
6
|
// command builds it for GJS + Node and runs each output, aggregating
|
|
7
7
|
// exit codes.
|
|
8
|
-
import { existsSync, statSync
|
|
9
|
-
import { join,
|
|
8
|
+
import { existsSync, statSync } from 'node:fs';
|
|
9
|
+
import { join, resolve, relative, sep } from 'node:path';
|
|
10
|
+
import { newestDepSignalMtimeMs, newestInputMtimeMs, packageBuildInputs } from '../utils/package-inputs.js';
|
|
10
11
|
import { isRuntimeAvailable, RUNTIMES } from '../utils/runtimes.js';
|
|
11
12
|
import { nodeBinary } from '../utils/run-node.js';
|
|
12
13
|
import { describeExit, spawnToCompletion } from '../utils/spawn.js';
|
|
@@ -99,7 +100,7 @@ export const testCommand = {
|
|
|
99
100
|
const outfile = join(outdir, `test.${runtime}.mjs`);
|
|
100
101
|
// Build stage (skip if --no-build OR (not --rebuild AND outfile fresher than src)).
|
|
101
102
|
if (args.build !== false) {
|
|
102
|
-
const needsBuild = args.rebuild || !isFresh(outfile, entry);
|
|
103
|
+
const needsBuild = args.rebuild || !isFresh(outfile, entry, cwd, outdir);
|
|
103
104
|
if (needsBuild) {
|
|
104
105
|
const buildStart = Date.now();
|
|
105
106
|
if (args.verbose) {
|
|
@@ -204,16 +205,35 @@ async function runTestBundle(outfile, runtime) {
|
|
|
204
205
|
if (result.code !== 0)
|
|
205
206
|
throw new Error(`node exited with ${describeExit(result)}`);
|
|
206
207
|
}
|
|
207
|
-
/**
|
|
208
|
-
|
|
208
|
+
/**
|
|
209
|
+
* True when `outfile` is at least as new as every build input of the package
|
|
210
|
+
* at `cwd`.
|
|
211
|
+
*
|
|
212
|
+
* The set is {@link packageBuildInputs}, the same one the build cache hashes.
|
|
213
|
+
* It used to be `dirname(entry)` — `tests/` for any package whose suite lives
|
|
214
|
+
* there, so `src/**` was never walked: a change to the code UNDER TEST left
|
|
215
|
+
* the bundle "fresh" and `gjsify test` reran the previous build and reported
|
|
216
|
+
* on it. That went both ways inside one dependency bump — a green run
|
|
217
|
+
* measuring a version that was no longer installed, then a red one after the
|
|
218
|
+
* source had already been repaired (#1651).
|
|
219
|
+
*
|
|
220
|
+
* `outdir` is subtracted as this command's own output; `entry` is added
|
|
221
|
+
* because `--entry ../shared/test.mts` may point outside the package.
|
|
222
|
+
*/
|
|
223
|
+
export function isFresh(outfile, entry, cwd, outdir) {
|
|
209
224
|
if (!existsSync(outfile))
|
|
210
225
|
return false;
|
|
211
|
-
const outMtime = statSync(outfile).mtimeMs;
|
|
212
|
-
const srcRoot = dirname(entry);
|
|
213
|
-
// Conservative: walk the src tree once. If the package has no `src/`,
|
|
214
|
-
// fall back to entry-only check.
|
|
215
226
|
try {
|
|
216
|
-
const
|
|
227
|
+
const outMtime = statSync(outfile).mtimeMs;
|
|
228
|
+
const extraOutputs = [];
|
|
229
|
+
const outdirRel = relative(cwd, outdir).split(sep).join('/');
|
|
230
|
+
if (outdirRel !== '' && !outdirRel.startsWith('../'))
|
|
231
|
+
extraOutputs.push(outdirRel);
|
|
232
|
+
const newest = Math.max(newestInputMtimeMs(packageBuildInputs(cwd, { extraOutputs })), statSync(entry).mtimeMs,
|
|
233
|
+
// A dependency bump changes no file in the package — and that is
|
|
234
|
+
// the shape #1651 was found in. `node_modules` is far too large
|
|
235
|
+
// to stat per run, so the lockfile stands in for it.
|
|
236
|
+
newestDepSignalMtimeMs(cwd));
|
|
217
237
|
return outMtime >= newest;
|
|
218
238
|
}
|
|
219
239
|
catch {
|
|
@@ -221,22 +241,3 @@ function isFresh(outfile, entry) {
|
|
|
221
241
|
return false;
|
|
222
242
|
}
|
|
223
243
|
}
|
|
224
|
-
function newestMtimeUnder(path) {
|
|
225
|
-
const st = statSync(path);
|
|
226
|
-
if (st.isFile())
|
|
227
|
-
return st.mtimeMs;
|
|
228
|
-
let max = st.mtimeMs;
|
|
229
|
-
for (const entry of readdirSync(path, { withFileTypes: true })) {
|
|
230
|
-
if (entry.name === 'node_modules' ||
|
|
231
|
-
entry.name === 'dist' ||
|
|
232
|
-
entry.name === 'lib' ||
|
|
233
|
-
entry.name.startsWith('.')) {
|
|
234
|
-
continue;
|
|
235
|
-
}
|
|
236
|
-
const child = join(path, entry.name);
|
|
237
|
-
const m = newestMtimeUnder(child);
|
|
238
|
-
if (m > max)
|
|
239
|
-
max = m;
|
|
240
|
-
}
|
|
241
|
-
return max;
|
|
242
|
-
}
|
|
@@ -14,13 +14,16 @@
|
|
|
14
14
|
// dependency, in topological build order, then in the target package.
|
|
15
15
|
// Skips deps that don't declare the script (`--if-present` behaviour).
|
|
16
16
|
// Stops on the first failure unless `--continue-on-error` is passed.
|
|
17
|
+
// A local dependency counts whether it is declared with the `workspace:`
|
|
18
|
+
// protocol or with a plain range the local member satisfies (@gjsify/workspace
|
|
19
|
+
// § buildDependencyGraph) — and the expansion SAYS when it found nothing.
|
|
17
20
|
//
|
|
18
21
|
// This replaces the manual `gjsify workspace <A> build && gjsify workspace
|
|
19
22
|
// <B> build && …` chains in root `build:infra` scripts: cascade build
|
|
20
23
|
// failures on uninbuilt workspace deps are the single biggest local-dev
|
|
21
24
|
// pain point in this monorepo.
|
|
22
25
|
import { spawnToCompletion } from '../utils/spawn.js';
|
|
23
|
-
import { buildDependencyGraph, discoverWorkspaces, topologicalSort } from '@gjsify/workspace';
|
|
26
|
+
import { affectedClosure, buildDependencyGraph, discoverWorkspaces, topologicalSort, } from '@gjsify/workspace';
|
|
24
27
|
import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
25
28
|
import { BuildCacheRunner, buildCacheEnabledByEnv } from '../utils/build-cache.js';
|
|
26
29
|
import { isGjs } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
@@ -135,11 +138,29 @@ export const workspaceCommand = {
|
|
|
135
138
|
// target itself.
|
|
136
139
|
let runList;
|
|
137
140
|
if (withDeps) {
|
|
138
|
-
const
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
|
|
141
|
+
const includeDev = args['include-dev'] === true;
|
|
142
|
+
// The SHARED graph, walked by the shared closure — the same two calls
|
|
143
|
+
// `gjsify foreach --with-dependencies` makes. This used to be a private
|
|
144
|
+
// copy of the walk here, and the copy is where the defect lived: it
|
|
145
|
+
// followed `workspace:` edges only, so in a monorepo declaring its local
|
|
146
|
+
// deps by plain semver range it matched nothing, built nothing and exited
|
|
147
|
+
// 0 (#1587). One walk means the rule for "what is a local dependency" has
|
|
148
|
+
// one place to be wrong in.
|
|
149
|
+
const forward = buildDependencyGraph(allWorkspaces, { includeDev });
|
|
150
|
+
const names = affectedClosure(forward, [target.name]);
|
|
151
|
+
const closure = allWorkspaces.filter((ws) => names.has(ws.name));
|
|
152
|
+
reportUnlinked(forward.unlinked, names);
|
|
153
|
+
const graph = buildDependencyGraph(closure, { includeDev });
|
|
142
154
|
runList = topologicalSort(graph);
|
|
155
|
+
if (runList.length === 1) {
|
|
156
|
+
// The expansion found nothing, and it SAYS so. A leaf package
|
|
157
|
+
// legitimately has no local dependencies, but "no local dependencies"
|
|
158
|
+
// and "the flag could not see them" produced the same silent exit 0,
|
|
159
|
+
// and that ambiguity is what cost a Flatpak build, a container run and
|
|
160
|
+
// a manual `ls` to resolve.
|
|
161
|
+
console.error(`gjsify workspace: --with-dependencies found no workspace dependencies of ` +
|
|
162
|
+
`"${target.name}" — running "${script}" in it alone.`);
|
|
163
|
+
}
|
|
143
164
|
}
|
|
144
165
|
else {
|
|
145
166
|
runList = [target];
|
|
@@ -197,48 +218,30 @@ export const workspaceCommand = {
|
|
|
197
218
|
},
|
|
198
219
|
};
|
|
199
220
|
/**
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
221
|
+
* Name a dependency that points at a local workspace but does not link to it, because
|
|
222
|
+
* the member's version does not satisfy the declared range.
|
|
223
|
+
*
|
|
224
|
+
* Only entries declared BY a package in the closure are printed: an unlinked pair
|
|
225
|
+
* elsewhere in the monorepo is somebody else's business, and printing all of them would
|
|
226
|
+
* bury the one that explains why this closure is the size it is.
|
|
227
|
+
*
|
|
228
|
+
* This is a warning, not a failure, and it deliberately asserts NO outcome, because the
|
|
229
|
+
* outcome depends on which installer placed `node_modules`: npm and yarn fetch such a
|
|
230
|
+
* dependency from the registry — the rule the graph follows, under which leaving it out of
|
|
231
|
+
* the build order is simply right — while `gjsify install` symlinks the local member
|
|
232
|
+
* whatever the range says. Under that one the build order really is short a package, so
|
|
233
|
+
* the message asks for the range and the version to be brought into agreement instead of
|
|
234
|
+
* telling the reader where the files came from.
|
|
204
235
|
*/
|
|
205
|
-
function
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
byName.set(ws.name, ws);
|
|
209
|
-
const seen = new Set();
|
|
210
|
-
const out = [];
|
|
211
|
-
const stack = [target];
|
|
212
|
-
while (stack.length > 0) {
|
|
213
|
-
const ws = stack.pop();
|
|
214
|
-
if (seen.has(ws.name))
|
|
236
|
+
function reportUnlinked(unlinked, closure) {
|
|
237
|
+
for (const entry of unlinked) {
|
|
238
|
+
if (!closure.has(entry.from))
|
|
215
239
|
continue;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
m.dependencies,
|
|
221
|
-
includeDev ? m.devDependencies : undefined,
|
|
222
|
-
m.optionalDependencies,
|
|
223
|
-
// peerDependencies excluded by default (yarn does the same)
|
|
224
|
-
];
|
|
225
|
-
for (const block of blocks) {
|
|
226
|
-
if (!block)
|
|
227
|
-
continue;
|
|
228
|
-
for (const [depName, spec] of Object.entries(block)) {
|
|
229
|
-
if (typeof spec !== 'string')
|
|
230
|
-
continue;
|
|
231
|
-
if (!spec.startsWith('workspace:'))
|
|
232
|
-
continue;
|
|
233
|
-
const dep = byName.get(depName);
|
|
234
|
-
if (!dep)
|
|
235
|
-
continue;
|
|
236
|
-
if (!seen.has(dep.name))
|
|
237
|
-
stack.push(dep);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
+
console.error(`gjsify workspace: "${entry.from}" depends on "${entry.to}": ${entry.spec}, which the local ` +
|
|
241
|
+
`workspace (${entry.to}@${entry.version}) does not satisfy — NOT built as a dependency. ` +
|
|
242
|
+
`npm and yarn take that one from the registry; \`gjsify install\` links the local one ` +
|
|
243
|
+
`anyway, so if it is the local one you mean, make the range admit ${entry.version}.`);
|
|
240
244
|
}
|
|
241
|
-
return out;
|
|
242
245
|
}
|
|
243
246
|
async function runOne(ws, script, extraArgs, runner, verbose, cache) {
|
|
244
247
|
// Build-cache fast path: on a key hit restore the stored outputs and
|
package/lib/index.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// This file is just the executable wrapper around `runCli`.
|
|
7
7
|
import { hideBin } from 'yargs/helpers';
|
|
8
8
|
import { runCli } from './cli-app.js';
|
|
9
|
+
import { exitOnReportedFailure } from './utils/cli-exit.js';
|
|
9
10
|
import { gjsExit } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
10
11
|
try {
|
|
11
12
|
await runCli(hideBin(process.argv));
|
|
@@ -43,3 +44,12 @@ catch (err) {
|
|
|
43
44
|
process.exitCode = 1;
|
|
44
45
|
gjsExit(1);
|
|
45
46
|
}
|
|
47
|
+
// The catch above is only HALF the entry's exit funnel, and the other half was
|
|
48
|
+
// missing: a handler that reports a failure by assigning `process.exitCode` and
|
|
49
|
+
// returning normally never reaches it. That is the Node idiom and the CLI uses
|
|
50
|
+
// it wherever the failure is already printed in full (`gjsify gresource`,
|
|
51
|
+
// `gjsify gsettings`, `gjsify barrels --check`, `setOxcExitCode`) — and under
|
|
52
|
+
// GJS, which has no atexit hook, nothing read it back, so the process ended 0
|
|
53
|
+
// with the error on stderr. `cli-exit.ts` carries the measurement and why `&&`
|
|
54
|
+
// did not protect against it (JumpLink/Learn6502#180).
|
|
55
|
+
exitOnReportedFailure();
|
|
@@ -11,9 +11,19 @@ import { type Workspace } from '@gjsify/workspace';
|
|
|
11
11
|
* still on v2's whole-dir ownership and could restore over a sibling unit; a v2
|
|
12
12
|
* manifest for them records `lib`, so it must not be replayed against the
|
|
13
13
|
* per-child units this version stores.
|
|
14
|
+
*
|
|
15
|
+
* v4: the input set is now `packageBuildInputs` instead of `src/**` + the
|
|
16
|
+
* manifests (#1651). Every v3 key was computed over a strictly narrower set,
|
|
17
|
+
* so replaying one would report a hit for a change it could not have seen.
|
|
18
|
+
*/
|
|
19
|
+
export declare const BUILD_CACHE_LAYOUT_VERSION = "v4";
|
|
20
|
+
/**
|
|
21
|
+
* Conventional per-package build output directories, checked in this order.
|
|
22
|
+
*
|
|
23
|
+
* Deliberately the same constant the input definition subtracts: a directory
|
|
24
|
+
* this cache STORES as an output must never also be hashed as an input, or
|
|
25
|
+
* storing it would invalidate the key it was stored under.
|
|
14
26
|
*/
|
|
15
|
-
export declare const BUILD_CACHE_LAYOUT_VERSION = "v3";
|
|
16
|
-
/** Conventional per-package build output directories, checked in this order. */
|
|
17
27
|
export declare const OUTPUT_DIR_CANDIDATES: readonly ["lib", "dist", "dist-templates"];
|
|
18
28
|
/** Keep at most this many keys per package; evict the least recently stored/used. */
|
|
19
29
|
export declare const MAX_KEYS_PER_PACKAGE = 2;
|
|
@@ -45,8 +55,9 @@ export declare function sanitizePackageDirName(name: string): string;
|
|
|
45
55
|
/** Streamed sha256 of one file (bounded memory, real contents). */
|
|
46
56
|
export declare function hashFileStream(path: string): string;
|
|
47
57
|
/**
|
|
48
|
-
* Content hash of a package's build INPUTS
|
|
49
|
-
*
|
|
58
|
+
* Content hash of a package's build INPUTS — the set is
|
|
59
|
+
* {@link packageBuildInputs}, shared with `gjsify test`'s freshness check so
|
|
60
|
+
* the two cannot drift about what an input is. Sorted, content-streamed:
|
|
50
61
|
* mtimes do not participate, so a `git checkout` with identical contents
|
|
51
62
|
* hashes identically across machines.
|
|
52
63
|
*/
|