bun-workspaces 1.0.0-alpha.37 → 1.0.0-alpha.38
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/README.md +15 -0
- package/package.json +1 -1
- package/src/cli/commands/commandsConfig.d.ts +16 -0
- package/src/cli/commands/commandsConfig.mjs +10 -0
- package/src/cli/commands/runScript/formatRunScriptOutput.d.ts +2 -1
- package/src/cli/commands/runScript/formatRunScriptOutput.mjs +9 -3
- package/src/cli/commands/runScript/handleRunScript.mjs +22 -5
- package/src/project/implementations/fileSystemProject.d.ts +4 -0
- package/src/project/implementations/fileSystemProject.mjs +20 -1
- package/src/runScript/runScript.d.ts +2 -0
- package/src/runScript/runScripts.d.ts +8 -0
- package/src/runScript/runScripts.mjs +136 -30
- package/src/workspaces/dependencyGraph/cycles.mjs +17 -11
package/README.md
CHANGED
|
@@ -67,6 +67,13 @@ bw run lint my-alias-a my-alias-b # Run by alias (set by optional config)
|
|
|
67
67
|
bw run lint "my-workspace-*" # Run for matching workspace names
|
|
68
68
|
bw run lint "alias:my-alias-pattern-*" "path:my-glob/**/*" # Use matching specifiers
|
|
69
69
|
|
|
70
|
+
# A workspace's script will wait until any workspaces it depends on have completed
|
|
71
|
+
# Similar to Bun's --filter behavior
|
|
72
|
+
bw run lint --dep-order
|
|
73
|
+
|
|
74
|
+
# Continue running scripts even if a dependency fails
|
|
75
|
+
bw run lint --dep-order --ignore-dep-failure
|
|
76
|
+
|
|
70
77
|
bw run lint --args="--my-appended-args" # Add args to each script call
|
|
71
78
|
bw run lint --args="--my-arg=<workspaceName>" # Use the workspace name in args
|
|
72
79
|
|
|
@@ -154,6 +161,14 @@ const runManyScripts = async () => {
|
|
|
154
161
|
|
|
155
162
|
// Optional. Whether to run the scripts in parallel
|
|
156
163
|
parallel: true,
|
|
164
|
+
|
|
165
|
+
// Optional. When true, a workspace's script will wait
|
|
166
|
+
// until any workspaces it depends on have completed
|
|
167
|
+
dependencyOrder: true,
|
|
168
|
+
|
|
169
|
+
// Optional. When true and dependencyOrder is true,
|
|
170
|
+
// continue running scripts even if a dependency fails
|
|
171
|
+
ignoreDependencyFailure: true,
|
|
157
172
|
});
|
|
158
173
|
|
|
159
174
|
// Get a stream of script output
|
package/package.json
CHANGED
|
@@ -161,6 +161,14 @@ export declare const CLI_COMMANDS_CONFIG: {
|
|
|
161
161
|
readonly values: ["bun", "system", "default"];
|
|
162
162
|
readonly description: "When using --inline, the shell to use to run the script";
|
|
163
163
|
};
|
|
164
|
+
readonly depOrder: {
|
|
165
|
+
readonly flags: ["-d", "--dep-order"];
|
|
166
|
+
readonly description: "Scripts for dependent workspaces run only after their dependencies";
|
|
167
|
+
};
|
|
168
|
+
readonly ignoreDepFailure: {
|
|
169
|
+
readonly flags: ["-f", "--ignore-dep-failure"];
|
|
170
|
+
readonly description: "In dependency order, continue running scripts even if a dependency fails";
|
|
171
|
+
};
|
|
164
172
|
readonly jsonOutfile: {
|
|
165
173
|
readonly flags: ["-j", "--json-outfile <file>"];
|
|
166
174
|
readonly description: "Output results in a JSON file";
|
|
@@ -304,6 +312,14 @@ export declare const getCliCommandConfig: (commandName: CliCommandName) =>
|
|
|
304
312
|
readonly values: ["bun", "system", "default"];
|
|
305
313
|
readonly description: "When using --inline, the shell to use to run the script";
|
|
306
314
|
};
|
|
315
|
+
readonly depOrder: {
|
|
316
|
+
readonly flags: ["-d", "--dep-order"];
|
|
317
|
+
readonly description: "Scripts for dependent workspaces run only after their dependencies";
|
|
318
|
+
};
|
|
319
|
+
readonly ignoreDepFailure: {
|
|
320
|
+
readonly flags: ["-f", "--ignore-dep-failure"];
|
|
321
|
+
readonly description: "In dependency order, continue running scripts even if a dependency fails";
|
|
322
|
+
};
|
|
307
323
|
readonly jsonOutfile: {
|
|
308
324
|
readonly flags: ["-j", "--json-outfile <file>"];
|
|
309
325
|
readonly description: "Output results in a JSON file";
|
|
@@ -141,6 +141,16 @@ const CLI_COMMANDS_CONFIG = {
|
|
|
141
141
|
values: [...SCRIPT_SHELL_OPTIONS, "default"],
|
|
142
142
|
description: `When using --inline, the shell to use to run the script`,
|
|
143
143
|
},
|
|
144
|
+
depOrder: {
|
|
145
|
+
flags: ["-d", "--dep-order"],
|
|
146
|
+
description:
|
|
147
|
+
"Scripts for dependent workspaces run only after their dependencies",
|
|
148
|
+
},
|
|
149
|
+
ignoreDepFailure: {
|
|
150
|
+
flags: ["-f", "--ignore-dep-failure"],
|
|
151
|
+
description:
|
|
152
|
+
"In dependency order, continue running scripts even if a dependency fails",
|
|
153
|
+
},
|
|
144
154
|
jsonOutfile: {
|
|
145
155
|
flags: ["-j", "--json-outfile <file>"],
|
|
146
156
|
description: "Output results in a JSON file",
|
|
@@ -4,12 +4,13 @@ import type {
|
|
|
4
4
|
} from "../../../project";
|
|
5
5
|
import type { OutputStreamName } from "../../../runScript";
|
|
6
6
|
export type FormatRunScriptOutputOptions = {
|
|
7
|
+
stripDisruptiveControls?: boolean;
|
|
7
8
|
prefix?: boolean;
|
|
8
9
|
scriptName: string;
|
|
9
10
|
};
|
|
10
11
|
export declare function formatRunScriptOutput(
|
|
11
12
|
output: RunScriptAcrossWorkspacesProcessOutput,
|
|
12
|
-
{ scriptName, prefix }: FormatRunScriptOutputOptions,
|
|
13
|
+
{ scriptName, stripDisruptiveControls, prefix }: FormatRunScriptOutputOptions,
|
|
13
14
|
): AsyncGenerator<{
|
|
14
15
|
line: string;
|
|
15
16
|
metadata: RunWorkspaceScriptMetadata & {
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// CONCATENATED MODULE: ./src/cli/commands/runScript/formatRunScriptOutput.ts
|
|
2
|
-
const sanitizeChunk = (input) => {
|
|
2
|
+
const sanitizeChunk = (input, stripDisruptiveControls = false) => {
|
|
3
|
+
if (!stripDisruptiveControls) {
|
|
4
|
+
return input.replace(/\r\n/g, "\n");
|
|
5
|
+
}
|
|
3
6
|
// 1) Normalize newline-ish controls
|
|
4
7
|
let s = input
|
|
5
8
|
.replace(/\r\n/g, "\n")
|
|
@@ -90,7 +93,10 @@ const sanitizeChunk = (input) => {
|
|
|
90
93
|
}
|
|
91
94
|
return out;
|
|
92
95
|
};
|
|
93
|
-
async function* formatRunScriptOutput(
|
|
96
|
+
async function* formatRunScriptOutput(
|
|
97
|
+
output,
|
|
98
|
+
{ scriptName, stripDisruptiveControls = true, prefix = false },
|
|
99
|
+
) {
|
|
94
100
|
const workspaceLineBuffers = {};
|
|
95
101
|
const formatLine = (line, workspaceName, scriptName) => {
|
|
96
102
|
const prefixedLine = prefix
|
|
@@ -100,7 +106,7 @@ async function* formatRunScriptOutput(output, { scriptName, prefix = false }) {
|
|
|
100
106
|
};
|
|
101
107
|
for await (const { metadata, chunk } of output.text()) {
|
|
102
108
|
const workspaceName = metadata.workspace.name;
|
|
103
|
-
const sanitizedChunk = sanitizeChunk(chunk);
|
|
109
|
+
const sanitizedChunk = sanitizeChunk(chunk, stripDisruptiveControls);
|
|
104
110
|
const prior = workspaceLineBuffers[workspaceName] ?? "";
|
|
105
111
|
const content = prior + sanitizedChunk;
|
|
106
112
|
const lines = content.split("\n");
|
|
@@ -53,6 +53,9 @@ const runScript = handleProjectCommand(
|
|
|
53
53
|
logger.debug(
|
|
54
54
|
`Command: Run script ${JSON.stringify(script)} for ${workspacePatterns.length ? "workspaces " + workspacePatterns.join(", ") : "all workspaces"} (parallel: ${!!options.parallel}, args: ${JSON.stringify(scriptArgs)})`,
|
|
55
55
|
);
|
|
56
|
+
const workspaceCount = project.findWorkspacesByPattern(
|
|
57
|
+
...workspacePatterns,
|
|
58
|
+
).length;
|
|
56
59
|
const { output, summary } = project.runScriptAcrossWorkspaces({
|
|
57
60
|
workspacePatterns: workspacePatterns.length
|
|
58
61
|
? workspacePatterns
|
|
@@ -67,6 +70,8 @@ const runScript = handleProjectCommand(
|
|
|
67
70
|
: true
|
|
68
71
|
: undefined,
|
|
69
72
|
args: scriptArgs,
|
|
73
|
+
dependencyOrder: options.depOrder,
|
|
74
|
+
ignoreDependencyFailure: options.ignoreDepFailure,
|
|
70
75
|
parallel:
|
|
71
76
|
typeof options.parallel === "boolean" ||
|
|
72
77
|
typeof options.parallel === "undefined"
|
|
@@ -87,6 +92,7 @@ const runScript = handleProjectCommand(
|
|
|
87
92
|
for await (const { line, metadata } of formatRunScriptOutput(output, {
|
|
88
93
|
prefix: options.prefix,
|
|
89
94
|
scriptName,
|
|
95
|
+
stripDisruptiveControls: workspaceCount > 1 || !!options.parallel,
|
|
90
96
|
})) {
|
|
91
97
|
process[metadata.streamName].write(line);
|
|
92
98
|
}
|
|
@@ -95,18 +101,29 @@ const runScript = handleProjectCommand(
|
|
|
95
101
|
const exitResults = await summary;
|
|
96
102
|
exitResults.scriptResults.forEach(
|
|
97
103
|
({ success, metadata: { workspace }, exitCode }) => {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
104
|
+
const isSkipped = exitCode === -1;
|
|
105
|
+
if (isSkipped) {
|
|
106
|
+
logger.info(
|
|
107
|
+
`➖ ${workspace.name}: ${scriptName} (skipped due to dependency failure)`,
|
|
108
|
+
);
|
|
109
|
+
} else {
|
|
110
|
+
logger.info(
|
|
111
|
+
`${success ? "✅" : "❌"} ${workspace.name}: ${scriptName}${exitCode ? ` (exited with code ${exitCode})` : ""}`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
101
114
|
},
|
|
102
115
|
);
|
|
103
116
|
const s = exitResults.scriptResults.length === 1 ? "" : "s";
|
|
117
|
+
const skippedCount = exitResults.scriptResults.filter(
|
|
118
|
+
({ exitCode }) => exitCode === -1,
|
|
119
|
+
).length;
|
|
120
|
+
const skippedMessage = skippedCount ? ` (${skippedCount} skipped)` : "";
|
|
104
121
|
if (exitResults.failureCount) {
|
|
105
|
-
const message = `${exitResults.failureCount} of ${exitResults.scriptResults.length} script${s} failed`;
|
|
122
|
+
const message = `${exitResults.failureCount} of ${exitResults.scriptResults.length} script${s} failed${skippedMessage}`;
|
|
106
123
|
logger.info(message);
|
|
107
124
|
} else {
|
|
108
125
|
logger.info(
|
|
109
|
-
`${exitResults.scriptResults.length} script${s} ran successfully`,
|
|
126
|
+
`${exitResults.scriptResults.length} script${s} ran successfully${skippedMessage}`,
|
|
110
127
|
);
|
|
111
128
|
}
|
|
112
129
|
if (options.jsonOutfile) {
|
|
@@ -79,6 +79,10 @@ export type RunScriptAcrossWorkspacesOptions = {
|
|
|
79
79
|
args?: string;
|
|
80
80
|
/** Whether to run the scripts in parallel (series by default) */
|
|
81
81
|
parallel?: ParallelOption;
|
|
82
|
+
/** When `true`, run scripts so that dependent workspaces run only after their dependencies */
|
|
83
|
+
dependencyOrder?: boolean;
|
|
84
|
+
/** When `true`, continue running scripts even if a dependency fails (Only relevant when `dependencyOrder` is `true`) */
|
|
85
|
+
ignoreDependencyFailure?: boolean;
|
|
82
86
|
};
|
|
83
87
|
export type RunScriptAcrossWorkspacesOutput = Simplify<
|
|
84
88
|
RunScriptsOutput<RunWorkspaceScriptMetadata>
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { checkIsRecursiveScript } from "../../runScript/recursion.mjs";
|
|
14
14
|
import { resolveScriptShell } from "../../runScript/scriptShellOption.mjs";
|
|
15
15
|
import { findWorkspaces, sortWorkspaces } from "../../workspaces/index.mjs";
|
|
16
|
+
import { preventDependencyCycles } from "../../workspaces/dependencyGraph/index.mjs";
|
|
16
17
|
import { PROJECT_ERRORS } from "../errors.mjs";
|
|
17
18
|
import {
|
|
18
19
|
ProjectBase,
|
|
@@ -29,6 +30,7 @@ import {
|
|
|
29
30
|
// CONCATENATED MODULE: external "../../runScript/recursion.mjs"
|
|
30
31
|
// CONCATENATED MODULE: external "../../runScript/scriptShellOption.mjs"
|
|
31
32
|
// CONCATENATED MODULE: external "../../workspaces/index.mjs"
|
|
33
|
+
// CONCATENATED MODULE: external "../../workspaces/dependencyGraph/index.mjs"
|
|
32
34
|
// CONCATENATED MODULE: external "../errors.mjs"
|
|
33
35
|
// CONCATENATED MODULE: external "./projectBase.mjs"
|
|
34
36
|
// CONCATENATED MODULE: ./src/project/implementations/fileSystemProject.ts
|
|
@@ -166,7 +168,7 @@ class _FileSystemProject extends ProjectBase {
|
|
|
166
168
|
this.workspaces.map((workspace) => workspace.name)
|
|
167
169
|
).flatMap((pattern) => this.findWorkspacesByPattern(pattern)),
|
|
168
170
|
);
|
|
169
|
-
|
|
171
|
+
let workspaces = matchedWorkspaces
|
|
170
172
|
.filter(
|
|
171
173
|
(workspace) =>
|
|
172
174
|
options.inline || workspace.scripts.includes(options.script),
|
|
@@ -195,6 +197,15 @@ class _FileSystemProject extends ProjectBase {
|
|
|
195
197
|
: `No matching workspaces found with script ${JSON.stringify(options.script)}`,
|
|
196
198
|
);
|
|
197
199
|
}
|
|
200
|
+
if (options.dependencyOrder) {
|
|
201
|
+
const cycleDetection = preventDependencyCycles(workspaces);
|
|
202
|
+
workspaces = cycleDetection.workspaces;
|
|
203
|
+
for (const cycle of cycleDetection.cycles) {
|
|
204
|
+
logger.warn(
|
|
205
|
+
`Dependency cycle detected: ${cycle.dependency} -> ${cycle.dependent} (ignoring)`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
198
209
|
const recursiveWorkspace = workspaces.find((workspace) =>
|
|
199
210
|
checkIsRecursiveScript(workspace.name, options.script),
|
|
200
211
|
);
|
|
@@ -257,8 +268,16 @@ class _FileSystemProject extends ProjectBase {
|
|
|
257
268
|
scriptCommand,
|
|
258
269
|
env: createScriptRuntimeEnvVars(scriptRuntimeMetadata),
|
|
259
270
|
shell,
|
|
271
|
+
dependsOn: options.dependencyOrder
|
|
272
|
+
? workspace.dependencies
|
|
273
|
+
.map((dependency) =>
|
|
274
|
+
workspaces.findIndex((w) => w.name === dependency),
|
|
275
|
+
)
|
|
276
|
+
.filter((index) => index !== -1)
|
|
277
|
+
: undefined,
|
|
260
278
|
};
|
|
261
279
|
}),
|
|
280
|
+
ignoreDependencyFailure: options.ignoreDependencyFailure,
|
|
262
281
|
parallel:
|
|
263
282
|
options.parallel === true
|
|
264
283
|
? {
|
|
@@ -7,6 +7,8 @@ export type RunScriptExit<ScriptMetadata extends object = object> = {
|
|
|
7
7
|
exitCode: number;
|
|
8
8
|
signal: NodeJS.Signals | null;
|
|
9
9
|
success: boolean;
|
|
10
|
+
/** Whether the script was skipped due to a failed dependency */
|
|
11
|
+
skipped?: boolean;
|
|
10
12
|
startTimeISO: string;
|
|
11
13
|
endTimeISO: string;
|
|
12
14
|
durationMs: number;
|
|
@@ -4,10 +4,15 @@ import { type OutputChunk, type OutputStreamName } from "./outputChunk";
|
|
|
4
4
|
import { type ParallelMaxValue } from "./parallel";
|
|
5
5
|
import { type RunScriptExit, type RunScriptResult } from "./runScript";
|
|
6
6
|
import { type ScriptCommand } from "./scriptCommand";
|
|
7
|
+
import { type ScriptShellOption } from "./scriptShellOption";
|
|
7
8
|
export type RunScriptsScript<ScriptMetadata extends object = object> = {
|
|
8
9
|
scriptCommand: ScriptCommand;
|
|
9
10
|
metadata: ScriptMetadata;
|
|
10
11
|
env: Record<string, string>;
|
|
12
|
+
/** The shell to use to run the script */
|
|
13
|
+
shell?: ScriptShellOption;
|
|
14
|
+
/** Indices of other scripts in the array that must complete before this one starts */
|
|
15
|
+
dependsOn?: number[];
|
|
11
16
|
};
|
|
12
17
|
export type RunScriptsScriptResult<ScriptMetadata extends object = object> = {
|
|
13
18
|
/** The result of running the script */
|
|
@@ -49,9 +54,12 @@ export type RunScriptsParallelOptions = {
|
|
|
49
54
|
export type RunScriptsOptions<ScriptMetadata extends object = object> = {
|
|
50
55
|
scripts: RunScriptsScript<ScriptMetadata>[];
|
|
51
56
|
parallel: boolean | RunScriptsParallelOptions;
|
|
57
|
+
/** When true, run scripts even if a dependency failed. Default: false (skip them). */
|
|
58
|
+
ignoreDependencyFailure?: boolean;
|
|
52
59
|
};
|
|
53
60
|
/** Run a list of scripts */
|
|
54
61
|
export declare const runScripts: <ScriptMetadata extends object = object>({
|
|
55
62
|
scripts,
|
|
56
63
|
parallel,
|
|
64
|
+
ignoreDependencyFailure,
|
|
57
65
|
}: RunScriptsOptions<ScriptMetadata>) => RunScriptsResult<ScriptMetadata>;
|
|
@@ -13,7 +13,60 @@ import { runScript } from "./runScript.mjs"; // CONCATENATED MODULE: external ".
|
|
|
13
13
|
// CONCATENATED MODULE: external "./runScript.mjs"
|
|
14
14
|
// CONCATENATED MODULE: ./src/runScript/runScripts.ts
|
|
15
15
|
|
|
16
|
-
/**
|
|
16
|
+
/** Validate dependency indices and detect cycles via DFS */ const validateScriptDependencies =
|
|
17
|
+
(scripts) => {
|
|
18
|
+
const scriptCount = scripts.length;
|
|
19
|
+
for (let i = 0; i < scriptCount; i++) {
|
|
20
|
+
const deps = scripts[i].dependsOn;
|
|
21
|
+
if (!deps) continue;
|
|
22
|
+
for (const dep of deps) {
|
|
23
|
+
if (dep === i) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Script at index ${i} has a self-referencing dependency`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
if (dep < 0 || dep >= scriptCount) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`Script at index ${i} depends on invalid index ${dep} (valid range: 0-${scriptCount - 1})`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const WHITE = 0;
|
|
36
|
+
const GRAY = 1;
|
|
37
|
+
const BLACK = 2;
|
|
38
|
+
const colors = new Array(scriptCount).fill(WHITE);
|
|
39
|
+
const visit = (node, path) => {
|
|
40
|
+
colors[node] = GRAY;
|
|
41
|
+
const deps = scripts[node].dependsOn;
|
|
42
|
+
if (deps) {
|
|
43
|
+
for (const dep of deps) {
|
|
44
|
+
if (colors[dep] === GRAY) {
|
|
45
|
+
const cycleStart = path.indexOf(dep);
|
|
46
|
+
const cycle = path.slice(cycleStart);
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Dependency cycle detected: ${[...cycle, dep].join(" -> ")}`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
if (colors[dep] === WHITE) {
|
|
52
|
+
visit(dep, [...path, dep]);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
colors[node] = BLACK;
|
|
57
|
+
};
|
|
58
|
+
for (let i = 0; i < scriptCount; i++) {
|
|
59
|
+
if (colors[i] === WHITE) {
|
|
60
|
+
visit(i, [i]);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
/** Run a list of scripts */ const runScripts = ({
|
|
65
|
+
scripts,
|
|
66
|
+
parallel,
|
|
67
|
+
ignoreDependencyFailure = false,
|
|
68
|
+
}) => {
|
|
69
|
+
validateScriptDependencies(scripts);
|
|
17
70
|
const startTime = new Date();
|
|
18
71
|
const scriptTriggers = scripts.map((_, index) => {
|
|
19
72
|
let trigger = () => {
|
|
@@ -48,33 +101,88 @@ import { runScript } from "./runScript.mjs"; // CONCATENATED MODULE: external ".
|
|
|
48
101
|
`Number of scripts to run in parallel (${parallelBatchSize}) is greater than the available CPUs (${recommendedParallelMax})`,
|
|
49
102
|
);
|
|
50
103
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
104
|
+
const pendingScripts = new Set(scripts.map((_, i) => i));
|
|
105
|
+
const runningScripts = new Set();
|
|
106
|
+
const completedScripts = new Set();
|
|
107
|
+
const exitResults = scripts.map(() => null);
|
|
108
|
+
const scriptProcessBytes = scripts.map(() => null);
|
|
109
|
+
const createSkippedExit = (index) => {
|
|
110
|
+
const now = new Date().toISOString();
|
|
111
|
+
return {
|
|
112
|
+
exitCode: -1,
|
|
113
|
+
signal: null,
|
|
114
|
+
success: false,
|
|
115
|
+
skipped: true,
|
|
116
|
+
startTimeISO: now,
|
|
117
|
+
endTimeISO: now,
|
|
118
|
+
durationMs: 0,
|
|
119
|
+
metadata: scripts[index].metadata,
|
|
66
120
|
};
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
121
|
+
};
|
|
122
|
+
const createSkippedResult = (index) => {
|
|
123
|
+
const skippedExit = createSkippedExit(index);
|
|
124
|
+
exitResults[index] = skippedExit;
|
|
125
|
+
return {
|
|
126
|
+
result: {
|
|
127
|
+
output: (async function* () {})(),
|
|
128
|
+
processOutput: createMultiProcessOutput([]),
|
|
129
|
+
exit: Promise.resolve(skippedExit),
|
|
130
|
+
metadata: scripts[index].metadata,
|
|
131
|
+
kill: () => {},
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
const hasDependencyFailure = (index) => {
|
|
136
|
+
const deps = scripts[index].dependsOn;
|
|
137
|
+
if (!deps) return false;
|
|
138
|
+
return deps.some(
|
|
139
|
+
(dep) => exitResults[dep] !== null && !exitResults[dep].success,
|
|
140
|
+
);
|
|
141
|
+
};
|
|
142
|
+
const areDependenciesMet = (index) => {
|
|
143
|
+
const deps = scripts[index].dependsOn;
|
|
144
|
+
if (!deps) return true;
|
|
145
|
+
return deps.every((dep) => completedScripts.has(dep));
|
|
146
|
+
};
|
|
147
|
+
const scheduleReadyScripts = () => {
|
|
148
|
+
let changed = true;
|
|
149
|
+
while (changed) {
|
|
150
|
+
changed = false;
|
|
151
|
+
for (const index of [...pendingScripts]) {
|
|
152
|
+
if (runningScripts.size >= parallelMax) return;
|
|
153
|
+
if (!areDependenciesMet(index)) continue;
|
|
154
|
+
if (!ignoreDependencyFailure && hasDependencyFailure(index)) {
|
|
155
|
+
pendingScripts.delete(index);
|
|
156
|
+
completedScripts.add(index);
|
|
157
|
+
scriptResults[index] = createSkippedResult(index);
|
|
158
|
+
scriptProcessBytes[index] = (async function* () {})();
|
|
159
|
+
scriptTriggers[index].trigger();
|
|
160
|
+
changed = true;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
pendingScripts.delete(index);
|
|
164
|
+
runningScripts.add(index);
|
|
165
|
+
const scriptResult = {
|
|
166
|
+
...scripts[index],
|
|
167
|
+
result: runScript({
|
|
168
|
+
...scripts[index],
|
|
169
|
+
env: {
|
|
170
|
+
...scripts[index].env,
|
|
171
|
+
_BW_PARALLEL_MAX: parallelMax.toString(),
|
|
172
|
+
},
|
|
173
|
+
}),
|
|
174
|
+
};
|
|
175
|
+
scriptResults[index] = scriptResult;
|
|
176
|
+
scriptProcessBytes[index] = scriptResult.result.processOutput.bytes();
|
|
177
|
+
scriptTriggers[index].trigger();
|
|
178
|
+
scriptResult.result.exit.then((exit) => {
|
|
179
|
+
runningScripts.delete(index);
|
|
180
|
+
completedScripts.add(index);
|
|
181
|
+
exitResults[index] = exit;
|
|
182
|
+
scheduleReadyScripts();
|
|
183
|
+
});
|
|
75
184
|
}
|
|
76
|
-
}
|
|
77
|
-
return scriptResult;
|
|
185
|
+
}
|
|
78
186
|
};
|
|
79
187
|
const scriptOutputQueues = scripts.map(() =>
|
|
80
188
|
["stdout", "stderr"].map(() => createAsyncIterableQueue()),
|
|
@@ -105,9 +213,7 @@ import { runScript } from "./runScript.mjs"; // CONCATENATED MODULE: external ".
|
|
|
105
213
|
});
|
|
106
214
|
outputReaders.push(
|
|
107
215
|
(async () => {
|
|
108
|
-
for await (const chunk of
|
|
109
|
-
index
|
|
110
|
-
].result.processOutput.bytes()) {
|
|
216
|
+
for await (const chunk of scriptProcessBytes[index]) {
|
|
111
217
|
outputQueue.push({
|
|
112
218
|
outputChunk: createOutputChunk(
|
|
113
219
|
chunk.metadata.streamName,
|
|
@@ -134,7 +240,7 @@ import { runScript } from "./runScript.mjs"; // CONCATENATED MODULE: external ".
|
|
|
134
240
|
});
|
|
135
241
|
};
|
|
136
242
|
const awaitSummary = async () => {
|
|
137
|
-
|
|
243
|
+
scheduleReadyScripts();
|
|
138
244
|
await handleScriptProcesses();
|
|
139
245
|
const scriptExitResults = await Promise.all(
|
|
140
246
|
scripts.map((_, index) => scriptResults[index].result.exit),
|
|
@@ -8,6 +8,8 @@ const preventDependencyCycles = (workspaces) => {
|
|
|
8
8
|
const inStack = new Set();
|
|
9
9
|
// dedupe cycle edges
|
|
10
10
|
const cyclesKeyed = new Map();
|
|
11
|
+
// all nodes that participate in at least one cycle
|
|
12
|
+
const cycleNodeSet = new Set();
|
|
11
13
|
const recordCycleEdge = (dependency, dependent) => {
|
|
12
14
|
const key = `${dependency}\u0000${dependent}`;
|
|
13
15
|
if (!cyclesKeyed.has(key))
|
|
@@ -34,6 +36,11 @@ const preventDependencyCycles = (workspaces) => {
|
|
|
34
36
|
// Cycle edge: current `name` depends on `dep`, and `dep` is already in the active stack
|
|
35
37
|
if (inStack.has(dep)) {
|
|
36
38
|
recordCycleEdge(dep, name);
|
|
39
|
+
// Mark every node between `dep` and `name` (inclusive) as a cycle participant.
|
|
40
|
+
// `name` is already at stack[stack.length - 1] since it was pushed above.
|
|
41
|
+
const depIndex = stack.indexOf(dep);
|
|
42
|
+
for (let i = depIndex; i < stack.length; i++)
|
|
43
|
+
cycleNodeSet.add(stack[i]);
|
|
37
44
|
continue;
|
|
38
45
|
}
|
|
39
46
|
// Missing dependency name: treat as a leaf chain [dep, name]
|
|
@@ -62,17 +69,16 @@ const preventDependencyCycles = (workspaces) => {
|
|
|
62
69
|
...workspace,
|
|
63
70
|
}));
|
|
64
71
|
const cycles = [...cyclesKeyed.values()];
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
(d) => d !== dependency,
|
|
72
|
+
// Remove all dependency/dependent edges between workspaces that share a cycle.
|
|
73
|
+
// This leaves no opinionated "winner": if two workspaces are in the same cycle,
|
|
74
|
+
// all edges between them are stripped.
|
|
75
|
+
for (const workspace of workspaces) {
|
|
76
|
+
if (cycleNodeSet.has(workspace.name)) {
|
|
77
|
+
workspace.dependencies = workspace.dependencies.filter(
|
|
78
|
+
(d) => !cycleNodeSet.has(d),
|
|
79
|
+
);
|
|
80
|
+
workspace.dependents = workspace.dependents.filter(
|
|
81
|
+
(d) => !cycleNodeSet.has(d),
|
|
76
82
|
);
|
|
77
83
|
}
|
|
78
84
|
}
|