@visulima/task-runner 1.0.0-alpha.5 → 1.0.0-alpha.7
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/CHANGELOG.md +48 -0
- package/dist/archive.d.ts +38 -0
- package/dist/cache.d.ts +31 -3
- package/dist/chrome-trace.d.ts +53 -0
- package/dist/file-access-tracker.d.ts +7 -1
- package/dist/fingerprint.d.ts +9 -0
- package/dist/incremental-hasher.d.ts +18 -0
- package/dist/index.d.ts +8 -3
- package/dist/index.js +22 -19
- package/dist/life-cycle.d.ts +2 -0
- package/dist/log-reporter.d.ts +34 -0
- package/dist/output-resolver.d.ts +20 -0
- package/dist/packem_shared/{Cache-iAjRMV2d.js → Cache-CWaX_c8U.js} +135 -45
- package/dist/packem_shared/{CompositeLifeCycle-7AtYw1dv.js → CompositeLifeCycle-CSVbRC_5.js} +10 -0
- package/dist/packem_shared/{FileAccessTracker-CrtBAt5D.js → FileAccessTracker-CQ5Ot7Hd.js} +68 -16
- package/dist/packem_shared/{FingerprintManager-Cu-ta9ee.js → FingerprintManager-CV7U4f4f.js} +22 -1
- package/dist/packem_shared/{IncrementalFileHasher-Cm_kJY5V.js → IncrementalFileHasher-BRS76-mb.js} +26 -0
- package/dist/packem_shared/LogReporter-BDt52HLu.js +44 -0
- package/dist/packem_shared/{RemoteCache-BFceSe4a.js → RemoteCache-DSU3lc87.js} +77 -37
- package/dist/packem_shared/{TaskOrchestrator-lLn-PH1m.js → TaskOrchestrator-rf45vW5c.js} +94 -15
- package/dist/packem_shared/{TerminalBuffer-CnPyFgPB.js → TerminalBuffer-qVJvbRQZ.js} +1 -1
- package/dist/packem_shared/{TrackedTaskExecutor-BGUKFE-7.js → TrackedTaskExecutor-CFPpQfXF.js} +1 -1
- package/dist/packem_shared/archive-UQHAnZUa.js +102 -0
- package/dist/packem_shared/{buildForwardDependencyMap-0BJFMMPv.js → buildForwardDependencyMap-DLPgKEto.js} +2 -1
- package/dist/packem_shared/{computeTaskHash-B2SVZqgp.js → computeTaskHash-DYqfrDGq.js} +122 -6
- package/dist/packem_shared/{createTaskGraph-CcsFaSrz.js → createTaskGraph-B7nH0kY_.js} +2 -2
- package/dist/packem_shared/{defaultTaskRunner-BdFTifsh.js → defaultTaskRunner-Cp7jCmIl.js} +28 -6
- package/dist/packem_shared/{extractPackageName-CbVNW-dr.js → extractPackageName-BllKetnz.js} +2 -1
- package/dist/packem_shared/{generateRunSummary-qn-_jKwt.js → generateRunSummary-BE1jnQ3H.js} +19 -1
- package/dist/packem_shared/{parsePartition-C4-P5RjK.js → parsePartition-BfLbHGAx.js} +18 -0
- package/dist/packem_shared/{projectGraphToDot-C8uYeaPo.js → projectGraphToDot-DU1lSe-c.js} +1 -1
- package/dist/packem_shared/resolveOutputs-n6MCKoTe.js +111 -0
- package/dist/packem_shared/{runConcurrentFallback-CGHz_f-Q.js → runConcurrentFallback-BTmgGV1H.js} +1 -1
- package/dist/packem_shared/{runConcurrently-qrkWyzXW.js → runConcurrently-CmfC4r-f.js} +1 -1
- package/dist/packem_shared/toChromeTrace-B2tZoJ-7.js +121 -0
- package/dist/remote-cache.d.ts +45 -0
- package/dist/run-summary.d.ts +26 -4
- package/dist/task-hasher.d.ts +37 -0
- package/dist/task-orchestrator.d.ts +2 -2
- package/dist/types.d.ts +137 -3
- package/index.js +52 -52
- package/package.json +13 -13
|
@@ -22,17 +22,30 @@ const {
|
|
|
22
22
|
mkdir,
|
|
23
23
|
writeFile,
|
|
24
24
|
rename,
|
|
25
|
-
stat,
|
|
26
25
|
rm,
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
readdir,
|
|
27
|
+
stat,
|
|
28
|
+
cp
|
|
29
29
|
} = __cjs_getBuiltinModule("node:fs/promises");
|
|
30
30
|
import { formatBytes, parseBytes } from '@visulima/humanizer';
|
|
31
31
|
import { join, resolve, dirname } from '@visulima/path';
|
|
32
|
+
import { e as extractTarBrotli, c as createTarBrotli } from './archive-UQHAnZUa.js';
|
|
33
|
+
import { resolveOutputs } from './resolveOutputs-n6MCKoTe.js';
|
|
32
34
|
import { u as uniqueId } from './utils-zO0ZRgtf.js';
|
|
33
35
|
|
|
34
36
|
const DEFAULT_MAX_CACHE_AGE = 7 * 24 * 60 * 60 * 1e3;
|
|
35
37
|
const DEFAULT_CACHE_DIRECTORY_NAME = ".task-runner-cache";
|
|
38
|
+
const assertSafeNamespace = (namespace) => {
|
|
39
|
+
if (namespace.includes("\0")) {
|
|
40
|
+
throw new Error("cacheNamespace: null bytes are not allowed.");
|
|
41
|
+
}
|
|
42
|
+
if (namespace.includes("/") || namespace.includes("\\")) {
|
|
43
|
+
throw new Error(`cacheNamespace: path separators are not allowed (received ${JSON.stringify(namespace)}).`);
|
|
44
|
+
}
|
|
45
|
+
if (namespace === "." || namespace === "..") {
|
|
46
|
+
throw new Error(`cacheNamespace: "${namespace}" would escape the cache subtree.`);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
36
49
|
const removeEntry = async (entryPath) => {
|
|
37
50
|
try {
|
|
38
51
|
await rm(entryPath, { force: true, recursive: true });
|
|
@@ -73,7 +86,11 @@ class Cache {
|
|
|
73
86
|
#indexWriteQueue = Promise.resolve();
|
|
74
87
|
constructor(options) {
|
|
75
88
|
this.#workspaceRoot = options.workspaceRoot;
|
|
76
|
-
|
|
89
|
+
const baseDirectory = options.cacheDirectory ?? join(options.workspaceRoot, DEFAULT_CACHE_DIRECTORY_NAME);
|
|
90
|
+
if (options.cacheNamespace !== void 0 && options.cacheNamespace.length > 0) {
|
|
91
|
+
assertSafeNamespace(options.cacheNamespace);
|
|
92
|
+
}
|
|
93
|
+
this.#cacheDirectory = options.cacheNamespace && options.cacheNamespace.length > 0 ? join(baseDirectory, "ns", options.cacheNamespace) : baseDirectory;
|
|
77
94
|
this.#maxCacheAge = options.maxCacheAge ?? DEFAULT_MAX_CACHE_AGE;
|
|
78
95
|
this.#maxCacheSize = options.maxCacheSize ? parseCacheSize(options.maxCacheSize) : void 0;
|
|
79
96
|
}
|
|
@@ -117,8 +134,14 @@ class Cache {
|
|
|
117
134
|
*
|
|
118
135
|
* Uses atomic write: builds the entry in a temporary directory,
|
|
119
136
|
* then renames into place to avoid partial reads by concurrent processes.
|
|
137
|
+
*
|
|
138
|
+
* `outputs` accepts the richer `OutputSpec[]` shape — glob
|
|
139
|
+
* patterns, negative globs, and `{ auto: true }` entries. Pass
|
|
140
|
+
* `autoWrites` alongside `{ auto: true }` so the resolver knows
|
|
141
|
+
* which files the task actually wrote; otherwise auto entries
|
|
142
|
+
* contribute nothing.
|
|
120
143
|
*/
|
|
121
|
-
async put(hash, terminalOutput, outputs, code, fingerprint) {
|
|
144
|
+
async put(hash, terminalOutput, outputs, code, fingerprint, autoWrites) {
|
|
122
145
|
const cacheEntryDirectory = join(this.#cacheDirectory, hash);
|
|
123
146
|
const temporaryDirectory = join(this.#cacheDirectory, `.tmp-${hash}-${uniqueId()}`);
|
|
124
147
|
try {
|
|
@@ -126,7 +149,7 @@ class Cache {
|
|
|
126
149
|
const writes = [
|
|
127
150
|
writeFile(join(temporaryDirectory, "code"), String(code)),
|
|
128
151
|
writeFile(join(temporaryDirectory, "terminalOutput"), terminalOutput),
|
|
129
|
-
this.#archiveOutputs(temporaryDirectory, outputs)
|
|
152
|
+
this.#archiveOutputs(temporaryDirectory, outputs, autoWrites)
|
|
130
153
|
];
|
|
131
154
|
if (fingerprint) {
|
|
132
155
|
writes.push(writeFile(join(temporaryDirectory, "fingerprint.json"), JSON.stringify(fingerprint)));
|
|
@@ -140,36 +163,21 @@ class Cache {
|
|
|
140
163
|
}
|
|
141
164
|
}
|
|
142
165
|
/**
|
|
143
|
-
* Restores cached outputs
|
|
166
|
+
* Restores cached outputs from the compressed `outputs.tar.br`
|
|
167
|
+
* archive. Returns `true` either when the archive was extracted
|
|
168
|
+
* successfully OR when the entry simply has no outputs to restore.
|
|
169
|
+
*
|
|
170
|
+
* The restore flow stages into a temp directory, then swaps each
|
|
171
|
+
* top-level entry into place (see {@link restoreOutputsCompressed})
|
|
172
|
+
* so a mid-restore failure never destroys the user's working tree.
|
|
173
|
+
* The `outputs` parameter is no longer consulted at restore time —
|
|
174
|
+
* the archive is authoritative, and top-level entries in the
|
|
175
|
+
* extracted staging become the set of swap roots. Still accepted
|
|
176
|
+
* for backward compat.
|
|
144
177
|
*/
|
|
145
|
-
async restoreOutputs(hash,
|
|
178
|
+
async restoreOutputs(hash, _outputs) {
|
|
146
179
|
const cacheEntryDirectory = join(this.#cacheDirectory, hash);
|
|
147
|
-
|
|
148
|
-
try {
|
|
149
|
-
await stat(outputsDirectory);
|
|
150
|
-
} catch {
|
|
151
|
-
return true;
|
|
152
|
-
}
|
|
153
|
-
try {
|
|
154
|
-
await Promise.all(
|
|
155
|
-
outputs.map(async (output) => {
|
|
156
|
-
const absoluteOutput = resolve(this.#workspaceRoot, output);
|
|
157
|
-
const cachedOutput = join(outputsDirectory, output);
|
|
158
|
-
try {
|
|
159
|
-
await stat(cachedOutput);
|
|
160
|
-
} catch {
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
const parentDirectory = dirname(absoluteOutput);
|
|
164
|
-
await mkdir(parentDirectory, { recursive: true });
|
|
165
|
-
await rm(absoluteOutput, { force: true, recursive: true });
|
|
166
|
-
await cp(cachedOutput, absoluteOutput, { recursive: true });
|
|
167
|
-
})
|
|
168
|
-
);
|
|
169
|
-
return true;
|
|
170
|
-
} catch {
|
|
171
|
-
return false;
|
|
172
|
-
}
|
|
180
|
+
return restoreOutputsCompressed(cacheEntryDirectory, this.#workspaceRoot);
|
|
173
181
|
}
|
|
174
182
|
/**
|
|
175
183
|
* Retrieves the most recent cached result for a task by its ID.
|
|
@@ -276,23 +284,105 @@ class Cache {
|
|
|
276
284
|
}
|
|
277
285
|
}
|
|
278
286
|
/**
|
|
279
|
-
* Archives task output files into the cache
|
|
287
|
+
* Archives task output files into the cache as a single
|
|
288
|
+
* brotli-compressed tarball (`outputs.tar.br`). See
|
|
289
|
+
* {@link archiveOutputsCompressed} for the staging + compression
|
|
290
|
+
* flow.
|
|
280
291
|
*/
|
|
281
|
-
async #archiveOutputs(cacheEntryDirectory, outputs) {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
292
|
+
async #archiveOutputs(cacheEntryDirectory, outputs, autoWrites) {
|
|
293
|
+
await archiveOutputsCompressed(this.#workspaceRoot, cacheEntryDirectory, outputs, autoWrites);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const OUTPUTS_ARCHIVE_FILENAME = "outputs.tar.br";
|
|
297
|
+
const archiveOutputsCompressed = async (workspaceRoot, cacheEntryDirectory, outputs, autoWrites) => {
|
|
298
|
+
if (outputs.length === 0) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const resolvedPaths = await resolveOutputs(workspaceRoot, outputs, autoWrites);
|
|
302
|
+
if (resolvedPaths.length === 0) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const stagingDirectory = join(cacheEntryDirectory, `.outputs-stage-${uniqueId()}`);
|
|
306
|
+
const finalPath = join(cacheEntryDirectory, OUTPUTS_ARCHIVE_FILENAME);
|
|
307
|
+
try {
|
|
308
|
+
await mkdir(stagingDirectory, { recursive: true });
|
|
309
|
+
const stagedFlags = await runBounded(STAGE_CONCURRENCY, resolvedPaths, async (relativePath) => {
|
|
310
|
+
const absoluteOutput = resolve(workspaceRoot, relativePath);
|
|
311
|
+
const stagedOutput = join(stagingDirectory, relativePath);
|
|
312
|
+
try {
|
|
313
|
+
await mkdir(dirname(stagedOutput), { recursive: true });
|
|
314
|
+
await cp(absoluteOutput, stagedOutput, { recursive: true });
|
|
315
|
+
return true;
|
|
316
|
+
} catch {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
const stagedCount = stagedFlags.filter(Boolean).length;
|
|
321
|
+
if (stagedCount === 0) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
await createTarBrotli(stagingDirectory, finalPath);
|
|
325
|
+
} finally {
|
|
326
|
+
await rm(stagingDirectory, { force: true, recursive: true }).catch(() => {
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
const STAGE_CONCURRENCY = 16;
|
|
331
|
+
const runBounded = async (limit, items, fn) => {
|
|
332
|
+
const results = Array.from({ length: items.length });
|
|
333
|
+
let cursor = 0;
|
|
334
|
+
const worker = async () => {
|
|
335
|
+
while (cursor < items.length) {
|
|
336
|
+
const index = cursor;
|
|
337
|
+
cursor += 1;
|
|
338
|
+
results[index] = await fn(items[index], index);
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker());
|
|
342
|
+
await Promise.all(workers);
|
|
343
|
+
return results;
|
|
344
|
+
};
|
|
345
|
+
const restoreOutputsCompressed = async (cacheEntryDirectory, workspaceRoot) => {
|
|
346
|
+
const archivePath = join(cacheEntryDirectory, OUTPUTS_ARCHIVE_FILENAME);
|
|
347
|
+
try {
|
|
348
|
+
await stat(archivePath);
|
|
349
|
+
} catch {
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
const stagingDirectory = join(cacheEntryDirectory, `.restore-${uniqueId()}`);
|
|
353
|
+
const backups = [];
|
|
354
|
+
try {
|
|
355
|
+
await mkdir(stagingDirectory, { recursive: true });
|
|
356
|
+
await extractTarBrotli(archivePath, stagingDirectory);
|
|
357
|
+
const topLevel = await readdir(stagingDirectory);
|
|
358
|
+
await runBounded(STAGE_CONCURRENCY, topLevel, async (entry) => {
|
|
359
|
+
const absoluteOutput = resolve(workspaceRoot, entry);
|
|
360
|
+
const stagedOutput = join(stagingDirectory, entry);
|
|
361
|
+
await mkdir(dirname(absoluteOutput), { recursive: true });
|
|
287
362
|
try {
|
|
288
363
|
await stat(absoluteOutput);
|
|
289
|
-
const
|
|
290
|
-
await
|
|
291
|
-
|
|
364
|
+
const backup = `${absoluteOutput}.pre-restore-${uniqueId()}`;
|
|
365
|
+
await rename(absoluteOutput, backup);
|
|
366
|
+
backups.push({ backup, original: absoluteOutput });
|
|
292
367
|
} catch {
|
|
293
368
|
}
|
|
369
|
+
await cp(stagedOutput, absoluteOutput, { recursive: true });
|
|
370
|
+
});
|
|
371
|
+
await Promise.all(backups.map(({ backup }) => rm(backup, { force: true, recursive: true }).catch(() => {
|
|
372
|
+
})));
|
|
373
|
+
return true;
|
|
374
|
+
} catch {
|
|
375
|
+
for (const { backup, original } of backups) {
|
|
376
|
+
await rm(original, { force: true, recursive: true }).catch(() => {
|
|
377
|
+
});
|
|
378
|
+
await rename(backup, original).catch(() => {
|
|
379
|
+
});
|
|
294
380
|
}
|
|
381
|
+
return false;
|
|
382
|
+
} finally {
|
|
383
|
+
await rm(stagingDirectory, { force: true, recursive: true }).catch(() => {
|
|
384
|
+
});
|
|
295
385
|
}
|
|
296
|
-
}
|
|
386
|
+
};
|
|
297
387
|
|
|
298
388
|
export { Cache, DEFAULT_CACHE_DIRECTORY_NAME, formatCacheSize, parseCacheSize };
|
package/dist/packem_shared/{CompositeLifeCycle-7AtYw1dv.js → CompositeLifeCycle-CSVbRC_5.js}
RENAMED
|
@@ -59,6 +59,16 @@ class CompositeLifeCycle {
|
|
|
59
59
|
lc.printCacheMiss?.(task, reasons);
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
+
onTaskStdout(task, chunk) {
|
|
63
|
+
for (const lc of this.#lifeCycles) {
|
|
64
|
+
lc.onTaskStdout?.(task, chunk);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
onTaskStderr(task, chunk) {
|
|
68
|
+
for (const lc of this.#lifeCycles) {
|
|
69
|
+
lc.onTaskStderr?.(task, chunk);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
62
72
|
}
|
|
63
73
|
class ConsoleLifeCycle {
|
|
64
74
|
#verbose;
|
|
@@ -47,11 +47,19 @@ const isStraceAvailable = () => {
|
|
|
47
47
|
return straceAvailable;
|
|
48
48
|
};
|
|
49
49
|
const STRACE_PATTERNS = [
|
|
50
|
-
{ pattern: /openat\(AT_FDCWD,\s*"([^"]+)"
|
|
51
|
-
{ pattern: /^(?:\d+\s+)?open\("([^"]+)"
|
|
52
|
-
{ pattern: /(
|
|
53
|
-
{ pattern: /
|
|
50
|
+
{ kind: "open", pattern: /openat\(AT_FDCWD,\s*"([^"]+)"/ },
|
|
51
|
+
{ kind: "open", pattern: /^(?:\d+\s+)?open\("([^"]+)"/ },
|
|
52
|
+
{ kind: "creat", pattern: /(?:^|\s)creat\("([^"]+)"/ },
|
|
53
|
+
{ kind: "stat", pattern: /(?:stat|lstat|newfstatat)\((?:AT_FDCWD,\s*)?"([^"]+)"/ },
|
|
54
|
+
{ kind: "stat", pattern: /access\("([^"]+)"/ },
|
|
55
|
+
{ kind: "getdents", pattern: /getdents(?:64)?\(\d+/ }
|
|
54
56
|
];
|
|
57
|
+
const parseOpenAccessType = (line) => {
|
|
58
|
+
if (/O_WRONLY|O_RDWR|O_CREAT|O_TRUNC|O_APPEND/.test(line)) {
|
|
59
|
+
return "write";
|
|
60
|
+
}
|
|
61
|
+
return "read";
|
|
62
|
+
};
|
|
55
63
|
class FileAccessTracker {
|
|
56
64
|
#workspaceRoot;
|
|
57
65
|
#excludePatterns;
|
|
@@ -93,7 +101,7 @@ class FileAccessTracker {
|
|
|
93
101
|
const traceDirectory = join(this.#workspaceRoot, "node_modules", ".cache", "task-runner");
|
|
94
102
|
await mkdir(traceDirectory, { recursive: true });
|
|
95
103
|
const traceFile = join(traceDirectory, `strace-${uniqueId()}.log`);
|
|
96
|
-
const straceCommand = `strace -f -qq -e trace=open,openat,stat,lstat,newfstatat,access,getdents,getdents64 -o ${traceFile} -- ${command}`;
|
|
104
|
+
const straceCommand = `strace -f -qq -e trace=open,openat,creat,stat,lstat,newfstatat,access,getdents,getdents64,unlink,unlinkat,rename,renameat,renameat2 -o ${traceFile} -- ${command}`;
|
|
97
105
|
return new Promise((_resolve) => {
|
|
98
106
|
const child = exec(
|
|
99
107
|
straceCommand,
|
|
@@ -125,14 +133,23 @@ class FileAccessTracker {
|
|
|
125
133
|
}
|
|
126
134
|
/**
|
|
127
135
|
* Parses strace output to extract file accesses.
|
|
136
|
+
*
|
|
137
|
+
* A single path may appear with both `read` and `write` types when a
|
|
138
|
+
* task reads a file and later rewrites it — self-modifying detection
|
|
139
|
+
* relies on seeing both, so we dedupe per `(path, type)` rather than
|
|
140
|
+
* by path alone.
|
|
128
141
|
*/
|
|
129
142
|
#parseStraceOutput(traceContent, cwd) {
|
|
130
143
|
const accesses = [];
|
|
131
|
-
const
|
|
144
|
+
const seen = /* @__PURE__ */ new Set();
|
|
132
145
|
for (const line of traceContent.split("\n")) {
|
|
133
146
|
const parsed = this.#parseStraceLine(line, cwd);
|
|
134
|
-
if (
|
|
135
|
-
|
|
147
|
+
if (!parsed) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const key = `${parsed.type}:${parsed.path}`;
|
|
151
|
+
if (!seen.has(key)) {
|
|
152
|
+
seen.add(key);
|
|
136
153
|
accesses.push(parsed);
|
|
137
154
|
}
|
|
138
155
|
}
|
|
@@ -140,23 +157,46 @@ class FileAccessTracker {
|
|
|
140
157
|
}
|
|
141
158
|
/**
|
|
142
159
|
* Parses a single strace output line.
|
|
143
|
-
* Each entry maps a regex to the file access
|
|
160
|
+
* Each entry maps a regex to the file access kind; the actual type
|
|
161
|
+
* (read vs write) depends on flag inspection for `open`/`openat`.
|
|
144
162
|
*/
|
|
145
163
|
#parseStraceLine(line, cwd) {
|
|
146
164
|
const isMissing = line.includes("ENOENT");
|
|
147
|
-
for (const {
|
|
165
|
+
for (const { kind, pattern } of STRACE_PATTERNS) {
|
|
148
166
|
const match = pattern.exec(line);
|
|
149
|
-
if (!match
|
|
167
|
+
if (!match) {
|
|
150
168
|
continue;
|
|
151
169
|
}
|
|
152
|
-
|
|
170
|
+
if (kind === "getdents") {
|
|
171
|
+
return void 0;
|
|
172
|
+
}
|
|
173
|
+
const capturedPath = match[1];
|
|
174
|
+
if (!capturedPath) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
let path = capturedPath;
|
|
153
178
|
if (!path.startsWith("/")) {
|
|
154
179
|
path = resolve(cwd, path);
|
|
155
180
|
}
|
|
156
181
|
if (this.#shouldExclude(path) || !path.startsWith(this.#workspaceRoot)) {
|
|
157
182
|
return void 0;
|
|
158
183
|
}
|
|
159
|
-
|
|
184
|
+
if (isMissing) {
|
|
185
|
+
return { path, type: "missing" };
|
|
186
|
+
}
|
|
187
|
+
const type = kind === "open" ? parseOpenAccessType(line) : kind === "creat" ? "write" : "stat";
|
|
188
|
+
return { path, type };
|
|
189
|
+
}
|
|
190
|
+
const destructive = /(?:^|\s)(?:unlink|unlinkat|rename|renameat|renameat2)\((?:AT_FDCWD,\s*)?"([^"]+)"/.exec(line);
|
|
191
|
+
if (destructive?.[1]) {
|
|
192
|
+
let path = destructive[1];
|
|
193
|
+
if (!path.startsWith("/")) {
|
|
194
|
+
path = resolve(cwd, path);
|
|
195
|
+
}
|
|
196
|
+
if (this.#shouldExclude(path) || !path.startsWith(this.#workspaceRoot)) {
|
|
197
|
+
return void 0;
|
|
198
|
+
}
|
|
199
|
+
return { path, type: "write" };
|
|
160
200
|
}
|
|
161
201
|
return void 0;
|
|
162
202
|
}
|
|
@@ -220,19 +260,31 @@ const patch = (obj, method, type) => {
|
|
|
220
260
|
};
|
|
221
261
|
};
|
|
222
262
|
|
|
223
|
-
//
|
|
263
|
+
// Reads / stats / directory listing
|
|
224
264
|
patch(fs, "readFileSync", "read");
|
|
225
265
|
patch(fs, "statSync", "stat");
|
|
226
266
|
patch(fs, "readdirSync", "readdir");
|
|
227
|
-
// Async (callback)
|
|
228
267
|
patch(fs, "readFile", "read");
|
|
229
268
|
patch(fs, "stat", "stat");
|
|
230
269
|
patch(fs, "readdir", "readdir");
|
|
231
|
-
// Async (promises)
|
|
232
270
|
patch(fsp, "readFile", "read");
|
|
233
271
|
patch(fsp, "stat", "stat");
|
|
234
272
|
patch(fsp, "readdir", "readdir");
|
|
235
273
|
|
|
274
|
+
// Writes and path-mutating ops — needed for self-modifying task detection
|
|
275
|
+
patch(fs, "writeFileSync", "write");
|
|
276
|
+
patch(fs, "appendFileSync", "write");
|
|
277
|
+
patch(fs, "unlinkSync", "write");
|
|
278
|
+
patch(fs, "renameSync", "write");
|
|
279
|
+
patch(fs, "writeFile", "write");
|
|
280
|
+
patch(fs, "appendFile", "write");
|
|
281
|
+
patch(fs, "unlink", "write");
|
|
282
|
+
patch(fs, "rename", "write");
|
|
283
|
+
patch(fsp, "writeFile", "write");
|
|
284
|
+
patch(fsp, "appendFile", "write");
|
|
285
|
+
patch(fsp, "unlink", "write");
|
|
286
|
+
patch(fsp, "rename", "write");
|
|
287
|
+
|
|
236
288
|
process.on("beforeExit", () => { logStream.end(); });
|
|
237
289
|
`;
|
|
238
290
|
|
package/dist/packem_shared/{FingerprintManager-Cu-ta9ee.js → FingerprintManager-CV7U4f4f.js}
RENAMED
|
@@ -34,6 +34,8 @@ class FingerprintManager {
|
|
|
34
34
|
const fileHashes = {};
|
|
35
35
|
const missingPaths = /* @__PURE__ */ new Set();
|
|
36
36
|
const directoryListings = {};
|
|
37
|
+
const readPaths = /* @__PURE__ */ new Set();
|
|
38
|
+
const writePaths = /* @__PURE__ */ new Set();
|
|
37
39
|
for (const access of accesses) {
|
|
38
40
|
const relativePath = relative(this.#workspaceRoot, access.path);
|
|
39
41
|
switch (access.type) {
|
|
@@ -43,6 +45,7 @@ class FingerprintManager {
|
|
|
43
45
|
}
|
|
44
46
|
case "read":
|
|
45
47
|
case "stat": {
|
|
48
|
+
readPaths.add(relativePath);
|
|
46
49
|
if (!fileHashes[relativePath]) {
|
|
47
50
|
const hash = await this.#hashFileWithCache(access.path);
|
|
48
51
|
if (hash) {
|
|
@@ -62,8 +65,19 @@ class FingerprintManager {
|
|
|
62
65
|
}
|
|
63
66
|
break;
|
|
64
67
|
}
|
|
68
|
+
case "write": {
|
|
69
|
+
writePaths.add(relativePath);
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const modifiedInputs = [];
|
|
75
|
+
for (const path of writePaths) {
|
|
76
|
+
if (readPaths.has(path)) {
|
|
77
|
+
modifiedInputs.push(path);
|
|
65
78
|
}
|
|
66
79
|
}
|
|
80
|
+
modifiedInputs.sort();
|
|
67
81
|
const commandHash = hashStrings(`${command}:${JSON.stringify(sortObjectKeys(args))}`);
|
|
68
82
|
const envHashes = {};
|
|
69
83
|
const matchedEnvVariables = FingerprintManager.#resolveEnvPatterns(envPatterns, envVariables);
|
|
@@ -75,7 +89,14 @@ class FingerprintManager {
|
|
|
75
89
|
envHashes[key] = hashStrings(`${key}=${value ?? ""}`);
|
|
76
90
|
}
|
|
77
91
|
const missingFiles = [...missingPaths].toSorted();
|
|
78
|
-
return {
|
|
92
|
+
return {
|
|
93
|
+
commandHash,
|
|
94
|
+
directoryListings,
|
|
95
|
+
envHashes,
|
|
96
|
+
fileHashes,
|
|
97
|
+
missingFiles,
|
|
98
|
+
...modifiedInputs.length > 0 ? { modifiedInputs } : {}
|
|
99
|
+
};
|
|
79
100
|
}
|
|
80
101
|
/**
|
|
81
102
|
* Validates a stored fingerprint against the current state.
|
package/dist/packem_shared/{IncrementalFileHasher-Cm_kJY5V.js → IncrementalFileHasher-BRS76-mb.js}
RENAMED
|
@@ -146,6 +146,32 @@ class IncrementalFileHasher {
|
|
|
146
146
|
clear() {
|
|
147
147
|
this.#snapshot.clear();
|
|
148
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Looks up the cached hash for `absolutePath` when its mtime and
|
|
151
|
+
* size match the snapshot; returns `undefined` otherwise. Callers
|
|
152
|
+
* that already have a `stat` result (e.g. `InProcessTaskHasher`)
|
|
153
|
+
* skip an extra syscall by passing it through directly.
|
|
154
|
+
*
|
|
155
|
+
* The snapshot is considered loaded on first call — lazy-load is
|
|
156
|
+
* synchronous-friendly here because the caller already performed
|
|
157
|
+
* an async `stat` before calling this method.
|
|
158
|
+
*/
|
|
159
|
+
getSnapshotHash(absolutePath, mtimeMs, size) {
|
|
160
|
+
const entry = this.#snapshot.get(absolutePath);
|
|
161
|
+
if (!entry) {
|
|
162
|
+
return void 0;
|
|
163
|
+
}
|
|
164
|
+
return entry.mtimeMs === mtimeMs && entry.size === size ? entry.hash : void 0;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Writes a fresh snapshot entry after the caller has computed the
|
|
168
|
+
* hash. Pairs with {@link IncrementalFileHasher.getSnapshotHash}
|
|
169
|
+
* — after a miss, the caller hashes the file and records the
|
|
170
|
+
* result here so the next run can reuse it.
|
|
171
|
+
*/
|
|
172
|
+
recordSnapshot(absolutePath, hash, mtimeMs, size) {
|
|
173
|
+
this.#snapshot.set(absolutePath, { hash, mtimeMs, size });
|
|
174
|
+
}
|
|
149
175
|
}
|
|
150
176
|
|
|
151
177
|
export { IncrementalFileHasher };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const formatTaskLabel = (task) => `${task.target.project}#${task.target.target}`;
|
|
2
|
+
const prefixLines = (text, label) => {
|
|
3
|
+
const trailing = text.endsWith("\n") ? "\n" : "";
|
|
4
|
+
const body = trailing ? text.slice(0, -1) : text;
|
|
5
|
+
if (body.length === 0) {
|
|
6
|
+
return trailing;
|
|
7
|
+
}
|
|
8
|
+
return `${body.split("\n").map((line) => `[${label}] ${line}`).join("\n")}${trailing}`;
|
|
9
|
+
};
|
|
10
|
+
class LogReporter {
|
|
11
|
+
#mode;
|
|
12
|
+
#write;
|
|
13
|
+
constructor(mode, write = (chunk) => process.stdout.write(chunk)) {
|
|
14
|
+
this.#mode = mode;
|
|
15
|
+
this.#write = write;
|
|
16
|
+
}
|
|
17
|
+
printTaskTerminalOutput(task, _status, terminalOutput) {
|
|
18
|
+
if (terminalOutput.length === 0) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (this.#mode === "interleaved") {
|
|
22
|
+
this.#write(terminalOutput.endsWith("\n") ? terminalOutput : `${terminalOutput}
|
|
23
|
+
`);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const label = formatTaskLabel(task);
|
|
27
|
+
if (this.#mode === "labeled") {
|
|
28
|
+
this.#write(prefixLines(terminalOutput.endsWith("\n") ? terminalOutput : `${terminalOutput}
|
|
29
|
+
`, label));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const body = terminalOutput.endsWith("\n") ? terminalOutput : `${terminalOutput}
|
|
33
|
+
`;
|
|
34
|
+
this.#write(`── ${label} ──
|
|
35
|
+
${body}
|
|
36
|
+
`);
|
|
37
|
+
}
|
|
38
|
+
// eslint-disable-next-line class-methods-use-this
|
|
39
|
+
endTasks(_taskResults) {
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const createLogReporter = (mode, write) => new LogReporter(mode, write);
|
|
43
|
+
|
|
44
|
+
export { LogReporter, createLogReporter };
|