@visulima/task-runner 1.0.0-alpha.2 → 1.0.0-alpha.20
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 +310 -0
- package/README.md +198 -52
- package/dist/index.d.ts +3795 -34
- package/dist/index.js +1 -20
- package/dist/packem_chunks/index.js +31 -0
- package/dist/packem_shared/Cache-C8FfeXpg.js +2 -0
- package/dist/packem_shared/CompositeLifeCycle-C6aee9GK.js +1 -0
- package/dist/packem_shared/FileAccessTracker-DBz_w4wl.js +50 -0
- package/dist/packem_shared/FingerprintManager-CYW2EwLc.js +2 -0
- package/dist/packem_shared/HttpRemoteCache-CpPl6lzE.js +1 -0
- package/dist/packem_shared/INPUT_URI_SCHEMES-Csrd0tlg.js +1 -0
- package/dist/packem_shared/IncrementalFileHasher-B-V3i2x-.js +1 -0
- package/dist/packem_shared/LogReporter-3R3oWj-Q.js +13 -0
- package/dist/packem_shared/ReapiRemoteCache-BXJip5wH.js +251 -0
- package/dist/packem_shared/TaskOrchestrator-CYj5MLwz.js +6 -0
- package/dist/packem_shared/TerminalBuffer-BtZy7TpT.js +3 -0
- package/dist/packem_shared/TrackedTaskExecutor-CtYLL3vS.js +1 -0
- package/dist/packem_shared/V2_ROOT-injxWBrl.js +1 -0
- package/dist/packem_shared/actionDigestForTaskHash-BOL4fZ9v.js +1 -0
- package/dist/packem_shared/archive-CDfGy5Lm.js +1 -0
- package/dist/packem_shared/buildForwardDependencyMap-DudUDFze.js +3 -0
- package/dist/packem_shared/collectFiles-W4bnBRpb.js +1 -0
- package/dist/packem_shared/collectNodeModulesBinDirs-CD-eDrtO.js +1 -0
- package/dist/packem_shared/computeTaskHash-CaPdG1BA.js +1 -0
- package/dist/packem_shared/containsBlob-DAU8R7GH.js +1 -0
- package/dist/packem_shared/createInputHandler-CkDCpPYy.js +1 -0
- package/dist/packem_shared/createTaskGraph-D8Jn_Dn9.js +1 -0
- package/dist/packem_shared/defaultTaskRunner-DMHpavzm.js +2 -0
- package/dist/packem_shared/detectFrameworks-WVZJOPgN.js +1 -0
- package/dist/packem_shared/detectScriptShell-CaTDk5cW.js +1 -0
- package/dist/packem_shared/digestBuffer-g11aCaDx.js +1 -0
- package/dist/packem_shared/enforceProjectConstraints-dNc1SwRi.js +1 -0
- package/dist/packem_shared/expandArguments-D7qvc6Rp.js +1 -0
- package/dist/packem_shared/expandShortcut-BErNHNXZ.js +1 -0
- package/dist/packem_shared/expandTokensInString-DVSFEdWu.js +1 -0
- package/dist/packem_shared/expandWildcard-DE0dOOZF.js +1 -0
- package/dist/packem_shared/extractPackageName-BeL6Gc3a.js +1 -0
- package/dist/packem_shared/findCycle-BY8-jmzB.js +1 -0
- package/dist/packem_shared/formatTimingTable-CP3rsDwf.js +7 -0
- package/dist/packem_shared/generateRunSummary-L9Z2NfWn.js +1 -0
- package/dist/packem_shared/getCurrentBranch-D-qoZByx.js +1 -0
- package/dist/packem_shared/getMainWorktreeRoot-DB9P2wDL.js +1 -0
- package/dist/packem_shared/isNativeAvailable-CkTjxb7P.js +1 -0
- package/dist/packem_shared/parseCommands-BHsXoUCd.js +1 -0
- package/dist/packem_shared/parsePartition-Bt1jBjZH.js +1 -0
- package/dist/packem_shared/projectGraphToDot-FN6oHDGH.js +250 -0
- package/dist/packem_shared/resolveCacheMode--4y60ODd.js +1 -0
- package/dist/packem_shared/resolveOutputs-CzGGEbcP.js +1 -0
- package/dist/packem_shared/runConcurrentFallback-BhJCT2LA.js +3 -0
- package/dist/packem_shared/runConcurrently-D1Ytsjaj.js +1 -0
- package/dist/packem_shared/runTeardown-DAn1xFWJ.js +1 -0
- package/dist/packem_shared/shell-quote-BhmqDUL1.js +1 -0
- package/dist/packem_shared/stripQuotes-jkZb0CL9.js +1 -0
- package/dist/packem_shared/toChromeTrace-DxN5NQIU.js +1 -0
- package/dist/packem_shared/tracked-executor-B90U4Um3.js +3 -0
- package/dist/packem_shared/utils-BH2W5Wml.js +1 -0
- package/dist/packem_shared/withRestart-DKtEGsQA.js +1 -0
- package/index.js +603 -0
- package/package.json +31 -19
- package/binding.js +0 -204
- package/dist/affected.d.ts +0 -48
- package/dist/cache.d.ts +0 -103
- package/dist/default-task-runner.d.ts +0 -44
- package/dist/file-access-tracker.d.ts +0 -53
- package/dist/fingerprint.d.ts +0 -45
- package/dist/framework-inference.d.ts +0 -35
- package/dist/graph-visualizer.d.ts +0 -74
- package/dist/incremental-hasher.d.ts +0 -58
- package/dist/life-cycle.d.ts +0 -36
- package/dist/lockfile-hasher.d.ts +0 -73
- package/dist/native-binding.d.ts +0 -64
- package/dist/packem_shared/Cache-IYpTYVUC.js +0 -298
- package/dist/packem_shared/CompositeLifeCycle-7AtYw1dv.js +0 -112
- package/dist/packem_shared/FileAccessTracker-CrtBAt5D.js +0 -239
- package/dist/packem_shared/FingerprintManager-D6Y0erg-.js +0 -227
- package/dist/packem_shared/IncrementalFileHasher-Ds3J6dgb.js +0 -151
- package/dist/packem_shared/RemoteCache-BDqrnDEi.js +0 -179
- package/dist/packem_shared/TaskOrchestrator-BvYs3ONw.js +0 -342
- package/dist/packem_shared/TaskScheduler-CJilHDta.js +0 -111
- package/dist/packem_shared/TrackedTaskExecutor-BGUKFE-7.js +0 -164
- package/dist/packem_shared/collectFiles-ClXHnHhg.js +0 -22
- package/dist/packem_shared/computeTaskHash-BoCnnvIJ.js +0 -356
- package/dist/packem_shared/createTaskGraph-CcsFaSrz.js +0 -164
- package/dist/packem_shared/defaultTaskRunner-CrW4v5Ye.js +0 -79
- package/dist/packem_shared/detectFrameworks-CeFzKE6J.js +0 -101
- package/dist/packem_shared/extractPackageName-CbVNW-dr.js +0 -189
- package/dist/packem_shared/filterAffectedTasks-I-18zPg6.js +0 -135
- package/dist/packem_shared/findCycle-DF4_BRdO.js +0 -212
- package/dist/packem_shared/generateRunSummary-qn-_jKwt.js +0 -134
- package/dist/packem_shared/isNativeAvailable-BWhnZ4ES.js +0 -19
- package/dist/packem_shared/projectGraphToDot-VdTjHcVp.js +0 -202
- package/dist/packem_shared/utils-zO0ZRgtf.js +0 -390
- package/dist/remote-cache.d.ts +0 -55
- package/dist/run-summary.d.ts +0 -89
- package/dist/task-graph-utils.d.ts +0 -39
- package/dist/task-graph.d.ts +0 -22
- package/dist/task-hasher.d.ts +0 -67
- package/dist/task-orchestrator.d.ts +0 -38
- package/dist/task-scheduler.d.ts +0 -18
- package/dist/tracked-executor.d.ts +0 -46
- package/dist/types.d.ts +0 -385
- package/dist/utils.d.ts +0 -39
|
@@ -1,239 +0,0 @@
|
|
|
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
|
-
exec,
|
|
22
|
-
execFileSync
|
|
23
|
-
} = __cjs_getBuiltinModule("node:child_process");
|
|
24
|
-
const {
|
|
25
|
-
mkdir,
|
|
26
|
-
readFile,
|
|
27
|
-
rm
|
|
28
|
-
} = __cjs_getBuiltinModule("node:fs/promises");
|
|
29
|
-
const {
|
|
30
|
-
platform
|
|
31
|
-
} = __cjs_getBuiltinModule("node:os");
|
|
32
|
-
import { resolve, join } from '@visulima/path';
|
|
33
|
-
import { u as uniqueId } from './utils-zO0ZRgtf.js';
|
|
34
|
-
|
|
35
|
-
import __cjs_mod__ from "node:module"; // -- packem CommonJS require shim --
|
|
36
|
-
const require = __cjs_mod__.createRequire(import.meta.url);
|
|
37
|
-
let straceAvailable;
|
|
38
|
-
const isStraceAvailable = () => {
|
|
39
|
-
if (straceAvailable === void 0) {
|
|
40
|
-
try {
|
|
41
|
-
execFileSync("strace", ["-V"], { stdio: "ignore" });
|
|
42
|
-
straceAvailable = true;
|
|
43
|
-
} catch {
|
|
44
|
-
straceAvailable = false;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return straceAvailable;
|
|
48
|
-
};
|
|
49
|
-
const STRACE_PATTERNS = [
|
|
50
|
-
{ pattern: /openat\(AT_FDCWD,\s*"([^"]+)"/, type: "read" },
|
|
51
|
-
{ pattern: /^(?:\d+\s+)?open\("([^"]+)"/, type: "read" },
|
|
52
|
-
{ pattern: /(?:stat|lstat|newfstatat)\((?:AT_FDCWD,\s*)?"([^"]+)"/, type: "stat" },
|
|
53
|
-
{ pattern: /access\("([^"]+)"/, type: "stat" }
|
|
54
|
-
];
|
|
55
|
-
class FileAccessTracker {
|
|
56
|
-
#workspaceRoot;
|
|
57
|
-
#excludePatterns;
|
|
58
|
-
/** Tracks active child processes for cleanup on abort */
|
|
59
|
-
#activeProcesses = /* @__PURE__ */ new Set();
|
|
60
|
-
constructor(workspaceRoot, excludePatterns) {
|
|
61
|
-
this.#workspaceRoot = resolve(workspaceRoot);
|
|
62
|
-
this.#excludePatterns = excludePatterns ?? [
|
|
63
|
-
/\/proc\//,
|
|
64
|
-
/\/sys\//,
|
|
65
|
-
/\/dev\//,
|
|
66
|
-
/\/tmp\//,
|
|
67
|
-
/\/etc\//,
|
|
68
|
-
/\.so(\.\d+)*$/,
|
|
69
|
-
/node_modules\/.package-lock\.json$/
|
|
70
|
-
];
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Returns true if file access tracking is supported on the current platform.
|
|
74
|
-
*/
|
|
75
|
-
// eslint-disable-next-line class-methods-use-this
|
|
76
|
-
isSupported() {
|
|
77
|
-
return platform() === "linux" && isStraceAvailable();
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Runs a command and tracks all file system accesses.
|
|
81
|
-
* On unsupported platforms, runs the command without tracking.
|
|
82
|
-
*/
|
|
83
|
-
async track(command, options = {}) {
|
|
84
|
-
if (!this.isSupported()) {
|
|
85
|
-
return this.#runWithoutTracking(command, options);
|
|
86
|
-
}
|
|
87
|
-
return this.#runWithStrace(command, options);
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Runs a command wrapped with strace to capture file accesses.
|
|
91
|
-
*/
|
|
92
|
-
async #runWithStrace(command, options) {
|
|
93
|
-
const traceDirectory = join(this.#workspaceRoot, "node_modules", ".cache", "task-runner");
|
|
94
|
-
await mkdir(traceDirectory, { recursive: true });
|
|
95
|
-
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}`;
|
|
97
|
-
return new Promise((_resolve) => {
|
|
98
|
-
const child = exec(
|
|
99
|
-
straceCommand,
|
|
100
|
-
{
|
|
101
|
-
cwd: options.cwd ?? this.#workspaceRoot,
|
|
102
|
-
env: { ...process.env, ...options.env },
|
|
103
|
-
maxBuffer: 50 * 1024 * 1024
|
|
104
|
-
// 50MB
|
|
105
|
-
},
|
|
106
|
-
async (_error, stdout, stderr) => {
|
|
107
|
-
this.#activeProcesses.delete(child);
|
|
108
|
-
let accesses = [];
|
|
109
|
-
try {
|
|
110
|
-
const traceContent = await readFile(traceFile, "utf8");
|
|
111
|
-
accesses = this.#parseStraceOutput(traceContent, options.cwd ?? this.#workspaceRoot);
|
|
112
|
-
} catch {
|
|
113
|
-
}
|
|
114
|
-
await rm(traceFile, { force: true }).catch(() => {
|
|
115
|
-
});
|
|
116
|
-
_resolve({
|
|
117
|
-
accesses,
|
|
118
|
-
code: child.exitCode ?? 1,
|
|
119
|
-
output: stdout + stderr
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
);
|
|
123
|
-
this.#activeProcesses.add(child);
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* Parses strace output to extract file accesses.
|
|
128
|
-
*/
|
|
129
|
-
#parseStraceOutput(traceContent, cwd) {
|
|
130
|
-
const accesses = [];
|
|
131
|
-
const seenPaths = /* @__PURE__ */ new Set();
|
|
132
|
-
for (const line of traceContent.split("\n")) {
|
|
133
|
-
const parsed = this.#parseStraceLine(line, cwd);
|
|
134
|
-
if (parsed && !seenPaths.has(parsed.path)) {
|
|
135
|
-
seenPaths.add(parsed.path);
|
|
136
|
-
accesses.push(parsed);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
return accesses;
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Parses a single strace output line.
|
|
143
|
-
* Each entry maps a regex to the file access type it represents.
|
|
144
|
-
*/
|
|
145
|
-
#parseStraceLine(line, cwd) {
|
|
146
|
-
const isMissing = line.includes("ENOENT");
|
|
147
|
-
for (const { pattern, type } of STRACE_PATTERNS) {
|
|
148
|
-
const match = pattern.exec(line);
|
|
149
|
-
if (!match?.[1]) {
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
let path = match[1];
|
|
153
|
-
if (!path.startsWith("/")) {
|
|
154
|
-
path = resolve(cwd, path);
|
|
155
|
-
}
|
|
156
|
-
if (this.#shouldExclude(path) || !path.startsWith(this.#workspaceRoot)) {
|
|
157
|
-
return void 0;
|
|
158
|
-
}
|
|
159
|
-
return { path, type: isMissing ? "missing" : type };
|
|
160
|
-
}
|
|
161
|
-
return void 0;
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* Checks if a path should be excluded from tracking.
|
|
165
|
-
*/
|
|
166
|
-
#shouldExclude(filePath) {
|
|
167
|
-
return this.#excludePatterns.some((pattern) => pattern.test(filePath));
|
|
168
|
-
}
|
|
169
|
-
/**
|
|
170
|
-
* Kills all active child processes. Called on abort/signal to prevent orphans.
|
|
171
|
-
*/
|
|
172
|
-
killAll() {
|
|
173
|
-
for (const child of this.#activeProcesses) {
|
|
174
|
-
try {
|
|
175
|
-
child.kill("SIGTERM");
|
|
176
|
-
} catch {
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
this.#activeProcesses.clear();
|
|
180
|
-
}
|
|
181
|
-
/**
|
|
182
|
-
* Runs a command without file access tracking.
|
|
183
|
-
*/
|
|
184
|
-
async #runWithoutTracking(command, options) {
|
|
185
|
-
return new Promise((_resolve) => {
|
|
186
|
-
const child = exec(
|
|
187
|
-
command,
|
|
188
|
-
{
|
|
189
|
-
cwd: options.cwd ?? this.#workspaceRoot,
|
|
190
|
-
env: { ...process.env, ...options.env },
|
|
191
|
-
maxBuffer: 50 * 1024 * 1024
|
|
192
|
-
},
|
|
193
|
-
(_error, stdout, stderr) => {
|
|
194
|
-
this.#activeProcesses.delete(child);
|
|
195
|
-
_resolve({
|
|
196
|
-
accesses: [],
|
|
197
|
-
code: child.exitCode ?? 1,
|
|
198
|
-
output: stdout + stderr
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
);
|
|
202
|
-
this.#activeProcesses.add(child);
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
const generatePreloadScript = (outputPath) => String.raw`
|
|
207
|
-
import { createWriteStream } from "node:fs";
|
|
208
|
-
|
|
209
|
-
const fs = require("node:fs");
|
|
210
|
-
const fsp = require("node:fs/promises");
|
|
211
|
-
const logStream = createWriteStream(${JSON.stringify(outputPath)}, { flags: "a" });
|
|
212
|
-
const log = (type, path) => { logStream.write(JSON.stringify({ type, path }) + "\n"); };
|
|
213
|
-
|
|
214
|
-
// Patch each fs method: save original, replace with logged wrapper
|
|
215
|
-
const patch = (obj, method, type) => {
|
|
216
|
-
const orig = obj[method];
|
|
217
|
-
obj[method] = function(...args) {
|
|
218
|
-
log(type, args[0]?.toString());
|
|
219
|
-
return orig.apply(this, args);
|
|
220
|
-
};
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
// Sync
|
|
224
|
-
patch(fs, "readFileSync", "read");
|
|
225
|
-
patch(fs, "statSync", "stat");
|
|
226
|
-
patch(fs, "readdirSync", "readdir");
|
|
227
|
-
// Async (callback)
|
|
228
|
-
patch(fs, "readFile", "read");
|
|
229
|
-
patch(fs, "stat", "stat");
|
|
230
|
-
patch(fs, "readdir", "readdir");
|
|
231
|
-
// Async (promises)
|
|
232
|
-
patch(fsp, "readFile", "read");
|
|
233
|
-
patch(fsp, "stat", "stat");
|
|
234
|
-
patch(fsp, "readdir", "readdir");
|
|
235
|
-
|
|
236
|
-
process.on("beforeExit", () => { logStream.end(); });
|
|
237
|
-
`;
|
|
238
|
-
|
|
239
|
-
export { FileAccessTracker, generatePreloadScript };
|
|
@@ -1,227 +0,0 @@
|
|
|
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
|
-
readdir,
|
|
22
|
-
stat
|
|
23
|
-
} = __cjs_getBuiltinModule("node:fs/promises");
|
|
24
|
-
import { resolve, relative } from '@visulima/path';
|
|
25
|
-
import { b as hashStrings, s as sortObjectKeys, h as hashFile } from './utils-zO0ZRgtf.js';
|
|
26
|
-
|
|
27
|
-
class FingerprintManager {
|
|
28
|
-
#workspaceRoot;
|
|
29
|
-
#fileHashCache = /* @__PURE__ */ new Map();
|
|
30
|
-
constructor(workspaceRoot) {
|
|
31
|
-
this.#workspaceRoot = resolve(workspaceRoot);
|
|
32
|
-
}
|
|
33
|
-
// eslint-disable-next-line sonarjs/cognitive-complexity
|
|
34
|
-
async createFingerprint(accesses, command, args, envVariables, envPatterns = [], untrackedEnvVariables = []) {
|
|
35
|
-
const fileHashes = {};
|
|
36
|
-
const missingPaths = /* @__PURE__ */ new Set();
|
|
37
|
-
const directoryListings = {};
|
|
38
|
-
for (const access of accesses) {
|
|
39
|
-
const relativePath = relative(this.#workspaceRoot, access.path);
|
|
40
|
-
switch (access.type) {
|
|
41
|
-
case "missing": {
|
|
42
|
-
missingPaths.add(relativePath);
|
|
43
|
-
break;
|
|
44
|
-
}
|
|
45
|
-
case "read":
|
|
46
|
-
case "stat": {
|
|
47
|
-
if (!fileHashes[relativePath]) {
|
|
48
|
-
const hash = await this.#hashFileWithCache(access.path);
|
|
49
|
-
if (hash) {
|
|
50
|
-
fileHashes[relativePath] = hash;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
break;
|
|
54
|
-
}
|
|
55
|
-
case "readdir": {
|
|
56
|
-
if (!directoryListings[relativePath]) {
|
|
57
|
-
try {
|
|
58
|
-
const entries = await readdir(access.path);
|
|
59
|
-
directoryListings[relativePath] = entries.toSorted();
|
|
60
|
-
} catch {
|
|
61
|
-
directoryListings[relativePath] = [];
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
break;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
const commandHash = hashStrings(`${command}:${JSON.stringify(sortObjectKeys(args))}`);
|
|
69
|
-
const envHashes = {};
|
|
70
|
-
const matchedEnvVariables = FingerprintManager.#resolveEnvPatterns(envPatterns, envVariables);
|
|
71
|
-
const untrackedSet = new Set(untrackedEnvVariables);
|
|
72
|
-
for (const [key, value] of Object.entries(matchedEnvVariables)) {
|
|
73
|
-
if (untrackedSet.has(key)) {
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
envHashes[key] = hashStrings(`${key}=${value ?? ""}`);
|
|
77
|
-
}
|
|
78
|
-
const missingFiles = [...missingPaths].toSorted();
|
|
79
|
-
return { commandHash, directoryListings, envHashes, fileHashes, missingFiles };
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Validates a stored fingerprint against the current state.
|
|
83
|
-
* Returns null if valid (cache hit), or an array of reasons (cache miss).
|
|
84
|
-
*
|
|
85
|
-
* Does NOT use the file hash cache — validation must see current disk state.
|
|
86
|
-
*/
|
|
87
|
-
async validate(fingerprint) {
|
|
88
|
-
const reasons = [];
|
|
89
|
-
const fileCheckPromises = Object.entries(fingerprint.fileHashes).map(async ([relativePath, previousHash]) => {
|
|
90
|
-
const absolutePath = resolve(this.#workspaceRoot, relativePath);
|
|
91
|
-
const currentHash = await hashFile(absolutePath);
|
|
92
|
-
if (!currentHash) {
|
|
93
|
-
return { detail: relativePath, previousHash, type: "file-deleted" };
|
|
94
|
-
}
|
|
95
|
-
if (currentHash !== previousHash) {
|
|
96
|
-
return { currentHash, detail: relativePath, previousHash, type: "file-changed" };
|
|
97
|
-
}
|
|
98
|
-
return void 0;
|
|
99
|
-
});
|
|
100
|
-
const missingCheckPromises = fingerprint.missingFiles.map(async (relativePath) => {
|
|
101
|
-
const absolutePath = resolve(this.#workspaceRoot, relativePath);
|
|
102
|
-
try {
|
|
103
|
-
await stat(absolutePath);
|
|
104
|
-
return { detail: relativePath, type: "file-created" };
|
|
105
|
-
} catch {
|
|
106
|
-
return void 0;
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
const directoryCheckPromises = Object.entries(fingerprint.directoryListings).map(async ([relativePath, previousEntries]) => {
|
|
110
|
-
const absolutePath = resolve(this.#workspaceRoot, relativePath);
|
|
111
|
-
try {
|
|
112
|
-
const readdirResult = await readdir(absolutePath);
|
|
113
|
-
const currentEntries = readdirResult.toSorted();
|
|
114
|
-
if (JSON.stringify(previousEntries) !== JSON.stringify(currentEntries)) {
|
|
115
|
-
return {
|
|
116
|
-
currentHash: JSON.stringify(currentEntries),
|
|
117
|
-
detail: relativePath,
|
|
118
|
-
previousHash: JSON.stringify(previousEntries),
|
|
119
|
-
type: "directory-changed"
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
} catch {
|
|
123
|
-
return { detail: relativePath, type: "directory-changed" };
|
|
124
|
-
}
|
|
125
|
-
return void 0;
|
|
126
|
-
});
|
|
127
|
-
const [fileResults, missingResults, directoryResults] = await Promise.all([
|
|
128
|
-
Promise.all(fileCheckPromises),
|
|
129
|
-
Promise.all(missingCheckPromises),
|
|
130
|
-
Promise.all(directoryCheckPromises)
|
|
131
|
-
]);
|
|
132
|
-
for (const r of [...fileResults, ...missingResults, ...directoryResults]) {
|
|
133
|
-
if (r) {
|
|
134
|
-
reasons.push(r);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
for (const [envName, previousHash] of Object.entries(fingerprint.envHashes)) {
|
|
138
|
-
const currentHash = hashStrings(`${envName}=${process.env[envName] ?? ""}`);
|
|
139
|
-
if (currentHash !== previousHash) {
|
|
140
|
-
reasons.push({
|
|
141
|
-
currentHash,
|
|
142
|
-
detail: envName,
|
|
143
|
-
previousHash,
|
|
144
|
-
type: "env-changed"
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
return reasons.length > 0 ? reasons : void 0;
|
|
149
|
-
}
|
|
150
|
-
// eslint-disable-next-line class-methods-use-this
|
|
151
|
-
validateCommand(fingerprint, command, args) {
|
|
152
|
-
const currentHash = hashStrings(`${command}:${JSON.stringify(sortObjectKeys(args))}`);
|
|
153
|
-
if (currentHash !== fingerprint.commandHash) {
|
|
154
|
-
return {
|
|
155
|
-
currentHash,
|
|
156
|
-
detail: "command arguments",
|
|
157
|
-
previousHash: fingerprint.commandHash,
|
|
158
|
-
type: "args-changed"
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
return void 0;
|
|
162
|
-
}
|
|
163
|
-
// eslint-disable-next-line class-methods-use-this
|
|
164
|
-
formatMissReasons(reasons) {
|
|
165
|
-
const lines = ["Cache miss reasons:"];
|
|
166
|
-
for (const reason of reasons) {
|
|
167
|
-
switch (reason.type) {
|
|
168
|
-
case "args-changed": {
|
|
169
|
-
lines.push(` - Command arguments changed`);
|
|
170
|
-
break;
|
|
171
|
-
}
|
|
172
|
-
case "directory-changed": {
|
|
173
|
-
lines.push(` - Directory contents changed: ${reason.detail}`);
|
|
174
|
-
break;
|
|
175
|
-
}
|
|
176
|
-
case "env-changed": {
|
|
177
|
-
lines.push(` - Environment variable changed: ${reason.detail}`);
|
|
178
|
-
break;
|
|
179
|
-
}
|
|
180
|
-
case "file-changed": {
|
|
181
|
-
lines.push(` - File modified: ${reason.detail}`);
|
|
182
|
-
break;
|
|
183
|
-
}
|
|
184
|
-
case "file-created": {
|
|
185
|
-
lines.push(` - File created (was missing): ${reason.detail}`);
|
|
186
|
-
break;
|
|
187
|
-
}
|
|
188
|
-
case "file-deleted": {
|
|
189
|
-
lines.push(` - File deleted: ${reason.detail}`);
|
|
190
|
-
break;
|
|
191
|
-
}
|
|
192
|
-
case "no-fingerprint": {
|
|
193
|
-
lines.push(` - No previous fingerprint found (first run)`);
|
|
194
|
-
break;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
return lines.join("\n");
|
|
199
|
-
}
|
|
200
|
-
static #resolveEnvPatterns(patterns, envVariables) {
|
|
201
|
-
const result = {};
|
|
202
|
-
for (const pattern of patterns) {
|
|
203
|
-
if (pattern.includes("*")) {
|
|
204
|
-
const prefix = pattern.replace("*", "");
|
|
205
|
-
for (const [key, value] of Object.entries(envVariables)) {
|
|
206
|
-
if (key.startsWith(prefix)) {
|
|
207
|
-
result[key] = value;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
} else {
|
|
211
|
-
result[pattern] = envVariables[pattern];
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
return result;
|
|
215
|
-
}
|
|
216
|
-
async #hashFileWithCache(filePath) {
|
|
217
|
-
const cached = this.#fileHashCache.get(filePath);
|
|
218
|
-
if (cached !== void 0) {
|
|
219
|
-
return cached;
|
|
220
|
-
}
|
|
221
|
-
const hash = await hashFile(filePath) ?? void 0;
|
|
222
|
-
this.#fileHashCache.set(filePath, hash);
|
|
223
|
-
return hash;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
export { FingerprintManager };
|
|
@@ -1,151 +0,0 @@
|
|
|
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
|
-
readFile,
|
|
22
|
-
mkdir,
|
|
23
|
-
writeFile,
|
|
24
|
-
stat
|
|
25
|
-
} = __cjs_getBuiltinModule("node:fs/promises");
|
|
26
|
-
import { c as collectFiles, x as xxh3Hash } from './utils-zO0ZRgtf.js';
|
|
27
|
-
import { join, dirname, relative } from '@visulima/path';
|
|
28
|
-
|
|
29
|
-
const DEFAULT_IGNORED_DIRS = /* @__PURE__ */ new Set([".cache", ".git", ".task-runner-cache", "coverage", "dist", "node_modules"]);
|
|
30
|
-
class IncrementalFileHasher {
|
|
31
|
-
#workspaceRoot;
|
|
32
|
-
#ignoredDirs;
|
|
33
|
-
#snapshotPath;
|
|
34
|
-
#snapshot = /* @__PURE__ */ new Map();
|
|
35
|
-
#loaded = false;
|
|
36
|
-
constructor(options) {
|
|
37
|
-
this.#workspaceRoot = options.workspaceRoot;
|
|
38
|
-
this.#ignoredDirs = options.ignoredDirs ?? DEFAULT_IGNORED_DIRS;
|
|
39
|
-
this.#snapshotPath = options.snapshotPath ?? join(options.workspaceRoot, "node_modules", ".cache", "task-runner", "file-snapshot.json");
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Loads the snapshot from disk if available.
|
|
43
|
-
* Called automatically on first use.
|
|
44
|
-
*/
|
|
45
|
-
async load() {
|
|
46
|
-
if (this.#loaded) {
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
this.#loaded = true;
|
|
50
|
-
if (!this.#snapshotPath) {
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
try {
|
|
54
|
-
const content = await readFile(this.#snapshotPath, "utf8");
|
|
55
|
-
const data = JSON.parse(content);
|
|
56
|
-
for (const [path, snap] of Object.entries(data)) {
|
|
57
|
-
this.#snapshot.set(path, snap);
|
|
58
|
-
}
|
|
59
|
-
} catch {
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Persists the current snapshot to disk for cross-run reuse.
|
|
64
|
-
*/
|
|
65
|
-
async save() {
|
|
66
|
-
if (!this.#snapshotPath) {
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
const directory = dirname(this.#snapshotPath);
|
|
70
|
-
await mkdir(directory, { recursive: true });
|
|
71
|
-
const data = {};
|
|
72
|
-
for (const [path, snap] of this.#snapshot) {
|
|
73
|
-
data[path] = snap;
|
|
74
|
-
}
|
|
75
|
-
await writeFile(this.#snapshotPath, JSON.stringify(data));
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Incrementally hashes all files in a directory.
|
|
79
|
-
*
|
|
80
|
-
* Only files whose mtime or size changed since the last snapshot
|
|
81
|
-
* are re-read and re-hashed. Unchanged files reuse the cached hash.
|
|
82
|
-
*
|
|
83
|
-
* Returns a map of relative paths → hashes.
|
|
84
|
-
*/
|
|
85
|
-
async hashDirectory(directoryPath) {
|
|
86
|
-
await this.load();
|
|
87
|
-
const result = {};
|
|
88
|
-
const filePaths = await collectFiles(directoryPath, this.#ignoredDirs);
|
|
89
|
-
const BATCH_SIZE = 64;
|
|
90
|
-
const batches = [];
|
|
91
|
-
for (let i = 0; i < filePaths.length; i += BATCH_SIZE) {
|
|
92
|
-
batches.push(filePaths.slice(i, i + BATCH_SIZE));
|
|
93
|
-
}
|
|
94
|
-
for (const batch of batches) {
|
|
95
|
-
const batchResults = await Promise.all(
|
|
96
|
-
batch.map(async (filePath) => {
|
|
97
|
-
const hash = await this.#hashFileIncremental(filePath);
|
|
98
|
-
const relativePath = relative(this.#workspaceRoot, filePath);
|
|
99
|
-
return { hash, relativePath };
|
|
100
|
-
})
|
|
101
|
-
);
|
|
102
|
-
for (const { hash, relativePath } of batchResults) {
|
|
103
|
-
if (hash) {
|
|
104
|
-
result[relativePath] = hash;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
return result;
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Hashes a single file incrementally.
|
|
112
|
-
* Returns the cached hash if mtime + size haven't changed.
|
|
113
|
-
*/
|
|
114
|
-
async #hashFileIncremental(filePath) {
|
|
115
|
-
try {
|
|
116
|
-
const fileStat = await stat(filePath);
|
|
117
|
-
if (!fileStat.isFile()) {
|
|
118
|
-
return void 0;
|
|
119
|
-
}
|
|
120
|
-
const cached = this.#snapshot.get(filePath);
|
|
121
|
-
if (cached && cached.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size) {
|
|
122
|
-
return cached.hash;
|
|
123
|
-
}
|
|
124
|
-
const content = await readFile(filePath);
|
|
125
|
-
const hash = xxh3Hash(content);
|
|
126
|
-
this.#snapshot.set(filePath, {
|
|
127
|
-
hash,
|
|
128
|
-
mtimeMs: fileStat.mtimeMs,
|
|
129
|
-
size: fileStat.size
|
|
130
|
-
});
|
|
131
|
-
return hash;
|
|
132
|
-
} catch {
|
|
133
|
-
this.#snapshot.delete(filePath);
|
|
134
|
-
return void 0;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
/**
|
|
138
|
-
* Returns how many files are in the snapshot (for diagnostics).
|
|
139
|
-
*/
|
|
140
|
-
get snapshotSize() {
|
|
141
|
-
return this.#snapshot.size;
|
|
142
|
-
}
|
|
143
|
-
/**
|
|
144
|
-
* Clears the in-memory snapshot.
|
|
145
|
-
*/
|
|
146
|
-
clear() {
|
|
147
|
-
this.#snapshot.clear();
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export { IncrementalFileHasher };
|