@vzn/vx 0.0.585 → 0.0.586
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/package.json +5 -5
- package/src/cache/index.ts +2 -0
- package/src/cache/inputs.ts +186 -14
- package/src/orchestrator/execute-task.ts +6 -6
- package/src/orchestrator/task-hash.ts +21 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.586",
|
|
4
4
|
"description": "The fastest task runner and build cache for JavaScript monorepos.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
"node": ">=18"
|
|
22
22
|
},
|
|
23
23
|
"optionalDependencies": {
|
|
24
|
-
"@vzn/vx-linux-x64": "0.0.
|
|
25
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
26
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
27
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
24
|
+
"@vzn/vx-linux-x64": "0.0.586",
|
|
25
|
+
"@vzn/vx-linux-arm64": "0.0.586",
|
|
26
|
+
"@vzn/vx-darwin-x64": "0.0.586",
|
|
27
|
+
"@vzn/vx-darwin-arm64": "0.0.586"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/cache/index.ts
CHANGED
package/src/cache/inputs.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
anyTaskGlob,
|
|
25
25
|
asTrees,
|
|
26
26
|
isExecutableMissing,
|
|
27
|
+
isInstalledPath,
|
|
27
28
|
isLiteralPattern,
|
|
28
29
|
normalizeGlob,
|
|
29
30
|
outputMatcher,
|
|
@@ -35,6 +36,7 @@ import {
|
|
|
35
36
|
UserError,
|
|
36
37
|
} from '../util/index.js'
|
|
37
38
|
import { GitFilesCache, runGitLsFiles } from './git-inputs.js'
|
|
39
|
+
import { FILE_HASH_RACY_MS, racyWindowMs } from './layer.js'
|
|
38
40
|
|
|
39
41
|
// The git side lives in git-inputs.ts; its whole public surface is
|
|
40
42
|
// re-exported here so a reader that reaches the resolver for it (the
|
|
@@ -93,6 +95,8 @@ const DEFAULT_FILE_GLOBS: readonly string[] = ['**/*']
|
|
|
93
95
|
|
|
94
96
|
export interface ResolvedInputs {
|
|
95
97
|
files: string[]
|
|
98
|
+
/** What `files` was filtered from, for {@link addedInput}. */
|
|
99
|
+
listings: InputListing[]
|
|
96
100
|
envValues: Array<[name: string, value: string | undefined]>
|
|
97
101
|
runtimeValues: Array<[command: string, output: string]>
|
|
98
102
|
workspaceRuntimeValues: Array<[command: string, output: string]>
|
|
@@ -177,7 +181,7 @@ export type ProjectFilesCache = Map<
|
|
|
177
181
|
>
|
|
178
182
|
|
|
179
183
|
export async function resolveInputs(args: ResolveInputsArgs): Promise<ResolvedInputs> {
|
|
180
|
-
const projectFiles = await resolveFiles({
|
|
184
|
+
const { files: projectFiles, listing } = await resolveFiles({
|
|
181
185
|
projectDir: args.projectDir,
|
|
182
186
|
workspaceRoot: args.workspaceRoot,
|
|
183
187
|
files: args.inputs?.files,
|
|
@@ -187,15 +191,18 @@ export async function resolveInputs(args: ResolveInputsArgs): Promise<ResolvedIn
|
|
|
187
191
|
...(args.projectFilesCache !== undefined ? { projectFilesCache: args.projectFilesCache } : {}),
|
|
188
192
|
})
|
|
189
193
|
let files = projectFiles
|
|
194
|
+
const listings = listing === undefined ? [] : [listing]
|
|
190
195
|
const wsDecl = args.inputs?.workspaceFiles
|
|
191
196
|
if (wsDecl !== undefined && wsDecl.length > 0) {
|
|
192
|
-
const
|
|
197
|
+
const ws = resolveWorkspaceFiles({
|
|
193
198
|
workspaceRoot: args.workspaceRoot,
|
|
194
199
|
workspaceFiles: wsDecl,
|
|
195
200
|
ownWorkspaceOutputs: args.ownWorkspaceOutputs ?? [],
|
|
196
201
|
...(args.gitFilesCache !== undefined ? { gitFilesCache: args.gitFilesCache } : {}),
|
|
197
202
|
...(args.workspaceFilesCache !== undefined ? { memo: args.workspaceFilesCache } : {}),
|
|
198
203
|
})
|
|
204
|
+
if (ws !== undefined) listings.push(ws.listing)
|
|
205
|
+
const wsFiles = ws === undefined ? [] : await ws.files
|
|
199
206
|
// Dedupe: when the project dir IS the workspace root (or a glob
|
|
200
207
|
// overlaps), the same absolute path can arrive via both lists —
|
|
201
208
|
// it must contribute to the key exactly once.
|
|
@@ -228,6 +235,7 @@ export async function resolveInputs(args: ResolveInputsArgs): Promise<ResolvedIn
|
|
|
228
235
|
])
|
|
229
236
|
return {
|
|
230
237
|
files,
|
|
238
|
+
listings,
|
|
231
239
|
envValues: resolveEnvValues(args.inputs?.env ?? [], args.envSource),
|
|
232
240
|
runtimeValues,
|
|
233
241
|
workspaceRuntimeValues,
|
|
@@ -245,13 +253,13 @@ export async function resolveInputs(args: ResolveInputsArgs): Promise<ResolvedIn
|
|
|
245
253
|
* codegen); the hard boundary continues to apply to project-relative
|
|
246
254
|
* `files` globs only.
|
|
247
255
|
*/
|
|
248
|
-
|
|
256
|
+
function resolveWorkspaceFiles(args: {
|
|
249
257
|
workspaceRoot: string
|
|
250
258
|
workspaceFiles: readonly string[]
|
|
251
259
|
ownWorkspaceOutputs: readonly string[]
|
|
252
260
|
gitFilesCache?: GitFilesCache
|
|
253
261
|
memo?: WorkspaceFilesCache
|
|
254
|
-
}): Promise<string[]
|
|
262
|
+
}): { files: Promise<string[]>; listing: InputListing } | undefined {
|
|
255
263
|
const positive: string[] = []
|
|
256
264
|
const negative: string[] = []
|
|
257
265
|
refuseOneAlternativeBrace(args.workspaceFiles, 'workspaceFiles')
|
|
@@ -259,7 +267,7 @@ async function resolveWorkspaceFiles(args: {
|
|
|
259
267
|
if (entry.startsWith('!')) negative.push(entry.slice(1))
|
|
260
268
|
else positive.push(entry)
|
|
261
269
|
}
|
|
262
|
-
if (positive.length === 0) return
|
|
270
|
+
if (positive.length === 0) return undefined
|
|
263
271
|
|
|
264
272
|
const isExcluded = anyTaskGlob([...ALWAYS_IGNORE, ...asTrees(negative)])
|
|
265
273
|
// A path the task's own outputs take back with `!` is no output, so it
|
|
@@ -285,20 +293,30 @@ async function resolveWorkspaceFiles(args: {
|
|
|
285
293
|
args.memo === undefined
|
|
286
294
|
? undefined
|
|
287
295
|
: JSON.stringify([positive, negative, args.ownWorkspaceOutputs])
|
|
296
|
+
let isPositive: ((rel: string) => boolean) | undefined
|
|
297
|
+
const excluded = (rel: string): boolean => isExcluded(rel) || ownOutput(rel)
|
|
298
|
+
const listing: InputListing = {
|
|
299
|
+
root: args.workspaceRoot,
|
|
300
|
+
listed: gitFiles,
|
|
301
|
+
isInput: (rel) => (isPositive ??= anyTaskGlob(asTrees(positive)))(rel) && !excluded(rel),
|
|
302
|
+
nested: () => false,
|
|
303
|
+
...reachOf(positive),
|
|
304
|
+
}
|
|
288
305
|
if (memoKey !== undefined) {
|
|
289
306
|
const hit = args.memo!.get(memoKey)
|
|
290
|
-
if (hit !== undefined && hit.snapshot === gitFiles) return hit.result
|
|
307
|
+
if (hit !== undefined && hit.snapshot === gitFiles) return { files: hit.result, listing }
|
|
291
308
|
}
|
|
309
|
+
isPositive ??= anyTaskGlob(asTrees(positive))
|
|
292
310
|
const result = resolveWorkspaceFilesOver(
|
|
293
311
|
args,
|
|
294
312
|
gitFiles,
|
|
295
313
|
positive,
|
|
296
|
-
|
|
297
|
-
|
|
314
|
+
isPositive,
|
|
315
|
+
excluded,
|
|
298
316
|
undecodable,
|
|
299
317
|
)
|
|
300
318
|
if (memoKey !== undefined) args.memo!.set(memoKey, { snapshot: gitFiles, result })
|
|
301
|
-
return result
|
|
319
|
+
return { files: result, listing }
|
|
302
320
|
}
|
|
303
321
|
|
|
304
322
|
async function resolveWorkspaceFilesOver(
|
|
@@ -1160,6 +1178,8 @@ interface FilesPlan {
|
|
|
1160
1178
|
isPositive: (rel: string) => boolean
|
|
1161
1179
|
/** The literal entries, each naming one path (see `unanswered`). */
|
|
1162
1180
|
literals: string[]
|
|
1181
|
+
/** The directories a positive entry reaches; see `InputListing`. */
|
|
1182
|
+
prefixes: string[]
|
|
1163
1183
|
/**
|
|
1164
1184
|
* Whether a project-relative path is an input by the declaration alone:
|
|
1165
1185
|
* a positive glob selects it, and no exclude and no own output takes it
|
|
@@ -1208,7 +1228,7 @@ function filesPlan(
|
|
|
1208
1228
|
ownOutput: outputMatcher(ownOutputs),
|
|
1209
1229
|
positiveGlobs: asTrees(positive).map(globFor),
|
|
1210
1230
|
isPositive: anyTaskGlob(asTrees(positive)),
|
|
1211
|
-
|
|
1231
|
+
...reachOf(positive),
|
|
1212
1232
|
verdicts: new Map<string, boolean>(),
|
|
1213
1233
|
}
|
|
1214
1234
|
filesPlans.set(key, plan)
|
|
@@ -1264,9 +1284,11 @@ function workspaceMatcher(
|
|
|
1264
1284
|
return m
|
|
1265
1285
|
}
|
|
1266
1286
|
|
|
1267
|
-
async function resolveFiles(
|
|
1287
|
+
async function resolveFiles(
|
|
1288
|
+
args: ResolveFilesArgs,
|
|
1289
|
+
): Promise<{ files: string[]; listing?: InputListing }> {
|
|
1268
1290
|
const plan = filesPlan(args.files, args.ownOutputs)
|
|
1269
|
-
if (plan === null) return []
|
|
1291
|
+
if (plan === null) return { files: [] }
|
|
1270
1292
|
const { positive, negative, isExcluded, ownOutput, positiveGlobs, isPositive } = plan
|
|
1271
1293
|
|
|
1272
1294
|
const nested = inNestedProject(args.projectDir, args.nestedProjectDirs)
|
|
@@ -1289,7 +1311,12 @@ async function resolveFiles(args: ResolveFilesArgs): Promise<string[]> {
|
|
|
1289
1311
|
// Identity, not equality: a re-enumeration hands back a new array even
|
|
1290
1312
|
// when the file set is unchanged, and that is exactly when this task's
|
|
1291
1313
|
// inputs must be walked again.
|
|
1292
|
-
if (memo !== undefined && memo.snapshot === gitFiles)
|
|
1314
|
+
if (memo !== undefined && memo.snapshot === gitFiles) {
|
|
1315
|
+
return {
|
|
1316
|
+
files: [...memo.result],
|
|
1317
|
+
listing: listingFor(args.projectDir, gitFiles, plan, nested),
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1293
1320
|
}
|
|
1294
1321
|
if (gitFiles === undefined) {
|
|
1295
1322
|
// Mid-run re-enumeration — or a project the workspace-wide populate
|
|
@@ -1365,7 +1392,23 @@ async function resolveFiles(args: ResolveFilesArgs): Promise<string[]> {
|
|
|
1365
1392
|
// Stored only on the way out: a declaration whose literal named an
|
|
1366
1393
|
// invisible file threw above, and every task sharing it must throw too.
|
|
1367
1394
|
args.projectFilesCache?.set(memoKey, { snapshot: gitFiles, result: resolved })
|
|
1368
|
-
return [...resolved]
|
|
1395
|
+
return { files: [...resolved], listing: listingFor(args.projectDir, gitFiles, plan, nested) }
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
function listingFor(
|
|
1399
|
+
root: string,
|
|
1400
|
+
listed: readonly string[],
|
|
1401
|
+
plan: FilesPlan,
|
|
1402
|
+
nested: (rel: string) => boolean,
|
|
1403
|
+
): InputListing {
|
|
1404
|
+
return {
|
|
1405
|
+
root,
|
|
1406
|
+
listed,
|
|
1407
|
+
isInput: (rel) => inPlan(plan, rel),
|
|
1408
|
+
nested,
|
|
1409
|
+
prefixes: plan.prefixes,
|
|
1410
|
+
literals: plan.literals,
|
|
1411
|
+
}
|
|
1369
1412
|
}
|
|
1370
1413
|
|
|
1371
1414
|
/**
|
|
@@ -1417,6 +1460,135 @@ function undecodableOnDisk(abs: string): boolean {
|
|
|
1417
1460
|
return false
|
|
1418
1461
|
}
|
|
1419
1462
|
|
|
1463
|
+
/**
|
|
1464
|
+
* The listing a task's input files were filtered from, and what reading it
|
|
1465
|
+
* again takes: {@link addedInput} looks for a file the listing lacked.
|
|
1466
|
+
*/
|
|
1467
|
+
export interface InputListing {
|
|
1468
|
+
root: string
|
|
1469
|
+
/** Root-relative paths, as git listed them. */
|
|
1470
|
+
listed: readonly string[]
|
|
1471
|
+
isInput: (rel: string) => boolean
|
|
1472
|
+
nested: (rel: string) => boolean
|
|
1473
|
+
/** Root-relative directories a positive entry reaches, `''` for the root. */
|
|
1474
|
+
prefixes: readonly string[]
|
|
1475
|
+
/** The literal entries, each naming one path. */
|
|
1476
|
+
literals: readonly string[]
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
function reachOf(positive: readonly string[]): { prefixes: string[]; literals: string[] } {
|
|
1480
|
+
const prefixes = new Set<string>()
|
|
1481
|
+
for (const p of asTrees(positive)) {
|
|
1482
|
+
if (isLiteralPattern(p)) continue
|
|
1483
|
+
const prefix = staticPrefix(p)
|
|
1484
|
+
prefixes.add(prefix === '.' ? '' : prefix)
|
|
1485
|
+
}
|
|
1486
|
+
return {
|
|
1487
|
+
prefixes: [...prefixes],
|
|
1488
|
+
literals: positive.map(normalizeGlob).filter(isLiteralPattern).map(stripTrailingSlash),
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
/** Every directory holding a listed path, `''` for the root, once per listing. */
|
|
1493
|
+
const listedDirsMemo = new WeakMap<readonly string[], Set<string>>()
|
|
1494
|
+
function listedDirs(listed: readonly string[]): Set<string> {
|
|
1495
|
+
let dirs = listedDirsMemo.get(listed)
|
|
1496
|
+
if (dirs !== undefined) return dirs
|
|
1497
|
+
dirs = new Set([''])
|
|
1498
|
+
for (const rel of listed) {
|
|
1499
|
+
for (let i = rel.lastIndexOf('/'); i > 0; i = rel.lastIndexOf('/', i - 1)) {
|
|
1500
|
+
const dir = rel.slice(0, i)
|
|
1501
|
+
if (dirs.has(dir)) break
|
|
1502
|
+
dirs.add(dir)
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
listedDirsMemo.set(listed, dirs)
|
|
1506
|
+
return dirs
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
function reaches(prefixes: readonly string[], rel: string): boolean {
|
|
1510
|
+
return prefixes.some(
|
|
1511
|
+
(p) => p === '' || rel === p || rel.startsWith(`${p}/`) || p.startsWith(`${rel}/`),
|
|
1512
|
+
)
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
/**
|
|
1516
|
+
* An input file that exists now and that the key did not fold, or undefined.
|
|
1517
|
+
* The facts re-check only the files the key folded, so a file ADDED under
|
|
1518
|
+
* an input glob while the command ran — and read by it — was saved under a
|
|
1519
|
+
* key without it, and replayed once the file was gone.
|
|
1520
|
+
*
|
|
1521
|
+
* Adding a name changes its directory's ctime, so the listing's directories
|
|
1522
|
+
* that a positive entry reaches are stat'ed, and only one changed since
|
|
1523
|
+
* `listedAt` (the listing's time, less the racy window) is read. A name
|
|
1524
|
+
* there that the key lacks and the declaration matches, or an unlisted
|
|
1525
|
+
* directory, is asked of git, which alone knows what is ignored; so is a
|
|
1526
|
+
* missing literal that now exists. Not seen: a file added inside a
|
|
1527
|
+
* directory that held no listed file before (only ignored ones, or none)
|
|
1528
|
+
* and was not itself created during the run — its parent's ctime stays.
|
|
1529
|
+
*/
|
|
1530
|
+
export function addedInput(
|
|
1531
|
+
listings: readonly InputListing[],
|
|
1532
|
+
keyFiles: ReadonlySet<string>,
|
|
1533
|
+
listedAt: number,
|
|
1534
|
+
): string | undefined {
|
|
1535
|
+
for (const l of listings) {
|
|
1536
|
+
const found = addedTo(l, keyFiles, listedAt)
|
|
1537
|
+
if (found !== undefined) return found
|
|
1538
|
+
}
|
|
1539
|
+
return undefined
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
function addedTo(
|
|
1543
|
+
l: InputListing,
|
|
1544
|
+
keyFiles: ReadonlySet<string>,
|
|
1545
|
+
listedAt: number,
|
|
1546
|
+
): string | undefined {
|
|
1547
|
+
const candidates: string[] = []
|
|
1548
|
+
for (const lit of l.literals) {
|
|
1549
|
+
if (l.nested(lit) || keyFiles.has(path.resolve(l.root, lit))) continue
|
|
1550
|
+
if (existsOnDisk(path.resolve(l.root, lit))) candidates.push(lit)
|
|
1551
|
+
}
|
|
1552
|
+
const known = listedDirs(l.listed)
|
|
1553
|
+
for (const dir of known) {
|
|
1554
|
+
if (!reaches(l.prefixes, dir) || (dir !== '' && l.nested(`${dir}/`))) continue
|
|
1555
|
+
const abs = dir === '' ? l.root : path.resolve(l.root, dir)
|
|
1556
|
+
let entries
|
|
1557
|
+
try {
|
|
1558
|
+
const st = lstatSync(abs, { throwIfNoEntry: false })
|
|
1559
|
+
if (st === undefined) continue
|
|
1560
|
+
if (st.ctimeMs < listedAt - racyWindowMs(st.ctimeMs, FILE_HASH_RACY_MS)) continue
|
|
1561
|
+
entries = readdirSync(abs, { withFileTypes: true })
|
|
1562
|
+
} catch {
|
|
1563
|
+
// Unreadable here: git answers for the whole directory instead.
|
|
1564
|
+
candidates.push(dir)
|
|
1565
|
+
continue
|
|
1566
|
+
}
|
|
1567
|
+
for (const e of entries) {
|
|
1568
|
+
const rel = dir === '' ? e.name : `${dir}/${e.name}`
|
|
1569
|
+
if (e.isDirectory()) {
|
|
1570
|
+
if (known.has(rel) || e.name === 'node_modules' || l.nested(`${rel}/`)) continue
|
|
1571
|
+
if (reaches(l.prefixes, rel)) candidates.push(rel)
|
|
1572
|
+
} else if (
|
|
1573
|
+
l.isInput(rel) &&
|
|
1574
|
+
!l.nested(rel) &&
|
|
1575
|
+
!isInstalledPath(rel) &&
|
|
1576
|
+
!keyFiles.has(path.resolve(abs, e.name))
|
|
1577
|
+
) {
|
|
1578
|
+
candidates.push(rel)
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
if (candidates.length === 0) return undefined
|
|
1583
|
+
for (const rel of runGitLsFiles(l.root).files) {
|
|
1584
|
+
if (!candidates.some((c) => c === '' || rel === c || rel.startsWith(`${c}/`))) continue
|
|
1585
|
+
if (isInstalledPath(rel) || l.nested(rel) || !l.isInput(rel)) continue
|
|
1586
|
+
const abs = path.resolve(l.root, rel)
|
|
1587
|
+
if (!keyFiles.has(abs) && isInputOnDisk(abs)) return abs
|
|
1588
|
+
}
|
|
1589
|
+
return undefined
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1420
1592
|
const FATAL_UTF8 = new TextDecoder('utf-8', { fatal: true })
|
|
1421
1593
|
|
|
1422
1594
|
/**
|
|
@@ -1330,14 +1330,14 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1330
1330
|
}
|
|
1331
1331
|
|
|
1332
1332
|
/**
|
|
1333
|
-
* The input that no longer holds what the key folded
|
|
1334
|
-
*
|
|
1335
|
-
* unnamed), undefined when none
|
|
1333
|
+
* The input that no longer holds what the key folded, or one the key did
|
|
1334
|
+
* not fold that is there now: `null` when the key re-derived just before
|
|
1335
|
+
* the command already differed (the file unnamed), undefined when none
|
|
1336
|
+
* moved.
|
|
1336
1337
|
*/
|
|
1337
1338
|
async function movedSinceKey(): Promise<string | null | undefined> {
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
: await movedInput(described!.facts, cache, described!.describedAt)
|
|
1339
|
+
if (described!.hash !== hash) return null
|
|
1340
|
+
return (await movedInput(described!.facts, cache, described!.describedAt)) ?? described!.added()
|
|
1341
1341
|
}
|
|
1342
1342
|
|
|
1343
1343
|
/**
|
|
@@ -5,6 +5,8 @@ import type { ProjectFilesCache, WorkspaceFilesCache } from '../cache/index.js'
|
|
|
5
5
|
import {
|
|
6
6
|
ABSENT_INPUT,
|
|
7
7
|
absentOr,
|
|
8
|
+
addedInput,
|
|
9
|
+
type InputListing,
|
|
8
10
|
type CacheKeyInput,
|
|
9
11
|
type CacheLayer,
|
|
10
12
|
FILE_HASH_RACY_MS,
|
|
@@ -111,7 +113,7 @@ export interface ComputeHashArgs {
|
|
|
111
113
|
export async function computeTaskHash(args: ComputeHashArgs): Promise<string> {
|
|
112
114
|
const end = span('task hash')
|
|
113
115
|
try {
|
|
114
|
-
return await args.cache.key(await resolveKeyInput(args))
|
|
116
|
+
return await args.cache.key((await resolveKeyInput(args)).input)
|
|
115
117
|
} finally {
|
|
116
118
|
end()
|
|
117
119
|
}
|
|
@@ -138,11 +140,16 @@ export interface InputFact {
|
|
|
138
140
|
* same sources as the fold: the index-OID map for clean tracked files,
|
|
139
141
|
* else `hashFile`'s stat memo (already warm after `key()`).
|
|
140
142
|
*/
|
|
141
|
-
export async function describeTaskInputs(
|
|
142
|
-
|
|
143
|
-
|
|
143
|
+
export async function describeTaskInputs(args: ComputeHashArgs): Promise<{
|
|
144
|
+
hash: string
|
|
145
|
+
inputs: TaskInputs
|
|
146
|
+
facts: InputFact[]
|
|
147
|
+
describedAt: number
|
|
148
|
+
/** An input file the key did not fold that exists now (see `addedInput`). */
|
|
149
|
+
added: () => string | undefined
|
|
150
|
+
}> {
|
|
144
151
|
const hashedAt = Date.now()
|
|
145
|
-
const input = await resolveKeyInput(args)
|
|
152
|
+
const { input, listings } = await resolveKeyInput(args)
|
|
146
153
|
const hash = await args.cache.key(input)
|
|
147
154
|
const sorted = [...input.inputFiles].sort()
|
|
148
155
|
const digests = await Promise.all(
|
|
@@ -194,10 +201,14 @@ export async function describeTaskInputs(
|
|
|
194
201
|
})
|
|
195
202
|
}
|
|
196
203
|
let upstream: TaskInputs['upstream'] | undefined
|
|
204
|
+
// A listing from the run's enumeration is as old as it; one listed with
|
|
205
|
+
// no run cache was listed during this describe.
|
|
206
|
+
const listedAt = args.gitFilesCache === undefined ? hashedAt : indexedAt
|
|
197
207
|
return {
|
|
198
208
|
hash,
|
|
199
209
|
facts,
|
|
200
210
|
describedAt: hashedAt,
|
|
211
|
+
added: () => addedInput(listings, new Set(input.inputFiles), listedAt),
|
|
201
212
|
inputs: {
|
|
202
213
|
files,
|
|
203
214
|
env: input.envValues.map(([name, value]) => ({ name, value })),
|
|
@@ -260,7 +271,9 @@ export async function movedInput(
|
|
|
260
271
|
return undefined
|
|
261
272
|
}
|
|
262
273
|
|
|
263
|
-
async function resolveKeyInput(
|
|
274
|
+
async function resolveKeyInput(
|
|
275
|
+
args: ComputeHashArgs,
|
|
276
|
+
): Promise<{ input: CacheKeyInput; listings: InputListing[] }> {
|
|
264
277
|
const cfg = args.node.config
|
|
265
278
|
const cacheCfg: CacheConfig | undefined = cfg.cache
|
|
266
279
|
const outputs = cacheCfg?.outputs.files ?? []
|
|
@@ -327,7 +340,7 @@ async function resolveKeyInput(args: ComputeHashArgs): Promise<CacheKeyInput> {
|
|
|
327
340
|
|
|
328
341
|
const effectiveForwardArgs = args.node.requested ? (args.forwardArgs ?? []) : []
|
|
329
342
|
|
|
330
|
-
|
|
343
|
+
const input: CacheKeyInput = {
|
|
331
344
|
taskId: args.node.id,
|
|
332
345
|
taskConfigHash,
|
|
333
346
|
projectPackageJsonHash,
|
|
@@ -345,6 +358,7 @@ async function resolveKeyInput(args: ComputeHashArgs): Promise<CacheKeyInput> {
|
|
|
345
358
|
...(args.node.keyParts !== undefined ? { pluginParts: args.node.keyParts } : {}),
|
|
346
359
|
...(args.captureInto !== undefined ? { captureInto: args.captureInto } : {}),
|
|
347
360
|
}
|
|
361
|
+
return { input, listings: resolved.listings }
|
|
348
362
|
}
|
|
349
363
|
|
|
350
364
|
/**
|