@gjsify/cli 0.14.0 → 0.16.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 +45 -44
- package/lib/actions/build.js +24 -1
- package/lib/cli-app.js +2 -1
- package/lib/commands/foreach.d.ts +1 -0
- package/lib/commands/foreach.js +48 -13
- package/lib/commands/index.d.ts +1 -0
- package/lib/commands/index.js +1 -0
- package/lib/commands/install.js +132 -65
- package/lib/commands/login.d.ts +29 -0
- package/lib/commands/login.js +122 -96
- package/lib/commands/onboard.d.ts +14 -0
- package/lib/commands/onboard.js +453 -0
- package/lib/commands/publish.d.ts +92 -0
- package/lib/commands/publish.js +270 -295
- package/lib/commands/trust.d.ts +21 -0
- package/lib/commands/trust.js +40 -70
- package/lib/commands/workspace.d.ts +2 -0
- package/lib/commands/workspace.js +64 -14
- 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/library-output.d.ts +30 -0
- package/lib/utils/library-output.js +92 -0
- package/lib/utils/npm-otp.d.ts +67 -0
- package/lib/utils/npm-otp.js +117 -0
- package/lib/utils/suggest.d.ts +13 -0
- package/lib/utils/suggest.js +51 -0
- package/package.json +19 -18
package/lib/commands/trust.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { type NpmrcConfig } from '@gjsify/npm-registry';
|
|
1
2
|
import type { Command } from '../types/index.js';
|
|
3
|
+
import { OtpProvider } from '../utils/npm-otp.js';
|
|
2
4
|
import { type TrustEntry } from '../utils/trust-registry.js';
|
|
3
5
|
interface TrustOptions {
|
|
4
6
|
packages?: string[];
|
|
@@ -13,4 +15,23 @@ interface TrustOptions {
|
|
|
13
15
|
'no-private'?: boolean;
|
|
14
16
|
}
|
|
15
17
|
export declare const trustCommand: Command<unknown, TrustOptions>;
|
|
18
|
+
/** Parsed result of a trust-registry request. */
|
|
19
|
+
export interface TrustRequestResult {
|
|
20
|
+
status: number;
|
|
21
|
+
json: unknown;
|
|
22
|
+
text: string;
|
|
23
|
+
}
|
|
24
|
+
/** A `(method, url, body?) => TrustRequestResult` function bound to a registry auth + OTP context. */
|
|
25
|
+
export type TrustRequester = (method: string, url: string, body?: unknown) => Promise<TrustRequestResult>;
|
|
26
|
+
/**
|
|
27
|
+
* Build a trust-registry requester bound to an npmrc + a shared
|
|
28
|
+
* {@link OtpProvider}. Each request runs through `withOtpRetry` so a 2FA
|
|
29
|
+
* challenge reuses the cached code before prompting — the same instance passed
|
|
30
|
+
* to `createTrustRequester` and to the publish flow shares ONE OTP across a
|
|
31
|
+
* whole `gjsify onboard` sweep.
|
|
32
|
+
*/
|
|
33
|
+
export declare function createTrustRequester(ctx: {
|
|
34
|
+
npmrc: NpmrcConfig;
|
|
35
|
+
otpProvider: OtpProvider;
|
|
36
|
+
}): TrustRequester;
|
|
16
37
|
export type { TrustEntry };
|
package/lib/commands/trust.js
CHANGED
|
@@ -21,7 +21,7 @@ import { join } from 'node:path';
|
|
|
21
21
|
import { DEFAULT_REGISTRY, buildHeaders, registryFor, whoami } from '@gjsify/npm-registry';
|
|
22
22
|
import { discoverWorkspaces, filterWorkspaces } from '@gjsify/workspace';
|
|
23
23
|
import { hasAnyCredential, loadNpmrc } from '../utils/load-npmrc.js';
|
|
24
|
-
import {
|
|
24
|
+
import { OtpProvider, withOtpRetry } from '../utils/npm-otp.js';
|
|
25
25
|
import { classifyTrustList, githubTrustBody, normalizeWorkflowFile, parseRepoFromGitRemote, trustUrl, validateRepository, } from '../utils/trust-registry.js';
|
|
26
26
|
export const trustCommand = {
|
|
27
27
|
command: 'trust [packages..]',
|
|
@@ -148,63 +148,13 @@ export const trustCommand = {
|
|
|
148
148
|
if (dryRun)
|
|
149
149
|
console.log('(dry-run — nothing will be written)');
|
|
150
150
|
console.log();
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
let pendingOtp = args.otp;
|
|
159
|
-
const request = async (method, url, body) => {
|
|
160
|
-
const doFetch = async (code) => {
|
|
161
|
-
const headers = buildHeaders(url, { npmrc });
|
|
162
|
-
headers['accept'] = 'application/json';
|
|
163
|
-
if (body !== undefined)
|
|
164
|
-
headers['content-type'] = 'application/json';
|
|
165
|
-
if (code)
|
|
166
|
-
headers['npm-otp'] = code;
|
|
167
|
-
return fetch(url, {
|
|
168
|
-
method,
|
|
169
|
-
headers,
|
|
170
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
171
|
-
});
|
|
172
|
-
};
|
|
173
|
-
// 1) Try without an OTP — succeeds outright (read) or via an active
|
|
174
|
-
// 2FA-skip window.
|
|
175
|
-
let res = await doFetch();
|
|
176
|
-
// 2) On a challenge, use the --otp (once) or prompt, and retry.
|
|
177
|
-
if (await isOtpChallenge(res)) {
|
|
178
|
-
let code = pendingOtp;
|
|
179
|
-
pendingOtp = undefined; // one-shot — never reuse a stale code
|
|
180
|
-
if (!code) {
|
|
181
|
-
code = await promptLine('This operation requires a one-time password.\nEnter OTP: ');
|
|
182
|
-
}
|
|
183
|
-
if (!code) {
|
|
184
|
-
console.error('gjsify trust: no OTP entered.');
|
|
185
|
-
process.exit(1);
|
|
186
|
-
}
|
|
187
|
-
res = await doFetch(code);
|
|
188
|
-
// 3) Still challenged → the code was stale/expired. Prompt fresh once.
|
|
189
|
-
if (await isOtpChallenge(res)) {
|
|
190
|
-
const fresh = await promptLine('OTP rejected (expired?) — enter a fresh code: ');
|
|
191
|
-
if (!fresh) {
|
|
192
|
-
console.error('gjsify trust: no OTP entered.');
|
|
193
|
-
process.exit(1);
|
|
194
|
-
}
|
|
195
|
-
res = await doFetch(fresh);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
const text = await res.text().catch(() => '');
|
|
199
|
-
let json = undefined;
|
|
200
|
-
try {
|
|
201
|
-
json = text ? JSON.parse(text) : undefined;
|
|
202
|
-
}
|
|
203
|
-
catch {
|
|
204
|
-
/* non-JSON body — leave json undefined */
|
|
205
|
-
}
|
|
206
|
-
return { status: res.status, json, text };
|
|
207
|
-
};
|
|
151
|
+
// Cache-first OTP across the whole sweep (shared helper). Each request
|
|
152
|
+
// tries WITHOUT an OTP first (npm's ~5-min 2FA-skip window covers the
|
|
153
|
+
// packages after the first success) and only sends a code when actually
|
|
154
|
+
// challenged: the cached `--otp` / last-typed code first, then a fresh
|
|
155
|
+
// prompt when npm rejects it. See utils/npm-otp.ts.
|
|
156
|
+
const otpProvider = new OtpProvider(args.otp);
|
|
157
|
+
const request = createTrustRequester({ npmrc, otpProvider });
|
|
208
158
|
let ok = 0;
|
|
209
159
|
let skipped = 0;
|
|
210
160
|
let unpublished = 0;
|
|
@@ -288,18 +238,38 @@ export const trustCommand = {
|
|
|
288
238
|
}
|
|
289
239
|
},
|
|
290
240
|
};
|
|
291
|
-
/**
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
241
|
+
/**
|
|
242
|
+
* Build a trust-registry requester bound to an npmrc + a shared
|
|
243
|
+
* {@link OtpProvider}. Each request runs through `withOtpRetry` so a 2FA
|
|
244
|
+
* challenge reuses the cached code before prompting — the same instance passed
|
|
245
|
+
* to `createTrustRequester` and to the publish flow shares ONE OTP across a
|
|
246
|
+
* whole `gjsify onboard` sweep.
|
|
247
|
+
*/
|
|
248
|
+
export function createTrustRequester(ctx) {
|
|
249
|
+
return async (method, url, body) => {
|
|
250
|
+
const doFetch = (code) => {
|
|
251
|
+
const headers = buildHeaders(url, { npmrc: ctx.npmrc });
|
|
252
|
+
headers['accept'] = 'application/json';
|
|
253
|
+
if (body !== undefined)
|
|
254
|
+
headers['content-type'] = 'application/json';
|
|
255
|
+
if (code)
|
|
256
|
+
headers['npm-otp'] = code;
|
|
257
|
+
const init = { method, headers };
|
|
258
|
+
if (body !== undefined)
|
|
259
|
+
init.body = JSON.stringify(body);
|
|
260
|
+
return fetch(url, init);
|
|
261
|
+
};
|
|
262
|
+
const res = await withOtpRetry(doFetch, ctx.otpProvider);
|
|
263
|
+
const text = await res.text().catch(() => '');
|
|
264
|
+
let json = undefined;
|
|
265
|
+
try {
|
|
266
|
+
json = text ? JSON.parse(text) : undefined;
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
/* non-JSON body — leave json undefined */
|
|
270
|
+
}
|
|
271
|
+
return { status: res.status, json, text };
|
|
272
|
+
};
|
|
303
273
|
}
|
|
304
274
|
function firstLine(s) {
|
|
305
275
|
return s.split('\n')[0].slice(0, 200);
|
|
@@ -7,6 +7,8 @@ interface WorkspaceCmdOptions {
|
|
|
7
7
|
'continue-on-error'?: boolean;
|
|
8
8
|
'include-dev'?: boolean;
|
|
9
9
|
verbose?: boolean;
|
|
10
|
+
cached?: boolean;
|
|
10
11
|
}
|
|
11
12
|
export declare const workspaceCommand: Command<unknown, WorkspaceCmdOptions>;
|
|
13
|
+
export declare function detectPackageManager(): 'yarn' | 'npm' | 'gjsify';
|
|
12
14
|
export {};
|
|
@@ -15,9 +15,11 @@
|
|
|
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';
|
|
22
|
+
import { suggestClosest } from '../utils/suggest.js';
|
|
21
23
|
export const workspaceCommand = {
|
|
22
24
|
command: 'workspace <name> <script> [args..]',
|
|
23
25
|
description: 'Run a workspace script (`yarn workspace <name> run <script>` equivalent).',
|
|
@@ -41,7 +43,7 @@ export const workspaceCommand = {
|
|
|
41
43
|
// `-t`/`--topological` is the same flag spelled the way
|
|
42
44
|
// `gjsify foreach` already spells it — kept as an alias so
|
|
43
45
|
// muscle memory transfers.
|
|
44
|
-
description:
|
|
46
|
+
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
47
|
type: 'boolean',
|
|
46
48
|
alias: ['d', 't', 'topological'],
|
|
47
49
|
default: false,
|
|
@@ -61,6 +63,10 @@ export const workspaceCommand = {
|
|
|
61
63
|
type: 'boolean',
|
|
62
64
|
alias: 'v',
|
|
63
65
|
default: false,
|
|
66
|
+
})
|
|
67
|
+
.option('cached', {
|
|
68
|
+
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.',
|
|
69
|
+
type: 'boolean',
|
|
64
70
|
}),
|
|
65
71
|
handler: async (args) => {
|
|
66
72
|
// Walk up to the monorepo root — `gjsify workspace` is often
|
|
@@ -71,8 +77,21 @@ export const workspaceCommand = {
|
|
|
71
77
|
const allWorkspaces = discoverWorkspaces(root);
|
|
72
78
|
const target = allWorkspaces.find((w) => w.name === args.name);
|
|
73
79
|
if (!target) {
|
|
74
|
-
|
|
75
|
-
process.exit(
|
|
80
|
+
// IMPORTANT: `return process.exit(N)`, not a bare `process.exit(N)`.
|
|
81
|
+
// Under GJS `process.exit` cannot terminate synchronously (a parked
|
|
82
|
+
// GLib.MainLoop owns the thread) — it SCHEDULES the exit on a
|
|
83
|
+
// GLib.idle source and returns a forever-pending Promise. A bare
|
|
84
|
+
// call therefore falls through to the next statements, dereferences
|
|
85
|
+
// `target.manifest` on `undefined`, and the resulting throw races
|
|
86
|
+
// the already-scheduled exit into a SECOND `system.exit` →
|
|
87
|
+
// `m_should_exit assertion failed` → core dump. `return`-ing halts
|
|
88
|
+
// the handler so exactly one exit is scheduled. (No-op under Node,
|
|
89
|
+
// where `process.exit` never returns.)
|
|
90
|
+
const suggestion = suggestClosest(args.name, allWorkspaces.map((w) => w.name));
|
|
91
|
+
const hint = suggestion ? ` — did you mean "${suggestion}"?` : '';
|
|
92
|
+
console.error(`gjsify workspace: no workspace named "${args.name}"${hint} ` +
|
|
93
|
+
`(discovered ${allWorkspaces.length} workspace(s) under ${root})`);
|
|
94
|
+
return process.exit(1);
|
|
76
95
|
}
|
|
77
96
|
const runner = detectPackageManager();
|
|
78
97
|
const verbose = args.verbose === true;
|
|
@@ -80,8 +99,12 @@ export const workspaceCommand = {
|
|
|
80
99
|
const continueOnError = args['continue-on-error'] === true;
|
|
81
100
|
const targetScripts = target.manifest.scripts ?? {};
|
|
82
101
|
if (typeof targetScripts[args.script] !== 'string') {
|
|
83
|
-
|
|
84
|
-
|
|
102
|
+
const scriptHint = suggestClosest(args.script, Object.keys(targetScripts));
|
|
103
|
+
console.error(`gjsify workspace: workspace "${args.name}" has no script "${args.script}"` +
|
|
104
|
+
(scriptHint ? ` — did you mean "${scriptHint}"?` : ''));
|
|
105
|
+
// `return process.exit` — see the not-found branch above for why a
|
|
106
|
+
// bare `process.exit` is unsafe under GJS.
|
|
107
|
+
return process.exit(1);
|
|
85
108
|
}
|
|
86
109
|
// Build the run-list: either just the target, or its transitive
|
|
87
110
|
// workspace-dep closure in topological order followed by the
|
|
@@ -97,6 +120,20 @@ export const workspaceCommand = {
|
|
|
97
120
|
else {
|
|
98
121
|
runList = [target];
|
|
99
122
|
}
|
|
123
|
+
// Content-hash build cache (ADR 0006 phase 1). Explicit --cached /
|
|
124
|
+
// --no-cached wins; GJSIFY_BUILD_CACHE=1 is the env default so root
|
|
125
|
+
// script chains opt in without editing every nested invocation. Keys
|
|
126
|
+
// are computed against ALL discovered workspaces.
|
|
127
|
+
const cached = args.cached ?? buildCacheEnabledByEnv();
|
|
128
|
+
const cache = cached
|
|
129
|
+
? new BuildCacheRunner({
|
|
130
|
+
root,
|
|
131
|
+
workspaces: allWorkspaces,
|
|
132
|
+
script: args.script,
|
|
133
|
+
args: args.args ?? [],
|
|
134
|
+
targets: runList.map((w) => w.name),
|
|
135
|
+
})
|
|
136
|
+
: undefined;
|
|
100
137
|
const failures = [];
|
|
101
138
|
for (const ws of runList) {
|
|
102
139
|
const scripts = ws.manifest.scripts ?? {};
|
|
@@ -104,31 +141,35 @@ export const workspaceCommand = {
|
|
|
104
141
|
// Skip deps that don't declare the script — mirrors
|
|
105
142
|
// `gjsify foreach`'s --if-present behaviour. The target
|
|
106
143
|
// itself was validated above; this branch only filters
|
|
107
|
-
// intermediate deps.
|
|
144
|
+
// intermediate deps. A skipped package is also never touched
|
|
145
|
+
// by the build cache (the no-build-package safety rule).
|
|
108
146
|
if (verbose)
|
|
109
147
|
console.error(`[${ws.name}] (no "${args.script}" script — skipping)`);
|
|
110
148
|
continue;
|
|
111
149
|
}
|
|
112
150
|
try {
|
|
113
|
-
await runOne(ws, args.script, args.args ?? [], runner, verbose);
|
|
151
|
+
await runOne(ws, args.script, args.args ?? [], runner, verbose, cache);
|
|
114
152
|
}
|
|
115
153
|
catch (err) {
|
|
116
154
|
const e = err instanceof Error ? err : new Error(String(err));
|
|
117
155
|
console.error(`[${ws.name}] ${e.message}`);
|
|
118
156
|
if (!continueOnError) {
|
|
119
|
-
process.exit
|
|
157
|
+
// `return` — a bare `process.exit` would not halt under GJS
|
|
158
|
+
// and the loop would keep running the remaining deps.
|
|
159
|
+
return process.exit(1);
|
|
120
160
|
}
|
|
121
161
|
failures.push({ workspace: ws.name, error: e });
|
|
122
162
|
}
|
|
123
163
|
}
|
|
124
164
|
if (failures.length > 0) {
|
|
125
165
|
console.error(`gjsify workspace: ${failures.length} workspace(s) failed: ${failures.map((f) => f.workspace).join(', ')}`);
|
|
126
|
-
process.exit(1);
|
|
166
|
+
return process.exit(1);
|
|
127
167
|
}
|
|
128
168
|
// ensureMainLoop() (called inside spawn) keeps GJS alive after the
|
|
129
169
|
// child exits — without an explicit process.exit() the success path
|
|
130
|
-
// would park the loop forever.
|
|
131
|
-
|
|
170
|
+
// would park the loop forever. `return` for symmetry with the error
|
|
171
|
+
// paths above (see the not-found branch for the GJS rationale).
|
|
172
|
+
return process.exit(0);
|
|
132
173
|
},
|
|
133
174
|
};
|
|
134
175
|
/**
|
|
@@ -175,7 +216,14 @@ function collectTransitiveClosure(target, allWorkspaces, includeDev) {
|
|
|
175
216
|
}
|
|
176
217
|
return out;
|
|
177
218
|
}
|
|
178
|
-
async function runOne(ws, script, extraArgs, runner, verbose) {
|
|
219
|
+
async function runOne(ws, script, extraArgs, runner, verbose, cache) {
|
|
220
|
+
// Build-cache fast path: on a key hit restore the stored outputs and
|
|
221
|
+
// skip the script; on a miss snapshot the output dirs so only what the
|
|
222
|
+
// script MODIFIED is stored after success (never a committed no-build
|
|
223
|
+
// lib/ — see utils/build-cache.ts).
|
|
224
|
+
if (cache && cache.tryRestore(ws))
|
|
225
|
+
return;
|
|
226
|
+
const before = cache?.snapshotOutputs(ws);
|
|
179
227
|
const argv = runner === 'gjsify'
|
|
180
228
|
? ['run', script, ...extraArgs]
|
|
181
229
|
: ['run', script, ...(extraArgs.length > 0 ? ['--', ...extraArgs] : [])];
|
|
@@ -202,8 +250,10 @@ async function runOne(ws, script, extraArgs, runner, verbose) {
|
|
|
202
250
|
});
|
|
203
251
|
child.on('error', reject);
|
|
204
252
|
});
|
|
253
|
+
if (cache && before)
|
|
254
|
+
cache.storeAfterSuccess(ws, before);
|
|
205
255
|
}
|
|
206
|
-
function detectPackageManager() {
|
|
256
|
+
export function detectPackageManager() {
|
|
207
257
|
// Under GJS there is no Node, so neither `npm` nor `yarn` can run scripts —
|
|
208
258
|
// the only option is `gjsify` itself, resolved via the GJS shim on PATH
|
|
209
259
|
// (see `ensureGjsifyShimOnPath`). This is what makes node-free multi-package
|
|
@@ -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
|
+
}
|