@gjsify/cli 0.13.2 → 0.15.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/dist/cli.gjs.mjs +34 -29
- package/lib/commands/foreach.d.ts +1 -0
- package/lib/commands/foreach.js +48 -13
- package/lib/commands/install.js +132 -65
- package/lib/commands/storybook.d.ts +1 -0
- package/lib/commands/storybook.js +51 -9
- package/lib/commands/workspace.d.ts +1 -0
- package/lib/commands/workspace.js +34 -5
- package/lib/utils/build-cache.d.ts +157 -0
- package/lib/utils/build-cache.js +670 -0
- package/lib/utils/install-backend-native.js +104 -9
- package/lib/utils/install-backend.d.ts +10 -0
- package/lib/utils/install-cache-fs.d.ts +9 -0
- package/lib/utils/install-cache-fs.js +20 -6
- package/lib/utils/install-lock.d.ts +15 -0
- package/lib/utils/install-lock.js +293 -0
- package/lib/utils/run-node.d.ts +20 -0
- package/lib/utils/run-node.js +85 -0
- package/package.json +19 -18
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
// failures on uninbuilt workspace deps are the single biggest local-dev
|
|
16
16
|
// pain point in this monorepo.
|
|
17
17
|
import { spawn } from 'node:child_process';
|
|
18
|
-
import { buildDependencyGraph, discoverWorkspaces, topologicalSort
|
|
18
|
+
import { buildDependencyGraph, discoverWorkspaces, topologicalSort } from '@gjsify/workspace';
|
|
19
19
|
import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
20
|
+
import { BuildCacheRunner, buildCacheEnabledByEnv } from '../utils/build-cache.js';
|
|
20
21
|
import { isGjs } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
21
22
|
export const workspaceCommand = {
|
|
22
23
|
command: 'workspace <name> <script> [args..]',
|
|
@@ -41,7 +42,7 @@ export const workspaceCommand = {
|
|
|
41
42
|
// `-t`/`--topological` is the same flag spelled the way
|
|
42
43
|
// `gjsify foreach` already spells it — kept as an alias so
|
|
43
44
|
// muscle memory transfers.
|
|
44
|
-
description:
|
|
45
|
+
description: "Pre-build the target workspace's transitive workspace dependencies in topological order before running the script in the target. Deps that don't declare the script are skipped (--if-present behaviour). Replaces manual `gjsify workspace A build && gjsify workspace B build && …` chains.",
|
|
45
46
|
type: 'boolean',
|
|
46
47
|
alias: ['d', 't', 'topological'],
|
|
47
48
|
default: false,
|
|
@@ -61,6 +62,10 @@ export const workspaceCommand = {
|
|
|
61
62
|
type: 'boolean',
|
|
62
63
|
alias: 'v',
|
|
63
64
|
default: false,
|
|
65
|
+
})
|
|
66
|
+
.option('cached', {
|
|
67
|
+
description: 'Content-hash build cache (ADR 0006): skip the script for a workspace whose inputs (src/**, package.json, tsconfig*.json, transitive workspace deps, toolchain versions) are unchanged and restore its stored outputs instead; on miss run it and store the output dirs it modified. Also applies to the deps run via --with-dependencies. Default from GJSIFY_BUILD_CACHE=1.',
|
|
68
|
+
type: 'boolean',
|
|
64
69
|
}),
|
|
65
70
|
handler: async (args) => {
|
|
66
71
|
// Walk up to the monorepo root — `gjsify workspace` is often
|
|
@@ -97,6 +102,20 @@ export const workspaceCommand = {
|
|
|
97
102
|
else {
|
|
98
103
|
runList = [target];
|
|
99
104
|
}
|
|
105
|
+
// Content-hash build cache (ADR 0006 phase 1). Explicit --cached /
|
|
106
|
+
// --no-cached wins; GJSIFY_BUILD_CACHE=1 is the env default so root
|
|
107
|
+
// script chains opt in without editing every nested invocation. Keys
|
|
108
|
+
// are computed against ALL discovered workspaces.
|
|
109
|
+
const cached = args.cached ?? buildCacheEnabledByEnv();
|
|
110
|
+
const cache = cached
|
|
111
|
+
? new BuildCacheRunner({
|
|
112
|
+
root,
|
|
113
|
+
workspaces: allWorkspaces,
|
|
114
|
+
script: args.script,
|
|
115
|
+
args: args.args ?? [],
|
|
116
|
+
targets: runList.map((w) => w.name),
|
|
117
|
+
})
|
|
118
|
+
: undefined;
|
|
100
119
|
const failures = [];
|
|
101
120
|
for (const ws of runList) {
|
|
102
121
|
const scripts = ws.manifest.scripts ?? {};
|
|
@@ -104,13 +123,14 @@ export const workspaceCommand = {
|
|
|
104
123
|
// Skip deps that don't declare the script — mirrors
|
|
105
124
|
// `gjsify foreach`'s --if-present behaviour. The target
|
|
106
125
|
// itself was validated above; this branch only filters
|
|
107
|
-
// intermediate deps.
|
|
126
|
+
// intermediate deps. A skipped package is also never touched
|
|
127
|
+
// by the build cache (the no-build-package safety rule).
|
|
108
128
|
if (verbose)
|
|
109
129
|
console.error(`[${ws.name}] (no "${args.script}" script — skipping)`);
|
|
110
130
|
continue;
|
|
111
131
|
}
|
|
112
132
|
try {
|
|
113
|
-
await runOne(ws, args.script, args.args ?? [], runner, verbose);
|
|
133
|
+
await runOne(ws, args.script, args.args ?? [], runner, verbose, cache);
|
|
114
134
|
}
|
|
115
135
|
catch (err) {
|
|
116
136
|
const e = err instanceof Error ? err : new Error(String(err));
|
|
@@ -175,7 +195,14 @@ function collectTransitiveClosure(target, allWorkspaces, includeDev) {
|
|
|
175
195
|
}
|
|
176
196
|
return out;
|
|
177
197
|
}
|
|
178
|
-
async function runOne(ws, script, extraArgs, runner, verbose) {
|
|
198
|
+
async function runOne(ws, script, extraArgs, runner, verbose, cache) {
|
|
199
|
+
// Build-cache fast path: on a key hit restore the stored outputs and
|
|
200
|
+
// skip the script; on a miss snapshot the output dirs so only what the
|
|
201
|
+
// script MODIFIED is stored after success (never a committed no-build
|
|
202
|
+
// lib/ — see utils/build-cache.ts).
|
|
203
|
+
if (cache && cache.tryRestore(ws))
|
|
204
|
+
return;
|
|
205
|
+
const before = cache?.snapshotOutputs(ws);
|
|
179
206
|
const argv = runner === 'gjsify'
|
|
180
207
|
? ['run', script, ...extraArgs]
|
|
181
208
|
: ['run', script, ...(extraArgs.length > 0 ? ['--', ...extraArgs] : [])];
|
|
@@ -202,6 +229,8 @@ async function runOne(ws, script, extraArgs, runner, verbose) {
|
|
|
202
229
|
});
|
|
203
230
|
child.on('error', reject);
|
|
204
231
|
});
|
|
232
|
+
if (cache && before)
|
|
233
|
+
cache.storeAfterSuccess(ws, before);
|
|
205
234
|
}
|
|
206
235
|
function detectPackageManager() {
|
|
207
236
|
// Under GJS there is no Node, so neither `npm` nor `yarn` can run scripts —
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { type Workspace } from '@gjsify/workspace';
|
|
2
|
+
/** Bump to invalidate every existing cache entry (schema/semantic changes). */
|
|
3
|
+
export declare const BUILD_CACHE_LAYOUT_VERSION = "v1";
|
|
4
|
+
/** Conventional per-package build output directories, checked in this order. */
|
|
5
|
+
export declare const OUTPUT_DIR_CANDIDATES: readonly ["lib", "dist", "dist-templates"];
|
|
6
|
+
/** Keep at most this many keys per package; evict the least recently stored/used. */
|
|
7
|
+
export declare const MAX_KEYS_PER_PACKAGE = 2;
|
|
8
|
+
/** Toolchain packages whose resolved versions salt every cache key. */
|
|
9
|
+
export declare const TOOLCHAIN_PACKAGES: readonly ["@gjsify/cli", "@gjsify/tsc", "rolldown", "typescript"];
|
|
10
|
+
export interface BuildCacheManifest {
|
|
11
|
+
version: 1;
|
|
12
|
+
layout: string;
|
|
13
|
+
package: string;
|
|
14
|
+
script: string;
|
|
15
|
+
key: string;
|
|
16
|
+
/** Output dirs (relative to the package dir) the build modified. */
|
|
17
|
+
dirs: string[];
|
|
18
|
+
/** Content hash of the stored output dirs (mtime-independent). */
|
|
19
|
+
outputsHash: string;
|
|
20
|
+
createdAt: string;
|
|
21
|
+
usedAt: string;
|
|
22
|
+
}
|
|
23
|
+
/** `true` when GJSIFY_BUILD_CACHE is set to a non-empty, non-"0"/"false" value. */
|
|
24
|
+
export declare function buildCacheEnabledByEnv(env?: Record<string, string | undefined>): boolean;
|
|
25
|
+
/** Cache root for a workspace: `<root>/node_modules/.cache/gjsify/build`. */
|
|
26
|
+
export declare function buildCacheRoot(workspaceRoot: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Directory-safe package name. Scoped names collapse (`@gjsify/cli` →
|
|
29
|
+
* `gjsify-cli`); an 8-hex sha256 suffix of the exact name rules out
|
|
30
|
+
* collisions between e.g. `@a/b-c` and `@a-b/c`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function sanitizePackageDirName(name: string): string;
|
|
33
|
+
/** Streamed sha256 of one file (bounded memory, real contents). */
|
|
34
|
+
export declare function hashFileStream(path: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Content hash of a package's build INPUTS: every file under `src/**`, plus
|
|
37
|
+
* `package.json`, plus root `tsconfig*.json`. Sorted, content-streamed —
|
|
38
|
+
* mtimes do not participate, so a `git checkout` with identical contents
|
|
39
|
+
* hashes identically across machines.
|
|
40
|
+
*/
|
|
41
|
+
export declare function hashPackageInputs(location: string): string;
|
|
42
|
+
/**
|
|
43
|
+
* Mtime-independent content hash of a set of output dirs — used to skip the
|
|
44
|
+
* restore copy when the on-disk outputs already match the cached entry, and
|
|
45
|
+
* as an integrity stamp of what was stored.
|
|
46
|
+
*/
|
|
47
|
+
export declare function hashOutputDirs(location: string, dirs: readonly string[]): string;
|
|
48
|
+
/**
|
|
49
|
+
* Cheap per-dir change signature (existence + file list + sizes + mtimes).
|
|
50
|
+
* Taken BEFORE running the script and compared AFTER, to detect which output
|
|
51
|
+
* dirs the build actually modified. Content hashing is deliberately avoided
|
|
52
|
+
* here — a build that rewrites identical bytes still bumps mtimes, and that
|
|
53
|
+
* is exactly the "this dir belongs to the build" signal we want.
|
|
54
|
+
*/
|
|
55
|
+
export declare function snapshotOutputDirs(location: string): Record<string, string | null>;
|
|
56
|
+
/** Output dirs whose signature changed (or that newly appeared) since `before`. */
|
|
57
|
+
export declare function modifiedOutputDirs(location: string, before: Record<string, string | null>): string[];
|
|
58
|
+
export interface ComposeCacheKeyInput {
|
|
59
|
+
script: string;
|
|
60
|
+
/** Extra args forwarded to the script invocation (affect the outputs). */
|
|
61
|
+
args?: readonly string[];
|
|
62
|
+
/** Toolchain package → resolved version (or 'none'). */
|
|
63
|
+
toolchain: Record<string, string>;
|
|
64
|
+
/** The package's own input hash. */
|
|
65
|
+
ownHash: string;
|
|
66
|
+
/** dep name → input hash for the FULL transitive workspace-dep closure. */
|
|
67
|
+
depHashes: ReadonlyMap<string, string> | Record<string, string>;
|
|
68
|
+
}
|
|
69
|
+
/** Deterministic cache key (32 hex chars) from all invalidation ingredients. */
|
|
70
|
+
export declare function composeCacheKey(input: ComposeCacheKeyInput): string;
|
|
71
|
+
/**
|
|
72
|
+
* Resolve the toolchain versions that salt every key from the workspace's
|
|
73
|
+
* OWN resolution (`<root>/node_modules/<pkg>/package.json`). In the gjsify
|
|
74
|
+
* monorepo these are symlinks into the workspace itself, so a CLI/tsc bump
|
|
75
|
+
* invalidates immediately. A package that isn't resolvable maps to 'none'
|
|
76
|
+
* (deterministic). `@gjsify/cli` additionally falls back to the RUNNING
|
|
77
|
+
* CLI's own version so globally-installed CLIs still salt correctly.
|
|
78
|
+
*/
|
|
79
|
+
export declare function resolveToolchainVersions(workspaceRoot: string): Record<string, string>;
|
|
80
|
+
/** Directory of one cache entry: `<cacheRoot>/<pkg-sanitized>/<key>`. */
|
|
81
|
+
export declare function buildCacheEntryDir(cacheRoot: string, pkgName: string, key: string): string;
|
|
82
|
+
/** Read + validate a cache entry's manifest; any mismatch is a MISS (null). */
|
|
83
|
+
export declare function readCacheEntry(cacheRoot: string, pkgName: string, key: string): BuildCacheManifest | null;
|
|
84
|
+
/**
|
|
85
|
+
* Store the given output dirs of a package under `key`. Atomic: builds a
|
|
86
|
+
* `.tmp-<pid>` sibling then renames into place, so concurrent readers never
|
|
87
|
+
* observe a partial entry. Best-effort — failures are swallowed (the build
|
|
88
|
+
* itself already succeeded).
|
|
89
|
+
*/
|
|
90
|
+
export declare function storeCacheEntry(cacheRoot: string, pkg: {
|
|
91
|
+
name: string;
|
|
92
|
+
location: string;
|
|
93
|
+
}, key: string, script: string, dirs: readonly string[]): BuildCacheManifest | null;
|
|
94
|
+
/**
|
|
95
|
+
* Restore a validated entry into the package. Replaces EXACTLY the dirs the
|
|
96
|
+
* manifest records (delete-then-copy within the package); skips the copy
|
|
97
|
+
* when the on-disk outputs already content-match the entry. Returns how the
|
|
98
|
+
* hit was satisfied, or null when restoration failed (treat as MISS).
|
|
99
|
+
*/
|
|
100
|
+
export declare function restoreCacheEntry(cacheRoot: string, pkg: {
|
|
101
|
+
name: string;
|
|
102
|
+
location: string;
|
|
103
|
+
}, manifest: BuildCacheManifest): 'restored' | 'up-to-date' | null;
|
|
104
|
+
/** Evict the oldest entries (by last store/use) beyond `keep` per package. */
|
|
105
|
+
export declare function pruneCacheEntries(cacheRoot: string, pkgName: string, keep?: number): void;
|
|
106
|
+
export interface BuildCacheRunnerOptions {
|
|
107
|
+
/** Workspace (monorepo) root — owns node_modules/.cache. */
|
|
108
|
+
root: string;
|
|
109
|
+
/** ALL discovered workspaces (not just the selected set) for dep closure. */
|
|
110
|
+
workspaces: readonly Workspace[];
|
|
111
|
+
/** Script name being run (part of every key). */
|
|
112
|
+
script: string;
|
|
113
|
+
/** Extra args forwarded to every invocation (part of every key). */
|
|
114
|
+
args?: readonly string[];
|
|
115
|
+
/**
|
|
116
|
+
* Workspace names that will be run. Their input hashes (plus their
|
|
117
|
+
* closures') are computed EAGERLY at construction so every key reflects
|
|
118
|
+
* the tree state at RUN START — a build that mutates its own `src/`
|
|
119
|
+
* (adwaita-web's generated stylesheet module) must not make a sibling's
|
|
120
|
+
* key depend on scheduling order within the same run.
|
|
121
|
+
*/
|
|
122
|
+
targets?: readonly string[];
|
|
123
|
+
/** Log sink (default: stderr, keeping stdout clean for script output). */
|
|
124
|
+
log?: (line: string) => void;
|
|
125
|
+
}
|
|
126
|
+
export declare function buildCacheOptedOut(ws: Workspace): boolean;
|
|
127
|
+
/**
|
|
128
|
+
* Per-run orchestrator used by `gjsify workspace` / `gjsify foreach`:
|
|
129
|
+
* memoizes input hashes + keys across packages, restores on hit, snapshots +
|
|
130
|
+
* stores on miss. All entry points are fail-soft — any internal error logs a
|
|
131
|
+
* warning and reports a MISS so the script runs normally.
|
|
132
|
+
*/
|
|
133
|
+
export declare class BuildCacheRunner {
|
|
134
|
+
private readonly options;
|
|
135
|
+
private readonly cacheRoot;
|
|
136
|
+
private readonly graph;
|
|
137
|
+
private readonly toolchain;
|
|
138
|
+
private readonly ownHashes;
|
|
139
|
+
private readonly closures;
|
|
140
|
+
private readonly keys;
|
|
141
|
+
private readonly log;
|
|
142
|
+
constructor(options: BuildCacheRunnerOptions);
|
|
143
|
+
private ownHashOf;
|
|
144
|
+
/** BFS transitive workspace-dep closure (cycle-safe), excluding `name` itself. */
|
|
145
|
+
private closureOf;
|
|
146
|
+
/** Content-hash cache key for one workspace (memoized per run). */
|
|
147
|
+
keyFor(ws: Workspace): string;
|
|
148
|
+
/**
|
|
149
|
+
* Try to satisfy the script from the cache. Returns `true` on a hit
|
|
150
|
+
* (outputs restored / already current — the caller SKIPS the script).
|
|
151
|
+
*/
|
|
152
|
+
tryRestore(ws: Workspace): boolean;
|
|
153
|
+
/** Snapshot the candidate output dirs BEFORE running the script. */
|
|
154
|
+
snapshotOutputs(ws: Workspace): Record<string, string | null>;
|
|
155
|
+
/** After a SUCCESSFUL script run: store the dirs the build modified. */
|
|
156
|
+
storeAfterSuccess(ws: Workspace, before: Record<string, string | null>): void;
|
|
157
|
+
}
|