@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
|
@@ -18,40 +18,45 @@ const __cjs_getBuiltinModule = (module) => {
|
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
const {
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
createHmac,
|
|
22
|
+
timingSafeEqual
|
|
23
|
+
} = __cjs_getBuiltinModule("node:crypto");
|
|
23
24
|
const {
|
|
24
|
-
createWriteStream
|
|
25
|
+
createWriteStream,
|
|
26
|
+
createReadStream
|
|
25
27
|
} = __cjs_getBuiltinModule("node:fs");
|
|
26
28
|
const {
|
|
27
29
|
mkdir,
|
|
28
30
|
rm,
|
|
29
|
-
stat
|
|
30
|
-
readFile
|
|
31
|
+
stat
|
|
31
32
|
} = __cjs_getBuiltinModule("node:fs/promises");
|
|
32
33
|
const {
|
|
33
34
|
pipeline
|
|
34
35
|
} = __cjs_getBuiltinModule("node:stream/promises");
|
|
35
36
|
import { join } from '@visulima/path';
|
|
37
|
+
import { e as extractTarBrotli, a as extractTarGz, c as createTarBrotli, b as createTarGz } from './archive-UQHAnZUa.js';
|
|
36
38
|
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
39
|
+
const SIGNATURE_HEADER = "X-Artifact-Signature";
|
|
40
|
+
const MIN_SECRET_LENGTH = 16;
|
|
41
|
+
const computeArtifactSignatureStream = async (secret, hash, archivePath) => {
|
|
42
|
+
const hmac = createHmac("sha256", secret);
|
|
43
|
+
hmac.update(hash);
|
|
44
|
+
const source = createReadStream(archivePath);
|
|
45
|
+
for await (const chunk of source) {
|
|
46
|
+
hmac.update(chunk);
|
|
47
|
+
}
|
|
48
|
+
return hmac.digest("hex");
|
|
49
|
+
};
|
|
50
|
+
const signaturesMatch = (a, b) => {
|
|
51
|
+
if (a.length !== b.length) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
return timingSafeEqual(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
55
60
|
class RemoteCache {
|
|
56
61
|
#url;
|
|
57
62
|
#token;
|
|
@@ -60,6 +65,9 @@ class RemoteCache {
|
|
|
60
65
|
#read;
|
|
61
66
|
#write;
|
|
62
67
|
#onUploadError;
|
|
68
|
+
#compression;
|
|
69
|
+
#signingSecret;
|
|
70
|
+
#verifyOnDownload;
|
|
63
71
|
constructor(options) {
|
|
64
72
|
this.#url = options.url.replace(/\/$/, "");
|
|
65
73
|
this.#token = options.token;
|
|
@@ -68,6 +76,17 @@ class RemoteCache {
|
|
|
68
76
|
this.#read = options.read ?? true;
|
|
69
77
|
this.#write = options.write ?? true;
|
|
70
78
|
this.#onUploadError = options.onUploadError;
|
|
79
|
+
this.#compression = options.compression ?? "gzip";
|
|
80
|
+
if (options.signing) {
|
|
81
|
+
if (options.signing.secret.length < MIN_SECRET_LENGTH) {
|
|
82
|
+
throw new Error(`Remote cache signing secret must be at least ${String(MIN_SECRET_LENGTH)} characters.`);
|
|
83
|
+
}
|
|
84
|
+
this.#signingSecret = options.signing.secret;
|
|
85
|
+
this.#verifyOnDownload = options.signing.verifyOnDownload ?? false;
|
|
86
|
+
} else {
|
|
87
|
+
this.#signingSecret = void 0;
|
|
88
|
+
this.#verifyOnDownload = false;
|
|
89
|
+
}
|
|
71
90
|
}
|
|
72
91
|
/**
|
|
73
92
|
* Retrieves a cached artifact from the remote cache.
|
|
@@ -89,15 +108,23 @@ class RemoteCache {
|
|
|
89
108
|
if (!response.ok) {
|
|
90
109
|
return false;
|
|
91
110
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
111
|
+
const receivedSignature = this.#signingSecret ? response.headers.get(SIGNATURE_HEADER.toLowerCase()) ?? response.headers.get(SIGNATURE_HEADER) : null;
|
|
112
|
+
if (this.#signingSecret && !receivedSignature && this.#verifyOnDownload) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
if (!response.body) {
|
|
95
116
|
return false;
|
|
96
117
|
}
|
|
97
|
-
|
|
98
|
-
await pipeline(body,
|
|
118
|
+
await mkdir(localCacheDirectory, { recursive: true });
|
|
119
|
+
await pipeline(response.body, createWriteStream(archivePath));
|
|
120
|
+
if (this.#signingSecret && receivedSignature) {
|
|
121
|
+
const expected = await computeArtifactSignatureStream(this.#signingSecret, hash, archivePath);
|
|
122
|
+
if (!signaturesMatch(receivedSignature, expected)) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
99
126
|
await mkdir(entryDirectory, { recursive: true });
|
|
100
|
-
await extractTarGz(archivePath, entryDirectory);
|
|
127
|
+
await (this.#compression === "brotli" ? extractTarBrotli(archivePath, entryDirectory) : extractTarGz(archivePath, entryDirectory));
|
|
101
128
|
await rm(archivePath, { force: true });
|
|
102
129
|
return true;
|
|
103
130
|
} catch {
|
|
@@ -119,16 +146,29 @@ class RemoteCache {
|
|
|
119
146
|
const archivePath = join(localCacheDirectory, `.upload-${hash}.tar.gz`);
|
|
120
147
|
try {
|
|
121
148
|
await stat(join(entryDirectory, ".commit"));
|
|
122
|
-
await createTarGz(entryDirectory, archivePath);
|
|
149
|
+
await (this.#compression === "brotli" ? createTarBrotli(entryDirectory, archivePath) : createTarGz(entryDirectory, archivePath));
|
|
123
150
|
const artifactUrl = this.#buildUrl(`/v8/artifacts/${hash}`);
|
|
124
|
-
const
|
|
151
|
+
const { size } = await stat(archivePath);
|
|
152
|
+
const uploadHeaders = {
|
|
153
|
+
...this.#buildHeaders(),
|
|
154
|
+
"Content-Length": String(size),
|
|
155
|
+
// Advertise the compression format in a custom header so
|
|
156
|
+
// spec-compatible servers (and the matching download side)
|
|
157
|
+
// can branch if needed. The body is still an opaque blob
|
|
158
|
+
// from the server's perspective.
|
|
159
|
+
"Content-Type": "application/octet-stream",
|
|
160
|
+
"X-Artifact-Compression": this.#compression
|
|
161
|
+
};
|
|
162
|
+
if (this.#signingSecret) {
|
|
163
|
+
uploadHeaders[SIGNATURE_HEADER] = await computeArtifactSignatureStream(this.#signingSecret, hash, archivePath);
|
|
164
|
+
}
|
|
125
165
|
const response = await fetch(artifactUrl, {
|
|
126
|
-
body:
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
166
|
+
body: createReadStream(archivePath),
|
|
167
|
+
// @ts-expect-error — `duplex` is a Node-specific fetch
|
|
168
|
+
// option required when the body is a stream. The DOM
|
|
169
|
+
// `RequestInit` type doesn't include it yet.
|
|
170
|
+
duplex: "half",
|
|
171
|
+
headers: uploadHeaders,
|
|
132
172
|
method: "PUT",
|
|
133
173
|
signal: AbortSignal.timeout(this.#timeout)
|
|
134
174
|
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { d as resolveTaskCwd, a as createFailureResult, e as createXxh3Hasher } from './utils-zO0ZRgtf.js';
|
|
2
|
-
import { join } from '@visulima/path';
|
|
3
|
-
import { FingerprintManager } from './FingerprintManager-
|
|
4
|
-
import { generateRunSummary, writeRunSummary } from './generateRunSummary-
|
|
5
|
-
import { computeTaskHash } from './computeTaskHash-
|
|
6
|
-
import { TrackedTaskExecutor } from './TrackedTaskExecutor-
|
|
2
|
+
import { resolve, join } from '@visulima/path';
|
|
3
|
+
import { FingerprintManager } from './FingerprintManager-CV7U4f4f.js';
|
|
4
|
+
import { generateRunSummary, writeLastRunSummary, writeRunSummary } from './generateRunSummary-BE1jnQ3H.js';
|
|
5
|
+
import { computeTaskHash } from './computeTaskHash-DYqfrDGq.js';
|
|
6
|
+
import { TrackedTaskExecutor } from './TrackedTaskExecutor-CFPpQfXF.js';
|
|
7
7
|
|
|
8
8
|
const hashFingerprint = (fingerprint) => {
|
|
9
9
|
const hash = createXxh3Hasher();
|
|
@@ -26,11 +26,11 @@ const hashFingerprint = (fingerprint) => {
|
|
|
26
26
|
return hash.digest();
|
|
27
27
|
};
|
|
28
28
|
const createDeferred = () => {
|
|
29
|
-
let
|
|
29
|
+
let resolve2;
|
|
30
30
|
const promise = new Promise((r) => {
|
|
31
|
-
|
|
31
|
+
resolve2 = r;
|
|
32
32
|
});
|
|
33
|
-
return { promise, resolve };
|
|
33
|
+
return { promise, resolve: resolve2 };
|
|
34
34
|
};
|
|
35
35
|
class TaskOrchestrator {
|
|
36
36
|
#taskHasher;
|
|
@@ -101,9 +101,12 @@ class TaskOrchestrator {
|
|
|
101
101
|
process.removeListener("SIGTERM", signalHandler);
|
|
102
102
|
this.#lifeCycle.endCommand?.();
|
|
103
103
|
}
|
|
104
|
-
if (this.#
|
|
104
|
+
if (this.#taskGraph && !this.#aborted) {
|
|
105
105
|
const summary = generateRunSummary(this.#results, this.#taskGraph, this.#startTime);
|
|
106
|
-
await
|
|
106
|
+
await writeLastRunSummary(summary, this.#workspaceRoot);
|
|
107
|
+
if (this.#summarize) {
|
|
108
|
+
await writeRunSummary(summary, this.#workspaceRoot);
|
|
109
|
+
}
|
|
107
110
|
}
|
|
108
111
|
return this.#results;
|
|
109
112
|
}
|
|
@@ -243,7 +246,13 @@ class TaskOrchestrator {
|
|
|
243
246
|
};
|
|
244
247
|
this.#results.set(task.id, result);
|
|
245
248
|
if (code === 0 && task.cache !== false && task.hash) {
|
|
246
|
-
await this.#
|
|
249
|
+
const modified = await this.#detectSelfModifiedInputs(task);
|
|
250
|
+
if (modified.length > 0) {
|
|
251
|
+
result.selfModified = true;
|
|
252
|
+
this.#lifeCycle.printSelfModifyingSkip?.(task, modified);
|
|
253
|
+
} else {
|
|
254
|
+
await this.#cache.put(task.hash, terminalOutput, task.outputs, code);
|
|
255
|
+
}
|
|
247
256
|
}
|
|
248
257
|
return result;
|
|
249
258
|
} catch (error) {
|
|
@@ -252,6 +261,28 @@ class TaskOrchestrator {
|
|
|
252
261
|
return result;
|
|
253
262
|
}
|
|
254
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* Re-hashes every file recorded in `task.hashDetails.nodes` and returns
|
|
266
|
+
* the workspace-relative paths whose post-execution hash differs from the
|
|
267
|
+
* pre-execution hash. A non-empty result means the task wrote to its own
|
|
268
|
+
* tracked inputs and the cache entry would be unsafe to persist.
|
|
269
|
+
*/
|
|
270
|
+
async #detectSelfModifiedInputs(task) {
|
|
271
|
+
const nodes = task.hashDetails?.nodes;
|
|
272
|
+
if (!nodes || typeof this.#taskHasher.rehashFile !== "function") {
|
|
273
|
+
return [];
|
|
274
|
+
}
|
|
275
|
+
const rehash = this.#taskHasher.rehashFile.bind(this.#taskHasher);
|
|
276
|
+
const entries = Object.entries(nodes);
|
|
277
|
+
const checks = await Promise.all(
|
|
278
|
+
entries.map(async ([path, priorHash]) => {
|
|
279
|
+
const absolute = resolve(this.#workspaceRoot, path);
|
|
280
|
+
const fresh = await rehash(absolute);
|
|
281
|
+
return fresh !== void 0 && fresh !== priorHash ? path : void 0;
|
|
282
|
+
})
|
|
283
|
+
);
|
|
284
|
+
return checks.filter((path) => path !== void 0);
|
|
285
|
+
}
|
|
255
286
|
async #executeTaskWithTracking(task, startTime) {
|
|
256
287
|
if (!this.#fingerprintManager) {
|
|
257
288
|
return this.#executeTask(task, startTime);
|
|
@@ -262,12 +293,18 @@ class TaskOrchestrator {
|
|
|
262
293
|
let code;
|
|
263
294
|
let terminalOutput;
|
|
264
295
|
let fingerprint;
|
|
296
|
+
let trackerAccessCount = 0;
|
|
297
|
+
let usedRealTracker = false;
|
|
298
|
+
let autoWrites;
|
|
265
299
|
const shellCommand = this.#resolveCommand?.(task);
|
|
266
300
|
const canTrack = shellCommand && this.#trackedExecutor?.isTrackingSupported;
|
|
267
301
|
if (canTrack && this.#trackedExecutor) {
|
|
268
302
|
const trackedResult = await this.#trackedExecutor.execute(task, { captureOutput: this.#captureOutput, cwd }, shellCommand);
|
|
269
303
|
code = trackedResult.code;
|
|
270
304
|
terminalOutput = trackedResult.terminalOutput;
|
|
305
|
+
trackerAccessCount = trackedResult.accesses.length;
|
|
306
|
+
usedRealTracker = true;
|
|
307
|
+
autoWrites = trackedResult.accesses.filter((a) => a.type === "write").map((a) => a.path);
|
|
271
308
|
fingerprint = await this.#fingerprintManager.createFingerprint(
|
|
272
309
|
trackedResult.accesses,
|
|
273
310
|
taskCommand,
|
|
@@ -309,10 +346,20 @@ class TaskOrchestrator {
|
|
|
309
346
|
};
|
|
310
347
|
this.#results.set(task.id, result);
|
|
311
348
|
if (code === 0 && task.cache !== false && fingerprint) {
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
349
|
+
const modified = this.#detectSelfModifiedFingerprint(fingerprint);
|
|
350
|
+
const emptyFingerprintReason = this.#describeEmptyFingerprint(fingerprint, usedRealTracker, trackerAccessCount);
|
|
351
|
+
if (modified.length > 0) {
|
|
352
|
+
result.selfModified = true;
|
|
353
|
+
this.#lifeCycle.printSelfModifyingSkip?.(task, modified);
|
|
354
|
+
} else if (emptyFingerprintReason) {
|
|
355
|
+
result.emptyFingerprint = true;
|
|
356
|
+
this.#lifeCycle.printEmptyFingerprintWarning?.(task, emptyFingerprintReason);
|
|
357
|
+
} else {
|
|
358
|
+
const hash = hashFingerprint(fingerprint);
|
|
359
|
+
Object.assign(task, { hash });
|
|
360
|
+
await this.#cache.put(hash, terminalOutput, task.outputs, code, fingerprint, autoWrites);
|
|
361
|
+
await this.#cache.setTaskIndex(task.id, hash);
|
|
362
|
+
}
|
|
316
363
|
}
|
|
317
364
|
return result;
|
|
318
365
|
} catch (error) {
|
|
@@ -321,6 +368,38 @@ class TaskOrchestrator {
|
|
|
321
368
|
return result;
|
|
322
369
|
}
|
|
323
370
|
}
|
|
371
|
+
/**
|
|
372
|
+
* In auto-fingerprint mode, returns the workspace-relative paths the
|
|
373
|
+
* task both read *and* wrote during execution. {@link FingerprintManager}
|
|
374
|
+
* populates `modifiedInputs` from the tracker's `"write"`-typed accesses;
|
|
375
|
+
* backends that don't yet emit write accesses leave it empty.
|
|
376
|
+
*/
|
|
377
|
+
// eslint-disable-next-line class-methods-use-this
|
|
378
|
+
#detectSelfModifiedFingerprint(fingerprint) {
|
|
379
|
+
return fingerprint.modifiedInputs ?? [];
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Returns a human-readable reason string when a fingerprint looks
|
|
383
|
+
* suspiciously empty (tracker ran but observed no workspace files),
|
|
384
|
+
* or `undefined` when the fingerprint is trustworthy. Caching is
|
|
385
|
+
* skipped for empty fingerprints — silently persisting one would
|
|
386
|
+
* guarantee false cache hits on every subsequent run.
|
|
387
|
+
*
|
|
388
|
+
* Only flags results from the real tracker; the glob-based fallback
|
|
389
|
+
* path legitimately produces wide fingerprints and isn't at risk.
|
|
390
|
+
*/
|
|
391
|
+
// eslint-disable-next-line class-methods-use-this
|
|
392
|
+
#describeEmptyFingerprint(fingerprint, usedRealTracker, trackerAccessCount) {
|
|
393
|
+
if (!usedRealTracker) {
|
|
394
|
+
return void 0;
|
|
395
|
+
}
|
|
396
|
+
const hasAnyAccess = Object.keys(fingerprint.fileHashes).length > 0 || Object.keys(fingerprint.directoryListings).length > 0 || fingerprint.missingFiles.length > 0;
|
|
397
|
+
if (hasAnyAccess) {
|
|
398
|
+
return void 0;
|
|
399
|
+
}
|
|
400
|
+
const zeroAccesses = trackerAccessCount === 0;
|
|
401
|
+
return zeroAccesses ? "Tracker observed no workspace file accesses — likely a static binary on a platform without strace. Caching skipped." : "Tracker returned accesses but none fell inside the workspace. Caching skipped to avoid false cache hits.";
|
|
402
|
+
}
|
|
324
403
|
#dryRunResult(task, startTime) {
|
|
325
404
|
const cacheStatus = task.hash ? `[hash: ${task.hash.slice(0, 12)}...]` : "[no hash]";
|
|
326
405
|
const result = {
|
package/dist/packem_shared/{TrackedTaskExecutor-BGUKFE-7.js → TrackedTaskExecutor-CFPpQfXF.js}
RENAMED
|
@@ -27,7 +27,7 @@ const {
|
|
|
27
27
|
rm
|
|
28
28
|
} = __cjs_getBuiltinModule("node:fs/promises");
|
|
29
29
|
import { join } from '@visulima/path';
|
|
30
|
-
import { FileAccessTracker, generatePreloadScript } from './FileAccessTracker-
|
|
30
|
+
import { FileAccessTracker, generatePreloadScript } from './FileAccessTracker-CQ5Ot7Hd.js';
|
|
31
31
|
import { u as uniqueId } from './utils-zO0ZRgtf.js';
|
|
32
32
|
|
|
33
33
|
class TrackedTaskExecutor {
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { createRequire as __cjs_createRequire } from "node:module";
|
|
2
|
+
|
|
3
|
+
const __cjs_require = __cjs_createRequire(import.meta.url);
|
|
4
|
+
|
|
5
|
+
const __cjs_getProcess = typeof globalThis !== "undefined" && typeof globalThis.process !== "undefined" ? globalThis.process : process;
|
|
6
|
+
|
|
7
|
+
const __cjs_getBuiltinModule = (module) => {
|
|
8
|
+
// Check if we're in Node.js and version supports getBuiltinModule
|
|
9
|
+
if (typeof __cjs_getProcess !== "undefined" && __cjs_getProcess.versions && __cjs_getProcess.versions.node) {
|
|
10
|
+
const [major, minor] = __cjs_getProcess.versions.node.split(".").map(Number);
|
|
11
|
+
// Node.js 20.16.0+ and 22.3.0+
|
|
12
|
+
if (major > 22 || (major === 22 && minor >= 3) || (major === 20 && minor >= 16)) {
|
|
13
|
+
return __cjs_getProcess.getBuiltinModule(module);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
// Fallback to createRequire
|
|
17
|
+
return __cjs_require(module);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const {
|
|
21
|
+
execFile
|
|
22
|
+
} = __cjs_getBuiltinModule("node:child_process");
|
|
23
|
+
const {
|
|
24
|
+
createReadStream,
|
|
25
|
+
createWriteStream
|
|
26
|
+
} = __cjs_getBuiltinModule("node:fs");
|
|
27
|
+
const {
|
|
28
|
+
rm
|
|
29
|
+
} = __cjs_getBuiltinModule("node:fs/promises");
|
|
30
|
+
const {
|
|
31
|
+
pipeline
|
|
32
|
+
} = __cjs_getBuiltinModule("node:stream/promises");
|
|
33
|
+
const {
|
|
34
|
+
createBrotliDecompress,
|
|
35
|
+
createBrotliCompress,
|
|
36
|
+
constants
|
|
37
|
+
} = __cjs_getBuiltinModule("node:zlib");
|
|
38
|
+
|
|
39
|
+
const BROTLI_COMPRESS_OPTIONS = {
|
|
40
|
+
params: {
|
|
41
|
+
[constants.BROTLI_PARAM_MODE]: constants.BROTLI_MODE_TEXT,
|
|
42
|
+
[constants.BROTLI_PARAM_QUALITY]: 4
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const createTar = (sourceDirectory, outputPath) => new Promise((resolve, reject) => {
|
|
46
|
+
execFile("tar", ["-cf", outputPath, "-C", sourceDirectory, "."], (error) => {
|
|
47
|
+
if (error) {
|
|
48
|
+
reject(error);
|
|
49
|
+
} else {
|
|
50
|
+
resolve();
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
const extractTar = (archivePath, destinationDirectory) => new Promise((resolve, reject) => {
|
|
55
|
+
execFile("tar", ["-xf", archivePath, "-C", destinationDirectory], (error) => {
|
|
56
|
+
if (error) {
|
|
57
|
+
reject(error);
|
|
58
|
+
} else {
|
|
59
|
+
resolve();
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
const createTarGz = (sourceDirectory, outputPath) => new Promise((resolve, reject) => {
|
|
64
|
+
execFile("tar", ["-czf", outputPath, "-C", sourceDirectory, "."], (error) => {
|
|
65
|
+
if (error) {
|
|
66
|
+
reject(error);
|
|
67
|
+
} else {
|
|
68
|
+
resolve();
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
const extractTarGz = (archivePath, destinationDirectory) => new Promise((resolve, reject) => {
|
|
73
|
+
execFile("tar", ["-xzf", archivePath, "-C", destinationDirectory], (error) => {
|
|
74
|
+
if (error) {
|
|
75
|
+
reject(error);
|
|
76
|
+
} else {
|
|
77
|
+
resolve();
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
const createTarBrotli = async (sourceDirectory, outputPath) => {
|
|
82
|
+
const tarPath = `${outputPath}.tar`;
|
|
83
|
+
try {
|
|
84
|
+
await createTar(sourceDirectory, tarPath);
|
|
85
|
+
await pipeline(createReadStream(tarPath), createBrotliCompress(BROTLI_COMPRESS_OPTIONS), createWriteStream(outputPath));
|
|
86
|
+
} finally {
|
|
87
|
+
await rm(tarPath, { force: true }).catch(() => {
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const extractTarBrotli = async (archivePath, destinationDirectory) => {
|
|
92
|
+
const tarPath = `${archivePath}.tar`;
|
|
93
|
+
try {
|
|
94
|
+
await pipeline(createReadStream(archivePath), createBrotliDecompress(), createWriteStream(tarPath));
|
|
95
|
+
await extractTar(tarPath, destinationDirectory);
|
|
96
|
+
} finally {
|
|
97
|
+
await rm(tarPath, { force: true }).catch(() => {
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export { extractTarGz as a, createTarGz as b, createTarBrotli as c, extractTarBrotli as e };
|
|
@@ -33,7 +33,8 @@ const findProjectForFile = (filePath, projects) => {
|
|
|
33
33
|
let bestLength = 0;
|
|
34
34
|
for (const [name, config] of Object.entries(projects)) {
|
|
35
35
|
const { root } = config;
|
|
36
|
-
if ((filePath.startsWith(`${root}/`) || filePath === root) &&
|
|
36
|
+
if ((filePath.startsWith(`${root}/`) || filePath === root) && // Prefer the most specific (longest) match
|
|
37
|
+
root.length > bestLength) {
|
|
37
38
|
bestMatch = name;
|
|
38
39
|
bestLength = root.length;
|
|
39
40
|
}
|
|
@@ -21,12 +21,13 @@ const {
|
|
|
21
21
|
execFile
|
|
22
22
|
} = __cjs_getBuiltinModule("node:child_process");
|
|
23
23
|
const {
|
|
24
|
+
stat,
|
|
24
25
|
readFile
|
|
25
26
|
} = __cjs_getBuiltinModule("node:fs/promises");
|
|
26
27
|
import { b as hashStrings, s as sortObjectKeys, c as collectFiles, x as xxh3Hash, e as createXxh3Hasher } from './utils-zO0ZRgtf.js';
|
|
27
28
|
import { join, resolve, relative } from '@visulima/path';
|
|
28
29
|
import { getFrameworkEnvVariables } from './detectFrameworks-CeFzKE6J.js';
|
|
29
|
-
import { LockfileHasher } from './extractPackageName-
|
|
30
|
+
import { LockfileHasher } from './extractPackageName-BllKetnz.js';
|
|
30
31
|
import { loadNativeBindings } from './isNativeAvailable-BpD28A6Z.js';
|
|
31
32
|
|
|
32
33
|
const DEFAULT_GLOBAL_INPUTS = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "tsconfig.base.json", "tsconfig.json", ".env"];
|
|
@@ -73,7 +74,38 @@ const hashRuntimeValue = async (runtime) => {
|
|
|
73
74
|
}
|
|
74
75
|
return hashStrings(runtime, output);
|
|
75
76
|
};
|
|
77
|
+
const SHELL_SPECIAL_VARS = /* @__PURE__ */ new Set(["0", "!", "#", "$", "*", "-", "?", "@", "_"]);
|
|
78
|
+
const extractReferencedEnvVars = (command) => {
|
|
79
|
+
const found = /* @__PURE__ */ new Set();
|
|
80
|
+
const pattern = /\$(?:\{([^}]+)\}|([A-Z_]\w*))/gi;
|
|
81
|
+
let match;
|
|
82
|
+
while ((match = pattern.exec(command)) !== null) {
|
|
83
|
+
const raw = match[1] ?? match[2];
|
|
84
|
+
if (!raw) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const name = raw.split(/[#%:/?+\-=,^]/)[0]?.trim();
|
|
88
|
+
if (!name || SHELL_SPECIAL_VARS.has(name) || /^\d+$/.test(name)) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (!/^[A-Z_]\w*$/i.test(name)) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
found.add(name);
|
|
95
|
+
}
|
|
96
|
+
return [...found];
|
|
97
|
+
};
|
|
76
98
|
const isFileSetInput = (input) => "fileset" in input;
|
|
99
|
+
const normalizeFileset = (fileset) => {
|
|
100
|
+
if (typeof fileset === "string") {
|
|
101
|
+
return fileset;
|
|
102
|
+
}
|
|
103
|
+
const token = fileset.base === "workspace" ? "{workspaceRoot}" : "{projectRoot}";
|
|
104
|
+
if (fileset.pattern.startsWith("!")) {
|
|
105
|
+
return `!${token}/${fileset.pattern.slice(1)}`;
|
|
106
|
+
}
|
|
107
|
+
return `${token}/${fileset.pattern}`;
|
|
108
|
+
};
|
|
77
109
|
const isRuntimeInput = (input) => "runtime" in input;
|
|
78
110
|
const isEnvironmentInput = (input) => "env" in input;
|
|
79
111
|
const isExternalDependencyInput = (input) => "externalDependencies" in input;
|
|
@@ -90,6 +122,8 @@ class InProcessTaskHasher {
|
|
|
90
122
|
#smartLockfileHashing;
|
|
91
123
|
#lockfileHasher;
|
|
92
124
|
#frameworkInference;
|
|
125
|
+
#autoEnvVars;
|
|
126
|
+
#incrementalHasher;
|
|
93
127
|
#globalHash = void 0;
|
|
94
128
|
constructor(options) {
|
|
95
129
|
this.#workspaceRoot = options.workspaceRoot;
|
|
@@ -103,6 +137,8 @@ class InProcessTaskHasher {
|
|
|
103
137
|
this.#smartLockfileHashing = options.smartLockfileHashing ?? false;
|
|
104
138
|
this.#lockfileHasher = this.#smartLockfileHashing ? new LockfileHasher(options.workspaceRoot) : void 0;
|
|
105
139
|
this.#frameworkInference = options.frameworkInference ?? false;
|
|
140
|
+
this.#autoEnvVars = options.autoEnvVars ?? false;
|
|
141
|
+
this.#incrementalHasher = options.incrementalHasher;
|
|
106
142
|
}
|
|
107
143
|
async hashTask(task) {
|
|
108
144
|
const commandHash = this.#hashCommand(task);
|
|
@@ -117,7 +153,7 @@ class InProcessTaskHasher {
|
|
|
117
153
|
const negationPatterns = this.#collectNegationPatterns(inputs, task.target.project);
|
|
118
154
|
for (const input of inputs) {
|
|
119
155
|
if (isFileSetInput(input)) {
|
|
120
|
-
const fileHashes = await this.#hashFileSet(task, input.fileset, negationPatterns);
|
|
156
|
+
const fileHashes = await this.#hashFileSet(task, normalizeFileset(input.fileset), negationPatterns);
|
|
121
157
|
for (const [filePath, hash] of Object.entries(fileHashes)) {
|
|
122
158
|
nodes[filePath] = hash;
|
|
123
159
|
}
|
|
@@ -135,6 +171,17 @@ class InProcessTaskHasher {
|
|
|
135
171
|
for (const envVariable of this.#envVars) {
|
|
136
172
|
runtime[`env:${envVariable}`] = hashStrings(envVariable, process.env[envVariable] ?? "");
|
|
137
173
|
}
|
|
174
|
+
if (this.#autoEnvVars) {
|
|
175
|
+
const command = this.#resolveCommandText(task);
|
|
176
|
+
if (command) {
|
|
177
|
+
for (const name of extractReferencedEnvVars(command)) {
|
|
178
|
+
const key = `env:${name}`;
|
|
179
|
+
if (runtime[key] === void 0) {
|
|
180
|
+
runtime[key] = hashStrings(name, process.env[name] ?? "");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
138
185
|
const project = this.#projects[task.target.project];
|
|
139
186
|
if (project) {
|
|
140
187
|
if (this.#lockfileHasher) {
|
|
@@ -177,6 +224,21 @@ class InProcessTaskHasher {
|
|
|
177
224
|
hash.update(overridesJson);
|
|
178
225
|
return hash.digest();
|
|
179
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* Looks up the command string associated with a task so referenced
|
|
229
|
+
* env vars can be extracted. Checks `task.overrides.command` first
|
|
230
|
+
* (consumers like vis stash the resolved command there), then falls
|
|
231
|
+
* back to the project's target config.
|
|
232
|
+
*/
|
|
233
|
+
#resolveCommandText(task) {
|
|
234
|
+
const override = task.overrides["command"];
|
|
235
|
+
if (typeof override === "string") {
|
|
236
|
+
return override;
|
|
237
|
+
}
|
|
238
|
+
const project = this.#projects[task.target.project];
|
|
239
|
+
const command = project?.targets?.[task.target.target]?.command ?? this.#targetDefaults[task.target.target]?.command;
|
|
240
|
+
return typeof command === "string" ? command : void 0;
|
|
241
|
+
}
|
|
180
242
|
#resolveInputs(task) {
|
|
181
243
|
const project = this.#projects[task.target.project];
|
|
182
244
|
const targetConfig = project?.targets?.[task.target.target];
|
|
@@ -216,7 +278,7 @@ class InProcessTaskHasher {
|
|
|
216
278
|
if (!isFileSetInput(input)) {
|
|
217
279
|
continue;
|
|
218
280
|
}
|
|
219
|
-
const resolved = input.fileset.replace("{projectRoot}", projectRoot).replace("{workspaceRoot}", ".");
|
|
281
|
+
const resolved = normalizeFileset(input.fileset).replace("{projectRoot}", projectRoot).replace("{workspaceRoot}", ".");
|
|
220
282
|
if (resolved.startsWith("!")) {
|
|
221
283
|
patterns.push(
|
|
222
284
|
resolved.slice(1).replace(/\/\*\*\/\*$/, "").replace(/\/\*$/, "")
|
|
@@ -243,13 +305,30 @@ class InProcessTaskHasher {
|
|
|
243
305
|
try {
|
|
244
306
|
if (this.#native) {
|
|
245
307
|
const fileHashes = this.#native.hashFilesInDirectory(absoluteBase, this.#workspaceRoot);
|
|
308
|
+
const incremental = this.#incrementalHasher;
|
|
309
|
+
const recordPromises = [];
|
|
246
310
|
for (const { hash, path } of fileHashes) {
|
|
247
311
|
const absPath = resolve(this.#workspaceRoot, path);
|
|
248
|
-
if (
|
|
249
|
-
|
|
250
|
-
|
|
312
|
+
if (isExcluded(absPath)) {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
result[path] = hash;
|
|
316
|
+
this.#fileHashCache.set(absPath, hash);
|
|
317
|
+
if (incremental) {
|
|
318
|
+
recordPromises.push(
|
|
319
|
+
stat(absPath).then((s) => {
|
|
320
|
+
if (s.isFile()) {
|
|
321
|
+
incremental.recordSnapshot(absPath, hash, s.mtimeMs, s.size);
|
|
322
|
+
}
|
|
323
|
+
return void 0;
|
|
324
|
+
}).catch(() => {
|
|
325
|
+
})
|
|
326
|
+
);
|
|
251
327
|
}
|
|
252
328
|
}
|
|
329
|
+
if (recordPromises.length > 0) {
|
|
330
|
+
await Promise.all(recordPromises);
|
|
331
|
+
}
|
|
253
332
|
return result;
|
|
254
333
|
}
|
|
255
334
|
const files = await collectFiles(absoluteBase, IGNORED_DIRS);
|
|
@@ -272,6 +351,24 @@ class InProcessTaskHasher {
|
|
|
272
351
|
if (cached) {
|
|
273
352
|
return cached;
|
|
274
353
|
}
|
|
354
|
+
if (this.#incrementalHasher) {
|
|
355
|
+
try {
|
|
356
|
+
const fileStat = await stat(filePath);
|
|
357
|
+
if (fileStat.isFile()) {
|
|
358
|
+
const snapshotHit = this.#incrementalHasher.getSnapshotHash(filePath, fileStat.mtimeMs, fileStat.size);
|
|
359
|
+
if (snapshotHit) {
|
|
360
|
+
this.#fileHashCache.set(filePath, snapshotHit);
|
|
361
|
+
return snapshotHit;
|
|
362
|
+
}
|
|
363
|
+
const content = await readFile(filePath);
|
|
364
|
+
const hash = xxh3Hash(content);
|
|
365
|
+
this.#incrementalHasher.recordSnapshot(filePath, hash, fileStat.mtimeMs, fileStat.size);
|
|
366
|
+
this.#fileHashCache.set(filePath, hash);
|
|
367
|
+
return hash;
|
|
368
|
+
}
|
|
369
|
+
} catch {
|
|
370
|
+
}
|
|
371
|
+
}
|
|
275
372
|
try {
|
|
276
373
|
const content = await readFile(filePath);
|
|
277
374
|
const hash = xxh3Hash(content);
|
|
@@ -326,6 +423,25 @@ class InProcessTaskHasher {
|
|
|
326
423
|
this.#globalHash = void 0;
|
|
327
424
|
this.#lockfileHasher?.clearCache();
|
|
328
425
|
}
|
|
426
|
+
/**
|
|
427
|
+
* Reads `filePath` fresh and returns its content hash, bypassing the
|
|
428
|
+
* in-memory cache used during the initial `hashTask` pass.
|
|
429
|
+
*
|
|
430
|
+
* Used to detect tasks that modify their own tracked inputs: compare
|
|
431
|
+
* a pre-execution hash (from `task.hashDetails.nodes`) against the
|
|
432
|
+
* post-execution result of this method.
|
|
433
|
+
* @param filePath Absolute path to the file.
|
|
434
|
+
* @returns The fresh xxh3 hash, or `undefined` if the file cannot be read.
|
|
435
|
+
*/
|
|
436
|
+
// eslint-disable-next-line class-methods-use-this
|
|
437
|
+
async rehashFile(filePath) {
|
|
438
|
+
try {
|
|
439
|
+
const content = await readFile(filePath);
|
|
440
|
+
return xxh3Hash(content);
|
|
441
|
+
} catch {
|
|
442
|
+
return void 0;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
329
445
|
}
|
|
330
446
|
const computeTaskHash = (hashDetails) => {
|
|
331
447
|
const native = getNativeBindings();
|