@visulima/task-runner 1.0.0-alpha.3 → 1.0.0-alpha.4
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 +41 -0
- package/README.md +193 -51
- package/dist/affected.d.ts +37 -3
- package/dist/command-parser/expand-arguments.d.ts +11 -0
- package/dist/command-parser/expand-shortcut.d.ts +15 -0
- package/dist/command-parser/expand-wildcard.d.ts +13 -0
- package/dist/command-parser/index.d.ts +18 -0
- package/dist/command-parser/strip-quotes.d.ts +6 -0
- package/dist/concurrent-fallback.d.ts +16 -0
- package/dist/concurrent.d.ts +23 -0
- package/dist/detect-shell.d.ts +19 -0
- package/dist/flow-controllers/index.d.ts +7 -0
- package/dist/flow-controllers/input-handler.d.ts +44 -0
- package/dist/flow-controllers/log-timings.d.ts +18 -0
- package/dist/flow-controllers/restart-process.d.ts +21 -0
- package/dist/flow-controllers/teardown.d.ts +22 -0
- package/dist/index.d.ts +13 -3
- package/dist/index.js +26 -12
- package/dist/native-binding.d.ts +44 -2
- package/dist/packem_shared/{Cache-IYpTYVUC.js → Cache-C23LywYn.js} +2 -3
- package/dist/packem_shared/{FingerprintManager-D6Y0erg-.js → FingerprintManager-Cu-ta9ee.js} +0 -1
- package/dist/packem_shared/{IncrementalFileHasher-Ds3J6dgb.js → IncrementalFileHasher-Cm_kJY5V.js} +1 -1
- package/dist/packem_shared/{TaskOrchestrator-BvYs3ONw.js → TaskOrchestrator-lLn-PH1m.js} +2 -5
- package/dist/packem_shared/TerminalBuffer-D6zP2zLh.js +250 -0
- package/dist/packem_shared/{filterAffectedTasks-I-18zPg6.js → buildForwardDependencyMap-Cu08NWB1.js} +58 -20
- package/dist/packem_shared/{computeTaskHash-BoCnnvIJ.js → computeTaskHash-B2SVZqgp.js} +1 -2
- package/dist/packem_shared/createInputHandler-DTfePcTG.js +37 -0
- package/dist/packem_shared/{defaultTaskRunner-CrW4v5Ye.js → defaultTaskRunner-X1MIynHu.js} +6 -7
- package/dist/packem_shared/detectScriptShell-CR-xXKA4.js +53 -0
- package/dist/packem_shared/enforceProjectConstraints-_Ej0zHch.js +90 -0
- package/dist/packem_shared/expandArguments-0AwD2BIA.js +26 -0
- package/dist/packem_shared/expandShortcut-BVG05ee4.js +23 -0
- package/dist/packem_shared/expandWildcard-B0xN_knq.js +107 -0
- package/dist/packem_shared/{findCycle-DF4_BRdO.js → findCycle-DefgNYhg.js} +1 -1
- package/dist/packem_shared/formatTimingTable-3qtCM552.js +46 -0
- package/dist/packem_shared/isNativeAvailable-BpD28A6Z.js +44 -0
- package/dist/packem_shared/parseCommands-D-IgF8Zh.js +26 -0
- package/dist/packem_shared/{TaskScheduler-CJilHDta.js → parsePartition-C4-P5RjK.js} +44 -1
- package/dist/packem_shared/{projectGraphToDot-VdTjHcVp.js → projectGraphToDot-C8uYeaPo.js} +20 -3
- package/dist/packem_shared/runConcurrentFallback-3q46z4AS.js +357 -0
- package/dist/packem_shared/runConcurrently-ATDwJNR6.js +67 -0
- package/dist/packem_shared/runTeardown-BAezH79J.js +49 -0
- package/dist/packem_shared/stripQuotes-Cey-zwFf.js +9 -0
- package/dist/packem_shared/withRestart-BREjRJa4.js +49 -0
- package/dist/project-constraints.d.ts +16 -0
- package/dist/task-scheduler.d.ts +23 -0
- package/dist/terminal-buffer.d.ts +29 -0
- package/dist/types.d.ts +220 -1
- package/index.js +599 -0
- package/package.json +14 -13
- package/binding.js +0 -204
- package/dist/packem_shared/isNativeAvailable-BWhnZ4ES.js +0 -19
- package/dist/packem_shared/{RemoteCache-BDqrnDEi.js → RemoteCache-BFceSe4a.js} +1 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const INPUT_PREFIX_REGEX = /^(\S+?):(.+)/s;
|
|
2
|
+
const createInputHandler = (commands, options = {}) => {
|
|
3
|
+
const { defaultTarget = 0, inputStream = process.stdin, pauseOnFinish = true } = options;
|
|
4
|
+
const byIndex = /* @__PURE__ */ new Map();
|
|
5
|
+
const byName = /* @__PURE__ */ new Map();
|
|
6
|
+
for (const cmd of commands) {
|
|
7
|
+
byIndex.set(cmd.index, cmd);
|
|
8
|
+
if (cmd.name) {
|
|
9
|
+
byName.set(cmd.name, cmd);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const onData = (data) => {
|
|
13
|
+
const input = data.toString();
|
|
14
|
+
const match = INPUT_PREFIX_REGEX.exec(input);
|
|
15
|
+
if (match) {
|
|
16
|
+
const [, target, rest] = match;
|
|
17
|
+
const targetCmd = byName.get(target) ?? byIndex.get(Number(target));
|
|
18
|
+
if (targetCmd) {
|
|
19
|
+
targetCmd.stdin.write(rest);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const defaultCmd = byIndex.get(defaultTarget);
|
|
24
|
+
if (defaultCmd) {
|
|
25
|
+
defaultCmd.stdin.write(input);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
inputStream.on("data", onData);
|
|
29
|
+
return () => {
|
|
30
|
+
inputStream.removeListener("data", onData);
|
|
31
|
+
if (pauseOnFinish && typeof inputStream.pause === "function") {
|
|
32
|
+
inputStream.pause();
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export { createInputHandler };
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { Cache } from './Cache-
|
|
1
|
+
import { Cache } from './Cache-C23LywYn.js';
|
|
2
2
|
import { inferFrameworkEnvPatterns } from './detectFrameworks-CeFzKE6J.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { TaskScheduler } from './TaskScheduler-CJilHDta.js';
|
|
3
|
+
import { RemoteCache } from './RemoteCache-BFceSe4a.js';
|
|
4
|
+
import { InProcessTaskHasher } from './computeTaskHash-B2SVZqgp.js';
|
|
5
|
+
import { TaskOrchestrator } from './TaskOrchestrator-lLn-PH1m.js';
|
|
6
|
+
import { TaskScheduler } from './parsePartition-C4-P5RjK.js';
|
|
8
7
|
|
|
9
8
|
const resolveParallel = (parallel) => {
|
|
10
9
|
if (typeof parallel === "number") {
|
|
@@ -16,7 +15,7 @@ const resolveParallel = (parallel) => {
|
|
|
16
15
|
return 3;
|
|
17
16
|
};
|
|
18
17
|
const defaultTaskRunner = async (_tasks, options, context) => {
|
|
19
|
-
const { lifeCycle
|
|
18
|
+
const { lifeCycle, projectGraph, taskExecutor, taskGraph, workspaceRoot } = context;
|
|
20
19
|
const cache = new Cache({
|
|
21
20
|
cacheDirectory: options.cacheDirectory,
|
|
22
21
|
maxCacheAge: options.maxCacheAge,
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
execFileSync
|
|
22
|
+
} = __cjs_getBuiltinModule("node:child_process");
|
|
23
|
+
|
|
24
|
+
let cachedShellPath;
|
|
25
|
+
const detectScriptShell = () => {
|
|
26
|
+
if (cachedShellPath !== void 0) {
|
|
27
|
+
return cachedShellPath ?? void 0;
|
|
28
|
+
}
|
|
29
|
+
const envShell = process.env["npm_config_script_shell"];
|
|
30
|
+
if (envShell) {
|
|
31
|
+
cachedShellPath = envShell;
|
|
32
|
+
return envShell;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const result = execFileSync("npm", ["config", "get", "script-shell"], {
|
|
36
|
+
encoding: "utf8",
|
|
37
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
38
|
+
timeout: 5e3
|
|
39
|
+
}).trim();
|
|
40
|
+
if (result && result !== "undefined" && result !== "") {
|
|
41
|
+
cachedShellPath = result;
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
}
|
|
46
|
+
cachedShellPath = null;
|
|
47
|
+
return void 0;
|
|
48
|
+
};
|
|
49
|
+
const resetShellCache = () => {
|
|
50
|
+
cachedShellPath = void 0;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export { detectScriptShell, resetShellCache };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const enforceProjectConstraints = (projectGraph, constraints) => {
|
|
2
|
+
const violations = [];
|
|
3
|
+
const { dependencyKindRules, tagRelationships, typeBoundaries } = constraints;
|
|
4
|
+
const hasTagRules = tagRelationships && Object.keys(tagRelationships).length > 0;
|
|
5
|
+
const hasTypeBoundaries = typeBoundaries !== void 0;
|
|
6
|
+
const hasKindRules = dependencyKindRules !== void 0;
|
|
7
|
+
if (!hasTagRules && !hasTypeBoundaries && !hasKindRules) {
|
|
8
|
+
return violations;
|
|
9
|
+
}
|
|
10
|
+
const enforceAppBoundary = typeBoundaries?.enforceApplicationBoundary !== false;
|
|
11
|
+
const allowedDepTypes = typeBoundaries?.allowedDependencyTypes;
|
|
12
|
+
for (const [projectName, dependencies] of Object.entries(projectGraph.dependencies)) {
|
|
13
|
+
const sourceNode = projectGraph.nodes[projectName];
|
|
14
|
+
if (!sourceNode) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const sourceTags = sourceNode.data.tags ?? [];
|
|
18
|
+
const sourceType = sourceNode.type;
|
|
19
|
+
for (const dep of dependencies) {
|
|
20
|
+
const depNode = projectGraph.nodes[dep.target];
|
|
21
|
+
if (!depNode) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const depTags = depNode.data.tags ?? [];
|
|
25
|
+
const depType = depNode.type;
|
|
26
|
+
let appBoundaryViolated = false;
|
|
27
|
+
if (hasTypeBoundaries && enforceAppBoundary && depType === "application") {
|
|
28
|
+
appBoundaryViolated = true;
|
|
29
|
+
violations.push({
|
|
30
|
+
dependencyProject: dep.target,
|
|
31
|
+
message: `Project "${projectName}" depends on "${dep.target}", which is an application. Applications should not be depended upon by other projects.`,
|
|
32
|
+
rule: "type-boundary",
|
|
33
|
+
sourceProject: projectName
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
if (allowedDepTypes && !appBoundaryViolated) {
|
|
37
|
+
const allowed = allowedDepTypes[sourceType];
|
|
38
|
+
if (allowed && !allowed.includes(depType)) {
|
|
39
|
+
violations.push({
|
|
40
|
+
dependencyProject: dep.target,
|
|
41
|
+
message: `Project "${projectName}" (type: ${sourceType}) depends on "${dep.target}" (type: ${depType}). Allowed dependency types for "${sourceType}" are: ${allowed.join(", ")}.`,
|
|
42
|
+
rule: "type-boundary",
|
|
43
|
+
sourceProject: projectName
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (hasTagRules && tagRelationships) {
|
|
48
|
+
for (const sourceTag of sourceTags) {
|
|
49
|
+
const requiredTags = tagRelationships[sourceTag];
|
|
50
|
+
if (!requiredTags || requiredTags.length === 0) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const hasRequiredTag = depTags.some((tag) => requiredTags.includes(tag));
|
|
54
|
+
if (!hasRequiredTag) {
|
|
55
|
+
violations.push({
|
|
56
|
+
dependencyProject: dep.target,
|
|
57
|
+
message: `Project "${projectName}" (tag: ${sourceTag}) depends on "${dep.target}", which doesn't have any of the required tags: ${requiredTags.join(", ")}. ${depTags.length > 0 ? `"${dep.target}" has tags: ${depTags.join(", ")}.` : `"${dep.target}" has no tags.`}`,
|
|
58
|
+
rule: "tag-relationship",
|
|
59
|
+
sourceProject: projectName
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (hasKindRules && dependencyKindRules) {
|
|
65
|
+
if (dependencyKindRules.noProductionDependencyOnApplication && dep.type === "static" && depType === "application") {
|
|
66
|
+
violations.push({
|
|
67
|
+
dependencyProject: dep.target,
|
|
68
|
+
message: `Project "${projectName}" has a production dependency on "${dep.target}", which is an application. Production dependencies on applications are not allowed. Use devDependencies instead if needed for testing.`,
|
|
69
|
+
rule: "dependency-kind",
|
|
70
|
+
sourceProject: projectName
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
if (dependencyKindRules.noDevDependencyOnProductionDep && dep.type === "devDependency" && sourceType === "library") {
|
|
74
|
+
const hasProductionDep = dependencies.some((other) => other.target === dep.target && other.type === "static");
|
|
75
|
+
if (hasProductionDep) {
|
|
76
|
+
violations.push({
|
|
77
|
+
dependencyProject: dep.target,
|
|
78
|
+
message: `Project "${projectName}" has "${dep.target}" in both dependencies and devDependencies. This is redundant — remove it from devDependencies.`,
|
|
79
|
+
rule: "dependency-kind",
|
|
80
|
+
sourceProject: projectName
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return violations;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export { enforceProjectConstraints };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const PLACEHOLDER_REGEX = /\\?\{([@*]|[1-9]\d*)\}/g;
|
|
2
|
+
const shellQuote = (s) => `'${s.replaceAll("'", String.raw`'\''`)}'`;
|
|
3
|
+
const expandArguments = (config, additionalArguments) => {
|
|
4
|
+
if (additionalArguments.length === 0) {
|
|
5
|
+
return config;
|
|
6
|
+
}
|
|
7
|
+
const command = config.command.replaceAll(PLACEHOLDER_REGEX, (match, target) => {
|
|
8
|
+
if (match.startsWith("\\")) {
|
|
9
|
+
return match.slice(1);
|
|
10
|
+
}
|
|
11
|
+
const index = Number(target);
|
|
12
|
+
if (!Number.isNaN(index) && index <= additionalArguments.length) {
|
|
13
|
+
return shellQuote(additionalArguments[index - 1]);
|
|
14
|
+
}
|
|
15
|
+
if (target === "@") {
|
|
16
|
+
return additionalArguments.map(shellQuote).join(" ");
|
|
17
|
+
}
|
|
18
|
+
if (target === "*") {
|
|
19
|
+
return shellQuote(additionalArguments.join(" "));
|
|
20
|
+
}
|
|
21
|
+
return "";
|
|
22
|
+
});
|
|
23
|
+
return { ...config, command };
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export { expandArguments };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const SHORTCUT_REGEX = /^(npm|yarn|pnpm|bun|node|deno):(\S+)(.*)/;
|
|
2
|
+
const expandShortcut = (config) => {
|
|
3
|
+
const match = SHORTCUT_REGEX.exec(config.command);
|
|
4
|
+
if (!match) {
|
|
5
|
+
return config;
|
|
6
|
+
}
|
|
7
|
+
const [, prefix, script, args] = match;
|
|
8
|
+
let runPrefix;
|
|
9
|
+
if (prefix === "node") {
|
|
10
|
+
runPrefix = "node --run";
|
|
11
|
+
} else if (prefix === "deno") {
|
|
12
|
+
runPrefix = "deno task";
|
|
13
|
+
} else {
|
|
14
|
+
runPrefix = `${prefix} run`;
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
...config,
|
|
18
|
+
command: `${runPrefix} ${script}${args}`,
|
|
19
|
+
name: config.name ?? script
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export { expandShortcut };
|
|
@@ -0,0 +1,107 @@
|
|
|
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
|
+
existsSync,
|
|
22
|
+
readFileSync
|
|
23
|
+
} = __cjs_getBuiltinModule("node:fs");
|
|
24
|
+
const {
|
|
25
|
+
join
|
|
26
|
+
} = __cjs_getBuiltinModule("node:path");
|
|
27
|
+
|
|
28
|
+
const RUN_COMMAND_REGEX = /(?:npm|yarn|pnpm|bun) run|node --run|deno task/;
|
|
29
|
+
const readPackageScripts = (cwd) => {
|
|
30
|
+
try {
|
|
31
|
+
const raw = readFileSync(join(cwd, "package.json"), "utf8");
|
|
32
|
+
const pkg = JSON.parse(raw);
|
|
33
|
+
return pkg.scripts ?? {};
|
|
34
|
+
} catch {
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const readDenoTasks = (cwd) => {
|
|
39
|
+
const tasks = {};
|
|
40
|
+
for (const filename of ["deno.json", "deno.jsonc"]) {
|
|
41
|
+
const filepath = join(cwd, filename);
|
|
42
|
+
if (existsSync(filepath)) {
|
|
43
|
+
try {
|
|
44
|
+
let raw = readFileSync(filepath, "utf8");
|
|
45
|
+
if (filename.endsWith("c")) {
|
|
46
|
+
raw = raw.replaceAll(/"(?:[^"\\]|\\.)*"|\/\/[^\n]*/g, (match) => match.startsWith('"') ? match : "");
|
|
47
|
+
}
|
|
48
|
+
const config = JSON.parse(raw);
|
|
49
|
+
if (config.tasks) {
|
|
50
|
+
Object.assign(tasks, config.tasks);
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
}
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const pkgScripts = readPackageScripts(cwd);
|
|
58
|
+
return { ...pkgScripts, ...tasks };
|
|
59
|
+
};
|
|
60
|
+
const escapeRegExp = (s) => s.replaceAll(/[$()*+.?[\\\]^{|}]/g, String.raw`\$&`);
|
|
61
|
+
const expandWildcard = (config) => {
|
|
62
|
+
const { command } = config;
|
|
63
|
+
const runMatch = RUN_COMMAND_REGEX.exec(command);
|
|
64
|
+
if (!runMatch) {
|
|
65
|
+
return config;
|
|
66
|
+
}
|
|
67
|
+
const afterRun = command.slice(runMatch.index + runMatch[0].length).trim();
|
|
68
|
+
const scriptPattern = afterRun.split(/\s/)[0] ?? "";
|
|
69
|
+
if (!scriptPattern.includes("*")) {
|
|
70
|
+
return config;
|
|
71
|
+
}
|
|
72
|
+
const cwd = config.cwd ?? process.cwd();
|
|
73
|
+
const isDeno = runMatch[0] === "deno task";
|
|
74
|
+
const scripts = isDeno ? readDenoTasks(cwd) : readPackageScripts(cwd);
|
|
75
|
+
const scriptNames = Object.keys(scripts);
|
|
76
|
+
const parts = scriptPattern.split("*");
|
|
77
|
+
const regexString = parts.map(escapeRegExp).join("(.+)");
|
|
78
|
+
const wildcardRegex = new RegExp(`^${regexString}$`);
|
|
79
|
+
const omitMatch = /!\(([^)]+)\)/.exec(scriptPattern);
|
|
80
|
+
let omitRegex;
|
|
81
|
+
if (omitMatch) {
|
|
82
|
+
omitRegex = new RegExp(omitMatch[1]);
|
|
83
|
+
}
|
|
84
|
+
const matching = scriptNames.filter((name) => {
|
|
85
|
+
if (!wildcardRegex.test(name)) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
if (omitRegex?.test(name)) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
return true;
|
|
92
|
+
});
|
|
93
|
+
if (matching.length === 0) {
|
|
94
|
+
return config;
|
|
95
|
+
}
|
|
96
|
+
const remainingArgs = afterRun.slice(scriptPattern.length);
|
|
97
|
+
const runPrefix = command.slice(0, runMatch.index + runMatch[0].length);
|
|
98
|
+
return matching.map((scriptName) => {
|
|
99
|
+
return {
|
|
100
|
+
...config,
|
|
101
|
+
command: `${runPrefix} ${scriptName}${remainingArgs}`,
|
|
102
|
+
name: config.name ?? scriptName
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export { expandWildcard };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const formatDuration = (ms) => {
|
|
2
|
+
if (ms < 1e3) {
|
|
3
|
+
return `${Math.round(ms)}ms`;
|
|
4
|
+
}
|
|
5
|
+
if (ms < 6e4) {
|
|
6
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
7
|
+
}
|
|
8
|
+
const minutes = Math.floor(ms / 6e4);
|
|
9
|
+
const seconds = (ms % 6e4 / 1e3).toFixed(1);
|
|
10
|
+
return `${minutes}m ${seconds}s`;
|
|
11
|
+
};
|
|
12
|
+
const formatTimingTable = (closeEvents) => {
|
|
13
|
+
if (closeEvents.length === 0) {
|
|
14
|
+
return "";
|
|
15
|
+
}
|
|
16
|
+
const sorted = closeEvents.toSorted((a, b) => b.durationMs - a.durationMs);
|
|
17
|
+
const nameWidth = Math.max(4, ...sorted.map((e) => (e.name ?? String(e.index)).length));
|
|
18
|
+
const durationWidth = Math.max(8, ...sorted.map((e) => formatDuration(e.durationMs).length));
|
|
19
|
+
const codeWidth = Math.max(4, ...sorted.map((e) => String(e.exitCode).length));
|
|
20
|
+
const header = ["name".padEnd(nameWidth), "duration".padEnd(durationWidth), "code".padEnd(codeWidth), "killed", "command"].join(" │ ");
|
|
21
|
+
const separator = ["─".repeat(nameWidth), "─".repeat(durationWidth), "─".repeat(codeWidth), "─".repeat(6), "─".repeat(20)].join(
|
|
22
|
+
"─┼─"
|
|
23
|
+
);
|
|
24
|
+
const rows = sorted.map((event) => {
|
|
25
|
+
const name = (event.name ?? String(event.index)).padEnd(nameWidth);
|
|
26
|
+
const duration = formatDuration(event.durationMs).padEnd(durationWidth);
|
|
27
|
+
const code = String(event.exitCode).padEnd(codeWidth);
|
|
28
|
+
const killed = (event.killed ? "yes" : "no").padEnd(6);
|
|
29
|
+
const command = event.command.length > 40 ? `${event.command.slice(0, 39)}…` : event.command;
|
|
30
|
+
return [name, duration, code, killed, command].join(" │ ");
|
|
31
|
+
});
|
|
32
|
+
return [header, separator, ...rows].join("\n");
|
|
33
|
+
};
|
|
34
|
+
const logTimings = (closeEvents, output = process.stdout) => {
|
|
35
|
+
if (closeEvents.length === 0) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const table = formatTimingTable(closeEvents);
|
|
39
|
+
output.write(
|
|
40
|
+
"\n── Timing Summary ───────────────────────────────────\n\n"
|
|
41
|
+
);
|
|
42
|
+
output.write(table);
|
|
43
|
+
output.write("\n\n");
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export { formatTimingTable, logTimings };
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
createRequire
|
|
22
|
+
} = __cjs_getBuiltinModule("node:module");
|
|
23
|
+
|
|
24
|
+
let nativeBindings;
|
|
25
|
+
let loadAttempted = false;
|
|
26
|
+
const esmRequire = createRequire(import.meta.url);
|
|
27
|
+
const loadNativeBindings = () => {
|
|
28
|
+
if (loadAttempted) {
|
|
29
|
+
return nativeBindings;
|
|
30
|
+
}
|
|
31
|
+
loadAttempted = true;
|
|
32
|
+
try {
|
|
33
|
+
const loaded = esmRequire("../index.js");
|
|
34
|
+
if (typeof loaded.hashCommand === "function" && typeof loaded.hashFile === "function" && typeof loaded.runConcurrent === "function") {
|
|
35
|
+
nativeBindings = loaded;
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
nativeBindings = void 0;
|
|
39
|
+
}
|
|
40
|
+
return nativeBindings;
|
|
41
|
+
};
|
|
42
|
+
const isNativeAvailable = () => loadNativeBindings() !== void 0;
|
|
43
|
+
|
|
44
|
+
export { isNativeAvailable, loadNativeBindings };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { expandArguments } from './expandArguments-0AwD2BIA.js';
|
|
2
|
+
import { expandShortcut } from './expandShortcut-BVG05ee4.js';
|
|
3
|
+
import { expandWildcard } from './expandWildcard-B0xN_knq.js';
|
|
4
|
+
import { stripQuotes } from './stripQuotes-Cey-zwFf.js';
|
|
5
|
+
|
|
6
|
+
const parseCommands = (inputs, options = {}) => {
|
|
7
|
+
const { additionalArguments = [] } = options;
|
|
8
|
+
let configs = inputs.map((input) => {
|
|
9
|
+
if (typeof input === "string") {
|
|
10
|
+
return { command: input };
|
|
11
|
+
}
|
|
12
|
+
return { ...input };
|
|
13
|
+
});
|
|
14
|
+
configs = configs.map(stripQuotes);
|
|
15
|
+
configs = configs.map(expandShortcut);
|
|
16
|
+
configs = configs.flatMap((config) => {
|
|
17
|
+
const result = expandWildcard(config);
|
|
18
|
+
return Array.isArray(result) ? result : [result];
|
|
19
|
+
});
|
|
20
|
+
if (additionalArguments.length > 0) {
|
|
21
|
+
configs = configs.map((config) => expandArguments(config, additionalArguments));
|
|
22
|
+
}
|
|
23
|
+
return configs;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export { expandArguments, expandShortcut, expandWildcard, parseCommands, stripQuotes };
|
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
const parsePartition = (value) => {
|
|
2
|
+
const raw = value ?? process.env.VIS_PARTITION;
|
|
3
|
+
if (!raw) {
|
|
4
|
+
return void 0;
|
|
5
|
+
}
|
|
6
|
+
const parts = raw.split("/");
|
|
7
|
+
if (parts.length !== 2) {
|
|
8
|
+
throw new Error(`Invalid partition format: "${raw}". Expected format: "index/total" (e.g., "1/4").`);
|
|
9
|
+
}
|
|
10
|
+
const index = Number(parts[0]);
|
|
11
|
+
const total = Number(parts[1]);
|
|
12
|
+
if (!Number.isInteger(index) || !Number.isInteger(total) || index < 1 || total < 1) {
|
|
13
|
+
throw new Error(`Invalid partition values: "${raw}". Both index and total must be positive integers.`);
|
|
14
|
+
}
|
|
15
|
+
if (index > total) {
|
|
16
|
+
throw new Error(`Invalid partition index: ${index} exceeds total ${total}.`);
|
|
17
|
+
}
|
|
18
|
+
return { index, total };
|
|
19
|
+
};
|
|
1
20
|
const calculateProjectDepths = (projectGraph) => {
|
|
2
21
|
const depths = /* @__PURE__ */ new Map();
|
|
3
22
|
const visited = /* @__PURE__ */ new Set();
|
|
@@ -31,6 +50,30 @@ class TaskScheduler {
|
|
|
31
50
|
#totalTasks;
|
|
32
51
|
#dependentCounts;
|
|
33
52
|
#projectDepths;
|
|
53
|
+
/**
|
|
54
|
+
* Partitions a list of tasks for distributed CI execution.
|
|
55
|
+
* Tasks are sorted by ID for deterministic distribution, then split
|
|
56
|
+
* using ceiling division so partitions differ by at most one task.
|
|
57
|
+
* @param tasks The full list of tasks to partition
|
|
58
|
+
* @param partition The partition configuration (1-based index and total)
|
|
59
|
+
* @returns The subset of tasks assigned to this partition
|
|
60
|
+
*/
|
|
61
|
+
static partitionTasks(tasks, partition) {
|
|
62
|
+
if (partition.total < 1) {
|
|
63
|
+
throw new Error(`Invalid partition total: ${partition.total}. Must be at least 1.`);
|
|
64
|
+
}
|
|
65
|
+
if (partition.index < 1 || partition.index > partition.total) {
|
|
66
|
+
throw new Error(`Invalid partition index: ${partition.index}. Must be between 1 and ${partition.total}.`);
|
|
67
|
+
}
|
|
68
|
+
if (tasks.length === 0) {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
const sorted = [...tasks].toSorted((a, b) => a.id.localeCompare(b.id));
|
|
72
|
+
const size = Math.ceil(sorted.length / partition.total);
|
|
73
|
+
const start = size * (partition.index - 1);
|
|
74
|
+
const end = partition.index === partition.total ? sorted.length : size * partition.index;
|
|
75
|
+
return sorted.slice(start, end);
|
|
76
|
+
}
|
|
34
77
|
constructor(taskGraph, projectGraph, maxParallel = 3) {
|
|
35
78
|
this.#taskGraph = taskGraph;
|
|
36
79
|
this.#maxParallel = maxParallel;
|
|
@@ -108,4 +151,4 @@ class TaskScheduler {
|
|
|
108
151
|
}
|
|
109
152
|
}
|
|
110
153
|
|
|
111
|
-
export { TaskScheduler };
|
|
154
|
+
export { TaskScheduler, parsePartition };
|
|
@@ -153,7 +153,7 @@ const toGraphHtml = (taskGraph, options = {}) => {
|
|
|
153
153
|
graphData.edges = graphData.edges.filter((e) => focused.has(e.source) && focused.has(e.target));
|
|
154
154
|
graphData.roots = graphData.roots.filter((r) => focused.has(r));
|
|
155
155
|
}
|
|
156
|
-
return String.raw(_a || (_a = __template(['<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8">\n<title>Task Graph</title>\n<style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body { font-family: system-ui, -apple-system, sans-serif; background: #1a1a2e; color: #eee; }\n svg { width: 100vw; height: 100vh; }\n .node rect { rx: 6; ry: 6; stroke: #555; stroke-width: 1.5; cursor: pointer; }\n .node text { font-size: 11px; fill: #1a1a2e; font-weight: 600; pointer-events: none; }\n .node:hover rect { stroke: #fff; stroke-width: 2; }\n .edge { stroke: #444; stroke-width: 1.5; fill: none; marker-end: url(#arrow); }\n .label { font-size: 10px; fill: #888; }\n #info { position: fixed; top: 12px; right: 12px; background: #16213e; padding: 12px 16px; border-radius: 8px; font-size: 13px; }\n #info b { color: #e94560; }\n</style>\n</head>\n<body>\n<div id="info">\n <b>', "</b> tasks · <b>", "</b> dependencies · <b>", '</b> roots\n</div>\n<svg id="graph">\n <defs>\n <marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto">\n <path d="M 0 0 L 10 5 L 0 10 z" fill="#666"/>\n </marker>\n </defs>\n</svg>\n<script>\nconst data = ', ";\nconst svg = document.getElementById('graph');\nconst W = window.innerWidth, H = window.innerHeight;\nconst statusColors = {\n success: '#2ecc71', 'local-cache': '#3498db', 'remote-cache': '#9b59b6',\n failure: '#e74c3c', running: '#f39c12', skipped: '#95a5a6', pending: '#ecf0f1'\n};\nconst projectColors = {};\nconst palette = ['#e94560','#0f3460','#533483','#16c79a','#f39c12','#2ecc71','#3498db','#e67e22','#9b59b6','#1abc9c'];\nlet ci = 0;\ndata.nodes.forEach(n => {\n if (!projectColors[n.project]) projectColors[n.project] = palette[ci++ % palette.length];\n});\n\n// Simple force-directed layout\nconst nodes = data.nodes.map((n, i) => ({\n ...n, x: W/2 + (Math.random()-0.5)*400, y: H/2 + (Math.random()-0.5)*300, vx: 0, vy: 0\n}));\nconst nodeMap = new Map(nodes.map(n => [n.id, n]));\nconst edges = data.edges.map(e => ({ source: nodeMap.get(e.source), target: nodeMap.get(e.target) }));\n\nfunction simulate() {\n for (let iter = 0; iter < 300; iter++) {\n // Repulsion\n for (let i = 0; i < nodes.length; i++) {\n for (let j = i+1; j < nodes.length; j++) {\n let dx = nodes[j].x - nodes[i].x, dy = nodes[j].y - nodes[i].y;\n let d = Math.sqrt(dx*dx + dy*dy) || 1;\n let f = 8000 / (d * d);\n nodes[i].vx -= dx/d * f; nodes[i].vy -= dy/d * f;\n nodes[j].vx += dx/d * f; nodes[j].vy += dy/d * f;\n }\n }\n // Attraction (edges)\n edges.forEach(e => {\n if (!e.source || !e.target) return;\n let dx = e.target.x - e.source.x, dy = e.target.y - e.source.y;\n let d = Math.sqrt(dx*dx + dy*dy) || 1;\n let f = (d - 150) * 0.01;\n e.source.vx += dx/d * f; e.source.vy += dy/d * f;\n e.target.vx -= dx/d * f; e.target.vy -= dy/d * f;\n });\n // Gravity\n nodes.forEach(n => {\n n.vx += (W/2 - n.x) * 0.001;\n n.vy += (H/2 - n.y) * 0.001;\n n.x += n.vx * 0.3; n.y += n.vy * 0.3;\n n.vx *= 0.8; n.vy *= 0.8;\n n.x = Math.max(60, Math.min(W-60, n.x));\n n.y = Math.max(30, Math.min(H-30, n.y));\n });\n }\n}\nsimulate();\n\n// Render\nedges.forEach(e => {\n if (!e.source || !e.target) return;\n const line = document.createElementNS('http://www.w3.org/2000/svg','line');\n line.setAttribute('x1', e.source.x); line.setAttribute('y1', e.source.y);\n line.setAttribute('x2', e.target.x); line.setAttribute('y2', e.target.y);\n line.setAttribute('class','edge');\n svg.appendChild(line);\n});\nnodes.forEach(n => {\n const g = document.createElementNS('http://www.w3.org/2000/svg','g');\n g.setAttribute('class','node');\n g.setAttribute('transform','translate('+(n.x-50)+','+(n.y-14)+')');\n const rect = document.createElementNS('http://www.w3.org/2000/svg','rect');\n rect.setAttribute('width','100'); rect.setAttribute('height','28');\n rect.setAttribute('fill', n.status ? (statusColors[n.status]||'#ecf0f1') : projectColors[n.project]);\n g.appendChild(rect);\n const text = document.createElementNS('http://www.w3.org/2000/svg','text');\n text.setAttribute('x','50'); text.setAttribute('y','18'); text.setAttribute('text-anchor','middle');\n text.textContent = n.id.length > 14 ? n.target : n.id;\n g.appendChild(text);\n g.addEventListener('click', () => {\n const deps = data.edges.filter(e => e.source === n.id).map(e => e.target);\n const rdeps = data.edges.filter(e => e.target === n.id).map(e => e.source);\n alert(n.id + '\n\nDepends on: ' + (deps.join(', ')||'none') + '\nRequired by: ' + (rdeps.join(', ')||'none'));\n });\n svg.appendChild(g);\n});\n<\/script>\n</body>\n</html>"], ['<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8">\n<title>Task Graph</title>\n<style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body { font-family: system-ui, -apple-system, sans-serif; background: #1a1a2e; color: #eee; }\n svg { width: 100vw; height: 100vh; }\n .node rect { rx: 6; ry: 6; stroke: #555; stroke-width: 1.5; cursor: pointer; }\n .node text { font-size: 11px; fill: #1a1a2e; font-weight: 600; pointer-events: none; }\n .node:hover rect { stroke: #fff; stroke-width: 2; }\n .edge { stroke: #444; stroke-width: 1.5; fill: none; marker-end: url(#arrow); }\n .label { font-size: 10px; fill: #888; }\n #info { position: fixed; top: 12px; right: 12px; background: #16213e; padding: 12px 16px; border-radius: 8px; font-size: 13px; }\n #info b { color: #e94560; }\n</style>\n</head>\n<body>\n<div id="info">\n <b>', "</b> tasks · <b>", "</b> dependencies · <b>", '</b> roots\n</div>\n<svg id="graph">\n <defs>\n <marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto">\n <path d="M 0 0 L 10 5 L 0 10 z" fill="#666"/>\n </marker>\n </defs>\n</svg>\n<script>\nconst data = ', ";\nconst svg = document.getElementById('graph');\nconst W = window.innerWidth, H = window.innerHeight;\nconst statusColors = {\n success: '#2ecc71', 'local-cache': '#3498db', 'remote-cache': '#9b59b6',\n failure: '#e74c3c', running: '#f39c12', skipped: '#95a5a6', pending: '#ecf0f1'\n};\nconst projectColors = {};\nconst palette = ['#e94560','#0f3460','#533483','#16c79a','#f39c12','#2ecc71','#3498db','#e67e22','#9b59b6','#1abc9c'];\nlet ci = 0;\ndata.nodes.forEach(n => {\n if (!projectColors[n.project]) projectColors[n.project] = palette[ci++ % palette.length];\n});\n\n// Simple force-directed layout\nconst nodes = data.nodes.map((n, i) => ({\n ...n, x: W/2 + (Math.random()-0.5)*400, y: H/2 + (Math.random()-0.5)*300, vx: 0, vy: 0\n}));\nconst nodeMap = new Map(nodes.map(n => [n.id, n]));\nconst edges = data.edges.map(e => ({ source: nodeMap.get(e.source), target: nodeMap.get(e.target) }));\n\nfunction simulate() {\n for (let iter = 0; iter < 300; iter++) {\n // Repulsion\n for (let i = 0; i < nodes.length; i++) {\n for (let j = i+1; j < nodes.length; j++) {\n let dx = nodes[j].x - nodes[i].x, dy = nodes[j].y - nodes[i].y;\n let d = Math.sqrt(dx*dx + dy*dy) || 1;\n let f = 8000 / (d * d);\n nodes[i].vx -= dx/d * f; nodes[i].vy -= dy/d * f;\n nodes[j].vx += dx/d * f; nodes[j].vy += dy/d * f;\n }\n }\n // Attraction (edges)\n edges.forEach(e => {\n if (!e.source || !e.target) return;\n let dx = e.target.x - e.source.x, dy = e.target.y - e.source.y;\n let d = Math.sqrt(dx*dx + dy*dy) || 1;\n let f = (d - 150) * 0.01;\n e.source.vx += dx/d * f; e.source.vy += dy/d * f;\n e.target.vx -= dx/d * f; e.target.vy -= dy/d * f;\n });\n // Gravity\n nodes.forEach(n => {\n n.vx += (W/2 - n.x) * 0.001;\n n.vy += (H/2 - n.y) * 0.001;\n n.x += n.vx * 0.3; n.y += n.vy * 0.3;\n n.vx *= 0.8; n.vy *= 0.8;\n n.x = Math.max(60, Math.min(W-60, n.x));\n n.y = Math.max(30, Math.min(H-30, n.y));\n });\n }\n}\nsimulate();\n\n// Render\nedges.forEach(e => {\n if (!e.source || !e.target) return;\n const line = document.createElementNS('http://www.w3.org/2000/svg','line');\n line.setAttribute('x1', e.source.x); line.setAttribute('y1', e.source.y);\n line.setAttribute('x2', e.target.x); line.setAttribute('y2', e.target.y);\n line.setAttribute('class','edge');\n svg.appendChild(line);\n});\nnodes.forEach(n => {\n const g = document.createElementNS('http://www.w3.org/2000/svg','g');\n g.setAttribute('class','node');\n g.setAttribute('transform','translate('+(n.x-50)+','+(n.y-14)+')');\n const rect = document.createElementNS('http://www.w3.org/2000/svg','rect');\n rect.setAttribute('width','100'); rect.setAttribute('height','28');\n rect.setAttribute('fill', n.status ? (statusColors[n.status]||'#ecf0f1') : projectColors[n.project]);\n g.appendChild(rect);\n const text = document.createElementNS('http://www.w3.org/2000/svg','text');\n text.setAttribute('x','50'); text.setAttribute('y','18'); text.setAttribute('text-anchor','middle');\n text.textContent = n.id.length > 14 ? n.target : n.id;\n g.appendChild(text);\n g.addEventListener('click', () => {\n const deps = data.edges.filter(e => e.source === n.id).map(e => e.target);\n const rdeps = data.edges.filter(e => e.target === n.id).map(e => e.source);\n alert(n.id + '\\n\\nDepends on: ' + (deps.join(', ')||'none') + '\\nRequired by: ' + (rdeps.join(', ')||'none'));\n });\n svg.appendChild(g);\n});\n<\/script>\n</body>\n</html>"])), graphData.nodes.length, graphData.edges.length, graphData.roots.length, JSON.stringify(graphData).replaceAll("</", String.raw`<\/`));
|
|
156
|
+
return String.raw(_a || (_a = __template(['<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8">\n<title>Task Graph</title>\n<style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body { font-family: system-ui, -apple-system, sans-serif; background: #1a1a2e; color: #eee; }\n svg { width: 100vw; height: 100vh; }\n .node rect { rx: 6; ry: 6; stroke: #555; stroke-width: 1.5; cursor: pointer; }\n .node text { font-size: 11px; fill: #1a1a2e; font-weight: 600; pointer-events: none; }\n .node:hover rect { stroke: #fff; stroke-width: 2; }\n .edge { stroke: #444; stroke-width: 1.5; fill: none; marker-end: url(#arrow); }\n .label { font-size: 10px; fill: #888; }\n #info { position: fixed; top: 12px; right: 12px; background: #16213e; padding: 12px 16px; border-radius: 8px; font-size: 13px; }\n #info b { color: #e94560; }\n</style>\n</head>\n<body>\n<div id="info">\n <b>', "</b> tasks · <b>", "</b> dependencies · <b>", '</b> roots\n</div>\n<svg id="graph">\n <defs>\n <marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto">\n <path d="M 0 0 L 10 5 L 0 10 z" fill="#666"/>\n </marker>\n </defs>\n</svg>\n<script>\nconst data = ', ";\nconst svg = document.getElementById('graph');\nconst W = window.innerWidth, H = window.innerHeight;\nconst statusColors = {\n success: '#2ecc71', 'local-cache': '#3498db', 'remote-cache': '#9b59b6',\n failure: '#e74c3c', running: '#f39c12', skipped: '#95a5a6', pending: '#ecf0f1'\n};\nconst projectColors = {};\nconst palette = ['#e94560','#0f3460','#533483','#16c79a','#f39c12','#2ecc71','#3498db','#e67e22','#9b59b6','#1abc9c'];\nlet ci = 0;\ndata.nodes.forEach(n => {\n if (!projectColors[n.project]) projectColors[n.project] = palette[ci++ % palette.length];\n});\n\n// Simple force-directed layout\nconst nodes = data.nodes.map((n, i) => ({\n ...n, x: W/2 + (Math.random()-0.5)*400, y: H/2 + (Math.random()-0.5)*300, vx: 0, vy: 0\n}));\nconst nodeMap = new Map(nodes.map(n => [n.id, n]));\nconst edges = data.edges.map(e => ({ source: nodeMap.get(e.source), target: nodeMap.get(e.target) }));\n\nfunction simulate() {\n for (let iter = 0; iter < 300; iter++) {\n // Repulsion\n for (let i = 0; i < nodes.length; i++) {\n for (let j = i+1; j < nodes.length; j++) {\n let dx = nodes[j].x - nodes[i].x, dy = nodes[j].y - nodes[i].y;\n let d = Math.sqrt(dx*dx + dy*dy) || 1;\n let f = 8000 / (d * d);\n nodes[i].vx -= dx/d * f; nodes[i].vy -= dy/d * f;\n nodes[j].vx += dx/d * f; nodes[j].vy += dy/d * f;\n }\n }\n // Attraction (edges)\n edges.forEach(e => {\n if (!e.source || !e.target) {\n return;\n }\n let dx = e.target.x - e.source.x, dy = e.target.y - e.source.y;\n let d = Math.sqrt(dx*dx + dy*dy) || 1;\n let f = (d - 150) * 0.01;\n e.source.vx += dx/d * f; e.source.vy += dy/d * f;\n e.target.vx -= dx/d * f; e.target.vy -= dy/d * f;\n });\n // Gravity\n nodes.forEach(n => {\n n.vx += (W/2 - n.x) * 0.001;\n n.vy += (H/2 - n.y) * 0.001;\n n.x += n.vx * 0.3; n.y += n.vy * 0.3;\n n.vx *= 0.8; n.vy *= 0.8;\n n.x = Math.max(60, Math.min(W-60, n.x));\n n.y = Math.max(30, Math.min(H-30, n.y));\n });\n }\n}\nsimulate();\n\n// Render\nedges.forEach(e => {\n if (!e.source || !e.target) {\n return;\n }\n const line = document.createElementNS('http://www.w3.org/2000/svg','line');\n line.setAttribute('x1', e.source.x); line.setAttribute('y1', e.source.y);\n line.setAttribute('x2', e.target.x); line.setAttribute('y2', e.target.y);\n line.setAttribute('class','edge');\n svg.appendChild(line);\n});\nnodes.forEach(n => {\n const g = document.createElementNS('http://www.w3.org/2000/svg','g');\n g.setAttribute('class','node');\n g.setAttribute('transform','translate('+(n.x-50)+','+(n.y-14)+')');\n const rect = document.createElementNS('http://www.w3.org/2000/svg','rect');\n rect.setAttribute('width','100'); rect.setAttribute('height','28');\n rect.setAttribute('fill', n.status ? (statusColors[n.status]||'#ecf0f1') : projectColors[n.project]);\n g.appendChild(rect);\n const text = document.createElementNS('http://www.w3.org/2000/svg','text');\n text.setAttribute('x','50'); text.setAttribute('y','18'); text.setAttribute('text-anchor','middle');\n text.textContent = n.id.length > 14 ? n.target : n.id;\n g.appendChild(text);\n g.addEventListener('click', () => {\n const deps = data.edges.filter(e => e.source === n.id).map(e => e.target);\n const rdeps = data.edges.filter(e => e.target === n.id).map(e => e.source);\n alert(n.id + '\n\nDepends on: ' + (deps.join(', ')||'none') + '\nRequired by: ' + (rdeps.join(', ')||'none'));\n });\n svg.appendChild(g);\n});\n<\/script>\n</body>\n</html>"], ['<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8">\n<title>Task Graph</title>\n<style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body { font-family: system-ui, -apple-system, sans-serif; background: #1a1a2e; color: #eee; }\n svg { width: 100vw; height: 100vh; }\n .node rect { rx: 6; ry: 6; stroke: #555; stroke-width: 1.5; cursor: pointer; }\n .node text { font-size: 11px; fill: #1a1a2e; font-weight: 600; pointer-events: none; }\n .node:hover rect { stroke: #fff; stroke-width: 2; }\n .edge { stroke: #444; stroke-width: 1.5; fill: none; marker-end: url(#arrow); }\n .label { font-size: 10px; fill: #888; }\n #info { position: fixed; top: 12px; right: 12px; background: #16213e; padding: 12px 16px; border-radius: 8px; font-size: 13px; }\n #info b { color: #e94560; }\n</style>\n</head>\n<body>\n<div id="info">\n <b>', "</b> tasks · <b>", "</b> dependencies · <b>", '</b> roots\n</div>\n<svg id="graph">\n <defs>\n <marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto">\n <path d="M 0 0 L 10 5 L 0 10 z" fill="#666"/>\n </marker>\n </defs>\n</svg>\n<script>\nconst data = ', ";\nconst svg = document.getElementById('graph');\nconst W = window.innerWidth, H = window.innerHeight;\nconst statusColors = {\n success: '#2ecc71', 'local-cache': '#3498db', 'remote-cache': '#9b59b6',\n failure: '#e74c3c', running: '#f39c12', skipped: '#95a5a6', pending: '#ecf0f1'\n};\nconst projectColors = {};\nconst palette = ['#e94560','#0f3460','#533483','#16c79a','#f39c12','#2ecc71','#3498db','#e67e22','#9b59b6','#1abc9c'];\nlet ci = 0;\ndata.nodes.forEach(n => {\n if (!projectColors[n.project]) projectColors[n.project] = palette[ci++ % palette.length];\n});\n\n// Simple force-directed layout\nconst nodes = data.nodes.map((n, i) => ({\n ...n, x: W/2 + (Math.random()-0.5)*400, y: H/2 + (Math.random()-0.5)*300, vx: 0, vy: 0\n}));\nconst nodeMap = new Map(nodes.map(n => [n.id, n]));\nconst edges = data.edges.map(e => ({ source: nodeMap.get(e.source), target: nodeMap.get(e.target) }));\n\nfunction simulate() {\n for (let iter = 0; iter < 300; iter++) {\n // Repulsion\n for (let i = 0; i < nodes.length; i++) {\n for (let j = i+1; j < nodes.length; j++) {\n let dx = nodes[j].x - nodes[i].x, dy = nodes[j].y - nodes[i].y;\n let d = Math.sqrt(dx*dx + dy*dy) || 1;\n let f = 8000 / (d * d);\n nodes[i].vx -= dx/d * f; nodes[i].vy -= dy/d * f;\n nodes[j].vx += dx/d * f; nodes[j].vy += dy/d * f;\n }\n }\n // Attraction (edges)\n edges.forEach(e => {\n if (!e.source || !e.target) {\n return;\n }\n let dx = e.target.x - e.source.x, dy = e.target.y - e.source.y;\n let d = Math.sqrt(dx*dx + dy*dy) || 1;\n let f = (d - 150) * 0.01;\n e.source.vx += dx/d * f; e.source.vy += dy/d * f;\n e.target.vx -= dx/d * f; e.target.vy -= dy/d * f;\n });\n // Gravity\n nodes.forEach(n => {\n n.vx += (W/2 - n.x) * 0.001;\n n.vy += (H/2 - n.y) * 0.001;\n n.x += n.vx * 0.3; n.y += n.vy * 0.3;\n n.vx *= 0.8; n.vy *= 0.8;\n n.x = Math.max(60, Math.min(W-60, n.x));\n n.y = Math.max(30, Math.min(H-30, n.y));\n });\n }\n}\nsimulate();\n\n// Render\nedges.forEach(e => {\n if (!e.source || !e.target) {\n return;\n }\n const line = document.createElementNS('http://www.w3.org/2000/svg','line');\n line.setAttribute('x1', e.source.x); line.setAttribute('y1', e.source.y);\n line.setAttribute('x2', e.target.x); line.setAttribute('y2', e.target.y);\n line.setAttribute('class','edge');\n svg.appendChild(line);\n});\nnodes.forEach(n => {\n const g = document.createElementNS('http://www.w3.org/2000/svg','g');\n g.setAttribute('class','node');\n g.setAttribute('transform','translate('+(n.x-50)+','+(n.y-14)+')');\n const rect = document.createElementNS('http://www.w3.org/2000/svg','rect');\n rect.setAttribute('width','100'); rect.setAttribute('height','28');\n rect.setAttribute('fill', n.status ? (statusColors[n.status]||'#ecf0f1') : projectColors[n.project]);\n g.appendChild(rect);\n const text = document.createElementNS('http://www.w3.org/2000/svg','text');\n text.setAttribute('x','50'); text.setAttribute('y','18'); text.setAttribute('text-anchor','middle');\n text.textContent = n.id.length > 14 ? n.target : n.id;\n g.appendChild(text);\n g.addEventListener('click', () => {\n const deps = data.edges.filter(e => e.source === n.id).map(e => e.target);\n const rdeps = data.edges.filter(e => e.target === n.id).map(e => e.source);\n alert(n.id + '\\n\\nDepends on: ' + (deps.join(', ')||'none') + '\\nRequired by: ' + (rdeps.join(', ')||'none'));\n });\n svg.appendChild(g);\n});\n<\/script>\n</body>\n</html>"])), graphData.nodes.length, graphData.edges.length, graphData.roots.length, JSON.stringify(graphData).replaceAll("</", String.raw`<\/`));
|
|
157
157
|
};
|
|
158
158
|
const toGraphAscii = (taskGraph, options = {}) => {
|
|
159
159
|
const { taskStatuses } = options;
|
|
@@ -191,8 +191,25 @@ const projectGraphToDot = (projectGraph) => {
|
|
|
191
191
|
}
|
|
192
192
|
for (const [project, deps] of Object.entries(projectGraph.dependencies)) {
|
|
193
193
|
for (const dep of deps) {
|
|
194
|
-
const
|
|
195
|
-
|
|
194
|
+
const attributes = [];
|
|
195
|
+
switch (dep.type) {
|
|
196
|
+
case "devDependency": {
|
|
197
|
+
attributes.push("style=dotted", 'color="#888888"');
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
case "implicit": {
|
|
201
|
+
attributes.push("style=dashed");
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
case "peerDependency": {
|
|
205
|
+
attributes.push("style=dashed", 'color="#CC8800"');
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
default: {
|
|
209
|
+
attributes.push("style=solid");
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
lines.push(` "${project}" -> "${dep.target}" [${attributes.join(", ")}];`);
|
|
196
213
|
}
|
|
197
214
|
}
|
|
198
215
|
lines.push("}");
|