bun-workspaces 1.0.0-alpha.21 → 1.0.0-alpha.23

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 CHANGED
@@ -1,15 +1,25 @@
1
1
  <img src="./packages/doc-website/src/docs/public/bw-eye.png" alt="bun-workspaces" width="50" />
2
2
 
3
+ ### [📖 **See Full Documentation Here**: _https://bunworkspaces.com_](https://bunworkspaces.com)
4
+
3
5
  # bun-workspaces
4
6
 
5
- ### [**See Full Documentation Here:** _https://bunworkspaces.com_](https://bunworkspaces.com)
7
+ A CLI and API to enhance your monorepo development with Bun's [native workspaces](https://bun.sh/docs/install/workspaces) feature for nested JavaScript/TypeScript packages.
8
+
9
+ - Works right away, with no boilerplate required 🍔🍴
10
+ - Get metadata about your monorepo 🤖
11
+ - Run package.json scripts across workspaces 📋
12
+ - Supports running inline scripts as well ⌨️
6
13
 
7
- **_New: [An API is now officially released!](https://bunworkspaces.com/api)_**
14
+ This tool lets you decide the complexity of how you use it.
15
+ To get started, all you need is a normal project using [Bun's native workspaces](https://bun.sh/docs/install/workspaces) feature for nested JavaScript/TypeScript packages.
8
16
 
9
- This is a CLI and API that help you manage your monorepo on top of native [Bun workspaces](https://bun.sh/docs/install/workspaces), with no additional setup required. Get metadata about your workspaces and scripts, and run scripts across your workspaces.
17
+ Think of this as a power suit you can snap onto native workspaces, rather than another monorepo framework.
10
18
 
19
+ Start running some [CLI commands](https://bunworkspaces.com/cli) right away in your repo, or take full advantage of the [scripting API](https://bunworkspaces.com/api) and its features.
20
+ <br/><br/>
11
21
  <a href="https://buymeacoffee.com/scottmorse">
12
- <img src="./packages/doc-website/src/docs/public/bmac-logo-circle.png" alt="Link to Buy Me A Coffee" width="60" />
22
+ <img src="./packages/doc-website/src/docs/public/bmac-logo-circle.png" alt="Link to Buy Me A Coffee" width="50" />
13
23
  </a>
14
24
 
15
25
  ## Quick Start
@@ -37,7 +47,7 @@ bw list-workspaces
37
47
  bw ls --json --pretty # Output as formatted JSON
38
48
 
39
49
  # Run the lint script for all workspaces
40
- # that have it in their "scripts" field
50
+ # that have it in their package.json "scripts" field
41
51
  bw run-script lint
42
52
 
43
53
  # run is an alias for run-script
@@ -47,14 +57,15 @@ bw run lint "my-workspace-*" # Run for matching workspace names
47
57
  bw run lint --parallel # Run at the same time
48
58
  bw run lint --args="--my-appended-args" # Add args to each script call
49
59
  bw run lint --args="--my-arg=<workspaceName>" # Use the workspace name in args
50
-
51
- # Run an inline command from the workspace directory
52
- bw run "echo 'this is my inline script for <workspaceName>'" --inline
60
+ bw run "bun build" --inline --inline-name=build # Run an inline command
53
61
 
54
62
  # Show usage (you can pass --help to any command)
55
63
  bw help
56
64
  bw --help
57
65
 
66
+ # Show version
67
+ bw --version
68
+
58
69
  # Pass --cwd to any command
59
70
  bw --cwd=/path/to/your/project ls
60
71
  bw --cwd=/path/to/your/project run my-script
@@ -93,10 +104,11 @@ const runSingleScript = async () => {
93
104
  });
94
105
 
95
106
  // Get a stream of the script subprocess's output
96
- for await (const { text, textNoAnsi, streamName } of output) {
97
- console.log(text); // The output chunk's content (string)
98
- console.log(textNoAnsi); // Text with ANSI codes sanitized (string)
99
- console.log(streamName); // The output stream, "stdout" or "stderr"
107
+ for await (const chunk of output) {
108
+ console.log(chunk.raw); // The raw output content (Uint8Array)
109
+ console.log(chunk.decode()); // The output chunk's content (string)
110
+ console.log(chunk.decode({ stripAnsi: true })); // Text with ANSI codes sanitized
111
+ console.log(chunk.streamName); // The output stream, "stdout" or "stderr"
100
112
  }
101
113
 
102
114
  // Get data about the script execution after it exits
@@ -122,8 +134,9 @@ const runManyScripts = async () => {
122
134
 
123
135
  // Get a stream of script output
124
136
  for await (const { outputChunk, scriptMetadata } of output) {
125
- console.log(outputChunk.text); // the output chunk's content (string)
126
- console.log(outputChunk.textNoAnsi); // text with ANSI codes sanitized (string)
137
+ console.log(outputChunk.raw); // The raw output content (Uint8Array)
138
+ console.log(outputChunk.decode()); // the output chunk's content (string)
139
+ console.log(outputChunk.decode({ stripAnsi: true })); // text with ANSI codes sanitized (string)
127
140
  console.log(outputChunk.streamName); // "stdout" or "stderr"
128
141
 
129
142
  // The metadata can distinguish which workspace script
@@ -155,4 +168,4 @@ const runManyScripts = async () => {
155
168
  };
156
169
  ```
157
170
 
158
- _`bun-workspaces` is independent from the [Bun](https://bun.sh) project and is not affiliated with or endorsed by Oven. This project aims to enhance enhance the experience of Bun for its users._
171
+ _`bun-workspaces` is independent from the [Bun](https://bun.sh) project and is not affiliated with or endorsed by Anthropic. This project aims to enhance enhance the experience of Bun for its users._
package/bin/cli.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bun-workspaces",
3
- "version": "1.0.0-alpha.21",
3
+ "version": "1.0.0-alpha.23",
4
4
  "license": "MIT",
5
5
  "main": "src/index.mjs",
6
6
  "types": "src/index.d.ts",
@@ -15,7 +15,7 @@
15
15
  },
16
16
  "custom": {
17
17
  "bunVersion": {
18
- "build": "1.3.1",
18
+ "build": "1.3.4",
19
19
  "libraryConsumer": "^1.1.x"
20
20
  }
21
21
  },
@@ -64,16 +64,34 @@ const runScript = handleCommand(
64
64
  const { output, summary } = project.runScriptAcrossWorkspaces({
65
65
  workspacePatterns: workspaces.map(({ name }) => name),
66
66
  script,
67
- inline: options.inline,
67
+ inline: options.inline
68
+ ? options.inlineName
69
+ ? {
70
+ scriptName: options.inlineName,
71
+ }
72
+ : true
73
+ : undefined,
68
74
  args: options.args,
69
- parallel: !!options.parallel,
75
+ parallel:
76
+ typeof options.parallel === "boolean" ||
77
+ typeof options.parallel === "undefined"
78
+ ? options.parallel
79
+ : options.parallel === "true"
80
+ ? true
81
+ : options.parallel === "false"
82
+ ? false
83
+ : {
84
+ max: options.parallel,
85
+ },
70
86
  });
71
- const scriptName = options.inline ? "(inline)" : script;
87
+ const scriptName = options.inline
88
+ ? options.inlineName || "(inline)"
89
+ : script;
72
90
  const handleOutput = async () => {
73
91
  if (logger.printLevel === "silent") return;
74
92
  for await (const { outputChunk, scriptMetadata } of output) {
75
93
  commandOutputLogger.logOutput(
76
- outputChunk.text,
94
+ outputChunk.decode(),
77
95
  "info",
78
96
  process[outputChunk.streamName],
79
97
  options.prefix
@@ -90,8 +90,8 @@ declare const CLI_PROJECT_COMMANDS_CONFIG: {
90
90
  readonly description: 'Run a script in all workspaces that have it in their "scripts" field in package.json';
91
91
  readonly options: {
92
92
  readonly parallel: {
93
- readonly flags: "--parallel";
94
- readonly description: "Run the scripts in parallel";
93
+ readonly flags: "--parallel [max]";
94
+ readonly description: "Run the scripts in parallel. Pass an optional number, percentage, or keyword: 'default', 'auto', 'unbounded'";
95
95
  };
96
96
  readonly args: {
97
97
  readonly flags: "--args <args>";
@@ -105,6 +105,10 @@ declare const CLI_PROJECT_COMMANDS_CONFIG: {
105
105
  readonly flags: "--inline";
106
106
  readonly description: "Run the script as an inline command from the workspace directory";
107
107
  };
108
+ readonly inlineName: {
109
+ readonly flags: "--inline-name <name>";
110
+ readonly description: "An optional name for the script when --inline is passed";
111
+ };
108
112
  readonly jsonOutfile: {
109
113
  readonly flags: "--json-outfile <file>";
110
114
  readonly description: "Output results in a JSON file";
@@ -193,8 +197,8 @@ export declare const getProjectCommandConfig: (
193
197
  readonly description: 'Run a script in all workspaces that have it in their "scripts" field in package.json';
194
198
  readonly options: {
195
199
  readonly parallel: {
196
- readonly flags: "--parallel";
197
- readonly description: "Run the scripts in parallel";
200
+ readonly flags: "--parallel [max]";
201
+ readonly description: "Run the scripts in parallel. Pass an optional number, percentage, or keyword: 'default', 'auto', 'unbounded'";
198
202
  };
199
203
  readonly args: {
200
204
  readonly flags: "--args <args>";
@@ -208,6 +212,10 @@ export declare const getProjectCommandConfig: (
208
212
  readonly flags: "--inline";
209
213
  readonly description: "Run the script as an inline command from the workspace directory";
210
214
  };
215
+ readonly inlineName: {
216
+ readonly flags: "--inline-name <name>";
217
+ readonly description: "An optional name for the script when --inline is passed";
218
+ };
211
219
  readonly jsonOutfile: {
212
220
  readonly flags: "--json-outfile <file>";
213
221
  readonly description: "Output results in a JSON file";
@@ -79,8 +79,9 @@ const CLI_PROJECT_COMMANDS_CONFIG = {
79
79
  'Run a script in all workspaces that have it in their "scripts" field in package.json',
80
80
  options: {
81
81
  parallel: {
82
- flags: "--parallel",
83
- description: "Run the scripts in parallel",
82
+ flags: "--parallel [max]",
83
+ description:
84
+ "Run the scripts in parallel. Pass an optional number, percentage, or keyword: 'default', 'auto', 'unbounded'",
84
85
  },
85
86
  args: {
86
87
  flags: "--args <args>",
@@ -95,6 +96,10 @@ const CLI_PROJECT_COMMANDS_CONFIG = {
95
96
  description:
96
97
  "Run the script as an inline command from the workspace directory",
97
98
  },
99
+ inlineName: {
100
+ flags: "--inline-name <name>",
101
+ description: "An optional name for the script when --inline is passed",
102
+ },
98
103
  jsonOutfile: {
99
104
  flags: "--json-outfile <file>",
100
105
  description: "Output results in a JSON file",
@@ -0,0 +1,8 @@
1
+ export declare const USER_ENV_VARS: {
2
+ readonly parallelMaxDefault: "BW_PARALLEL_MAX_DEFAULT";
3
+ };
4
+ export type UserEnvVarName = keyof typeof USER_ENV_VARS;
5
+ export declare const getUserEnvVar: (key: UserEnvVarName) => string | undefined;
6
+ export declare const getUserEnvVarName: (
7
+ key: UserEnvVarName,
8
+ ) => "BW_PARALLEL_MAX_DEFAULT";
@@ -0,0 +1,8 @@
1
+ // CONCATENATED MODULE: ./src/config/userEnvVars.ts
2
+ const USER_ENV_VARS = {
3
+ parallelMaxDefault: "BW_PARALLEL_MAX_DEFAULT",
4
+ };
5
+ const getUserEnvVar = (key) => process.env[USER_ENV_VARS[key]];
6
+ const getUserEnvVarName = (key) => USER_ENV_VARS[key];
7
+
8
+ export { USER_ENV_VARS, getUserEnvVar, getUserEnvVarName };
package/src/index.d.ts CHANGED
@@ -13,10 +13,15 @@ export {
13
13
  type WorkspaceScriptCommandMethod,
14
14
  type RunWorkspaceScriptOptions,
15
15
  type RunWorkspaceScriptResult,
16
+ type InlineScriptOptions,
16
17
  type RunScriptAcrossWorkspacesOptions,
17
18
  type RunScriptAcrossWorkspacesResult,
18
19
  type OutputChunk,
19
20
  type OutputStreamName,
21
+ type PercentageValue,
22
+ type ParallelMaxValue,
23
+ type ParallelOption,
24
+ type RunScriptsParallelOptions,
20
25
  } from "./project";
21
26
  export { type Workspace } from "./workspaces";
22
27
  export { type SimpleAsyncIterable } from "./internal/types";
@@ -19,7 +19,7 @@ const RUNTIME_MODE = RUNTIME_MODE_VALUES.includes(_RUNTIME_MODE)
19
19
  if (RUNTIME_MODE !== _RUNTIME_MODE) {
20
20
  // eslint-disable-next-line no-console
21
21
  console.error(
22
- `Env var RUNTIME_MODE has an invalid value: "${_RUNTIME_MODE}". Defaulting to "${RUNTIME_MODE}". Accepted values: ${RUNTIME_MODE_VALUES.join(", ")}.`,
22
+ `Env var _BW_RUNTIME_MODE has an invalid value: "${_RUNTIME_MODE}". Defaulting to "${RUNTIME_MODE}". Accepted values: ${RUNTIME_MODE_VALUES.join(", ")}.`,
23
23
  );
24
24
  }
25
25
  const IS_TEST = RUNTIME_MODE === "test";
@@ -1,7 +1,11 @@
1
1
  import type { Simplify } from "../../internal/types";
2
2
  import { type Workspace } from "../../workspaces";
3
3
  import type { Project } from "../project";
4
- import { type RunScriptResult, type RunScriptsResult } from "../runScript";
4
+ import {
5
+ type RunScriptResult,
6
+ type RunScriptsParallelOptions,
7
+ type RunScriptsResult,
8
+ } from "../runScript";
5
9
  import { ProjectBase } from "./projectBase";
6
10
  /** Arguments for {@link createFileSystemProject} */
7
11
  export type CreateFileSystemProjectOptions = {
@@ -14,6 +18,10 @@ export type CreateFileSystemProjectOptions = {
14
18
  */
15
19
  name?: string;
16
20
  };
21
+ export interface InlineScriptOptions {
22
+ /** A name to act as a label for the inline script */
23
+ scriptName: string;
24
+ }
17
25
  /** Arguments for `FileSystemProject.runWorkspaceScript` */
18
26
  export type RunWorkspaceScriptOptions = {
19
27
  /** The name of the workspace to run the script in */
@@ -21,7 +29,7 @@ export type RunWorkspaceScriptOptions = {
21
29
  /** The name of the script to run, or an inline command when `inline` is true */
22
30
  script: string;
23
31
  /** Whether to run the script as an inline command */
24
- inline?: boolean;
32
+ inline?: boolean | InlineScriptOptions;
25
33
  /** The arguments to append to the script command */
26
34
  args?: string;
27
35
  };
@@ -33,6 +41,7 @@ export type RunWorkspaceScriptMetadata = {
33
41
  export type RunWorkspaceScriptResult = Simplify<
34
42
  RunScriptResult<RunWorkspaceScriptMetadata>
35
43
  >;
44
+ export type ParallelOption = boolean | RunScriptsParallelOptions;
36
45
  /** Arguments for `FileSystemProject.runScriptAcrossWorkspaces` */
37
46
  export type RunScriptAcrossWorkspacesOptions = {
38
47
  /** Workspace names, aliases, or patterns including a wildcard */
@@ -40,11 +49,11 @@ export type RunScriptAcrossWorkspacesOptions = {
40
49
  /** The name of the script to run, or an inline command when `inline` is true */
41
50
  script: string;
42
51
  /** Whether to run the script as an inline command */
43
- inline?: boolean;
52
+ inline?: boolean | InlineScriptOptions;
44
53
  /** The arguments to append to the script command. `<workspaceName>` will be replaced with the workspace name */
45
54
  args?: string;
46
55
  /** Whether to run the scripts in parallel (series by default) */
47
- parallel?: boolean;
56
+ parallel?: ParallelOption;
48
57
  };
49
58
  /** Result of `FileSystemProject.runScriptAcrossWorkspaces` */
50
59
  export type RunScriptAcrossWorkspacesResult = Simplify<
@@ -69,13 +69,17 @@ class _FileSystemProject extends ProjectBase {
69
69
  logger.debug(
70
70
  `Running script ${options.script} in workspace: ${workspace.name}`,
71
71
  );
72
+ const inlineScriptName =
73
+ typeof options.inline === "object"
74
+ ? (options.inline?.scriptName ?? "")
75
+ : "";
72
76
  const scriptRuntimeMetadata = {
73
77
  projectPath: this.rootDirectory,
74
78
  projectName: this.name,
75
79
  workspacePath: resolveWorkspacePath(this, workspace),
76
80
  workspaceRelativePath: workspace.path,
77
81
  workspaceName: workspace.name,
78
- scriptName: options.inline ? "" : options.script,
82
+ scriptName: options.inline ? inlineScriptName : options.script,
79
83
  };
80
84
  const args = interpolateScriptRuntimeMetadata(
81
85
  options.args ?? "",
@@ -134,13 +138,17 @@ class _FileSystemProject extends ProjectBase {
134
138
  );
135
139
  return runScripts({
136
140
  scripts: workspaces.map((workspace) => {
141
+ const inlineScriptName =
142
+ typeof options.inline === "object"
143
+ ? (options.inline?.scriptName ?? "")
144
+ : "";
137
145
  const scriptRuntimeMetadata = {
138
146
  projectPath: this.rootDirectory,
139
147
  projectName: this.name,
140
148
  workspacePath: resolveWorkspacePath(this, workspace),
141
149
  workspaceRelativePath: workspace.path,
142
150
  workspaceName: workspace.name,
143
- scriptName: options.inline ? "" : options.script,
151
+ scriptName: options.inline ? inlineScriptName : options.script,
144
152
  };
145
153
  const args = interpolateScriptRuntimeMetadata(
146
154
  options.args ?? "",
@@ -170,7 +178,7 @@ class _FileSystemProject extends ProjectBase {
170
178
  env: createScriptRuntimeEnvVars(scriptRuntimeMetadata),
171
179
  };
172
180
  }),
173
- parallel: !!options.parallel,
181
+ parallel: options.parallel ?? false,
174
182
  });
175
183
  }
176
184
  }
@@ -1,3 +1,5 @@
1
1
  export * from "./runScript";
2
2
  export * from "./runScripts";
3
3
  export * from "./scriptCommand";
4
+ export * from "./outputChunk";
5
+ export * from "./parallel";
@@ -1,3 +1,5 @@
1
1
  export * from "./runScript.mjs";
2
2
  export * from "./runScripts.mjs";
3
- export * from "./scriptCommand.mjs"; // CONCATENATED MODULE: ./src/project/runScript/index.ts
3
+ export * from "./scriptCommand.mjs";
4
+ export * from "./outputChunk.mjs";
5
+ export * from "./parallel.mjs"; // CONCATENATED MODULE: ./src/project/runScript/index.ts
@@ -0,0 +1,22 @@
1
+ import type { OutputStreamName } from "./runScript";
2
+ export interface DecodeOptions {
3
+ /** Whether to strip ANSI escape codes */
4
+ stripAnsi?: boolean;
5
+ }
6
+ /** Output captured from a script subprocess */
7
+ export interface OutputChunk {
8
+ /** The source of the output, `"stdout"` or `"stderr"` */
9
+ streamName: OutputStreamName;
10
+ /** Raw output text. Pass `true` to strip ANSI escape codes. */
11
+ decode(options?: DecodeOptions): string;
12
+ /** The raw output content */
13
+ raw: Uint8Array<ArrayBuffer>;
14
+ /** @deprecated Use `decode()` instead */
15
+ text: string;
16
+ /** @deprecated Use `decode({ stripAnsi: true })` instead */
17
+ textNoAnsi: string;
18
+ }
19
+ export declare const createOutputChunk: (
20
+ streamName: OutputStreamName,
21
+ raw: Uint8Array<ArrayBuffer>,
22
+ ) => OutputChunk;
@@ -0,0 +1,30 @@
1
+ import { sanitizeAnsi } from "../../internal/regex.mjs"; // CONCATENATED MODULE: external "../../internal/regex.mjs"
2
+ // CONCATENATED MODULE: ./src/project/runScript/outputChunk.ts
3
+
4
+ class _OutputChunk {
5
+ streamName;
6
+ raw;
7
+ constructor(streamName, raw) {
8
+ this.streamName = streamName;
9
+ this.raw = raw;
10
+ }
11
+ decode(options) {
12
+ const { stripAnsi = false } = options ?? {};
13
+ const text = new TextDecoder().decode(this.raw);
14
+ return stripAnsi ? sanitizeAnsi(text) : text;
15
+ }
16
+ /** @deprecated Use `decode()` instead */ get text() {
17
+ // TODO remove in future major release
18
+ return this.decode();
19
+ }
20
+ /** @deprecated Use `decode(true)` instead */ get textNoAnsi() {
21
+ // TODO remove in future major release
22
+ return this.decode({
23
+ stripAnsi: true,
24
+ });
25
+ }
26
+ }
27
+ const createOutputChunk = (streamName, raw) =>
28
+ new _OutputChunk(streamName, raw);
29
+
30
+ export { createOutputChunk };
@@ -0,0 +1,12 @@
1
+ export type PercentageValue = `${number}%`;
2
+ export type ParallelMaxValue =
3
+ | number
4
+ | "auto"
5
+ | "default"
6
+ | "unbounded"
7
+ | PercentageValue;
8
+ /** Should always return at least 1 */
9
+ export declare const determineParallelMax: (
10
+ value: ParallelMaxValue,
11
+ fromEnvVar?: boolean,
12
+ ) => number;
@@ -0,0 +1,52 @@
1
+ import { availableParallelism } from "node:os";
2
+ import { getUserEnvVar, getUserEnvVarName } from "../../config/userEnvVars.mjs";
3
+ import { BunWorkspacesError } from "../../internal/error.mjs"; // CONCATENATED MODULE: external "node:os"
4
+ // CONCATENATED MODULE: external "../../config/userEnvVars.mjs"
5
+ // CONCATENATED MODULE: external "../../internal/error.mjs"
6
+ // CONCATENATED MODULE: ./src/project/runScript/parallel.ts
7
+
8
+ /** Should always return at least 1 */ const determineParallelMax = (
9
+ value,
10
+ fromEnvVar = false,
11
+ ) => {
12
+ const errorMessageSuffix = fromEnvVar
13
+ ? ` (set by env var ${getUserEnvVarName("parallelMaxDefault")})`
14
+ : "";
15
+ if (!isNaN(Number(value))) {
16
+ value = Math.floor(Number(value));
17
+ }
18
+ if (typeof value === "number") {
19
+ if (value < 1 || isNaN(value)) {
20
+ throw new BunWorkspacesError(
21
+ `Parallel max value must be at least 1${errorMessageSuffix}`,
22
+ );
23
+ }
24
+ return Math.floor(value);
25
+ }
26
+ if (value === "default") {
27
+ const defaultMax = getUserEnvVar("parallelMaxDefault")?.trim();
28
+ if (defaultMax === "default") return determineParallelMax("auto");
29
+ return determineParallelMax(defaultMax ?? "auto", true);
30
+ }
31
+ if (value === "unbounded") {
32
+ return Infinity;
33
+ }
34
+ const cpuCount = Math.max(1, availableParallelism());
35
+ if (value === "auto") {
36
+ return cpuCount;
37
+ }
38
+ if (value.endsWith("%")) {
39
+ const percentage = parseFloat(value.slice(0, -1));
40
+ if (isNaN(percentage) || percentage <= 0 || percentage > 100) {
41
+ throw new BunWorkspacesError(
42
+ `Parallel max value must be a number greater than 0 and less than or equal to 100${errorMessageSuffix}`,
43
+ );
44
+ }
45
+ return Math.max(1, Math.floor((cpuCount * percentage) / 100));
46
+ }
47
+ throw new BunWorkspacesError(
48
+ `Invalid parallel max value: ${JSON.stringify(value)}${errorMessageSuffix}`,
49
+ );
50
+ };
51
+
52
+ export { determineParallelMax };
@@ -1,15 +1,7 @@
1
1
  import type { SimpleAsyncIterable } from "../../internal/types";
2
+ import { type OutputChunk } from "./outputChunk";
2
3
  import type { ScriptCommand } from "./scriptCommand";
3
4
  export type OutputStreamName = "stdout" | "stderr";
4
- /** Output captured from a script subprocess */
5
- export type OutputChunk = {
6
- /** The source of the output, `"stdout"` or `"stderr"` */
7
- streamName: OutputStreamName;
8
- /** Raw output text */
9
- text: string;
10
- /** Stripped of ANSI escape codes */
11
- textNoAnsi: string;
12
- };
13
5
  export type RunScriptExit<ScriptMetadata extends object = object> = {
14
6
  exitCode: number;
15
7
  signal: NodeJS.Signals | null;
@@ -1,8 +1,8 @@
1
1
  import { mergeAsyncIterables } from "../../internal/mergeAsyncIterables.mjs";
2
2
  import { IS_WINDOWS } from "../../internal/os.mjs";
3
- import { sanitizeAnsi } from "../../internal/regex.mjs"; // CONCATENATED MODULE: external "../../internal/mergeAsyncIterables.mjs"
3
+ import { createOutputChunk } from "./outputChunk.mjs"; // CONCATENATED MODULE: external "../../internal/mergeAsyncIterables.mjs"
4
4
  // CONCATENATED MODULE: external "../../internal/os.mjs"
5
- // CONCATENATED MODULE: external "../../internal/regex.mjs"
5
+ // CONCATENATED MODULE: external "./outputChunk.mjs"
6
6
  // CONCATENATED MODULE: ./src/project/runScript/runScript.ts
7
7
 
8
8
  /**
@@ -28,12 +28,7 @@ import { sanitizeAnsi } from "../../internal/regex.mjs"; // CONCATENATED MODULE:
28
28
  const stream = proc[streamName];
29
29
  if (stream) {
30
30
  for await (const chunk of stream) {
31
- const text = new TextDecoder().decode(chunk);
32
- yield {
33
- streamName,
34
- text,
35
- textNoAnsi: sanitizeAnsi(text),
36
- };
31
+ yield createOutputChunk(streamName, chunk);
37
32
  }
38
33
  }
39
34
  }
@@ -1,9 +1,7 @@
1
1
  import type { SimpleAsyncIterable } from "../../internal/types";
2
- import {
3
- type OutputChunk,
4
- type RunScriptExit,
5
- type RunScriptResult,
6
- } from "./runScript";
2
+ import type { OutputChunk } from "./outputChunk";
3
+ import { type ParallelMaxValue } from "./parallel";
4
+ import { type RunScriptExit, type RunScriptResult } from "./runScript";
7
5
  import { type ScriptCommand } from "./scriptCommand";
8
6
  export type RunScriptsScript<ScriptMetadata extends object = object> = {
9
7
  scriptCommand: ScriptCommand;
@@ -36,9 +34,12 @@ export type RunScriptsResult<ScriptMetadata extends object = object> = {
36
34
  /** Resolves with a results summary after all scripts have exited */
37
35
  summary: Promise<RunScriptsSummary<ScriptMetadata>>;
38
36
  };
37
+ export type RunScriptsParallelOptions = {
38
+ max: ParallelMaxValue;
39
+ };
39
40
  export type RunScriptsOptions<ScriptMetadata extends object = object> = {
40
41
  scripts: RunScriptsScript<ScriptMetadata>[];
41
- parallel: boolean;
42
+ parallel: boolean | RunScriptsParallelOptions;
42
43
  };
43
44
  /** Run a list of scripts */
44
45
  export declare const runScripts: <ScriptMetadata extends object = object>({
@@ -1,49 +1,85 @@
1
1
  import { createAsyncIterableQueue } from "../../internal/asyncIterableQueue.mjs";
2
+ import { logger } from "../../internal/logger.mjs";
3
+ import { determineParallelMax } from "./parallel.mjs";
2
4
  import { runScript } from "./runScript.mjs"; // CONCATENATED MODULE: external "../../internal/asyncIterableQueue.mjs"
5
+ // CONCATENATED MODULE: external "../../internal/logger.mjs"
6
+ // CONCATENATED MODULE: external "./parallel.mjs"
3
7
  // CONCATENATED MODULE: external "./runScript.mjs"
4
8
  // CONCATENATED MODULE: ./src/project/runScript/runScripts.ts
5
9
 
6
10
  /** Run a list of scripts */ const runScripts = ({ scripts, parallel }) => {
7
11
  const startTime = new Date();
8
- const scriptStartTriggers = scripts.map((_, index) => {
12
+ const scriptTriggers = scripts.map((_, index) => {
9
13
  let trigger = () => {
10
14
  void 0;
11
15
  };
12
- let result = {};
13
16
  const promise = new Promise((res) => {
14
17
  trigger = () => res(result);
15
18
  });
16
- result = {
17
- promise,
18
- trigger,
19
- index,
20
- };
21
- return {
19
+ const result = {
22
20
  promise,
23
21
  trigger,
24
22
  index,
25
23
  };
24
+ return result;
26
25
  });
27
26
  const outputQueue = createAsyncIterableQueue();
28
27
  const scriptResults = scripts.map(() => null);
29
- function triggerScript(index) {
28
+ const parallelMax =
29
+ parallel === false
30
+ ? 1
31
+ : determineParallelMax(
32
+ typeof parallel === "boolean" ? "default" : parallel.max,
33
+ );
34
+ const parallelBatchSize = Math.min(parallelMax, scripts.length);
35
+ const recommendedParallelMax = determineParallelMax("auto");
36
+ if (
37
+ parallel &&
38
+ parallelBatchSize > recommendedParallelMax &&
39
+ process.env._BW_IS_INTERNAL_TEST !== "true"
40
+ ) {
41
+ logger.warn(
42
+ `Number of scripts to run in parallel (${parallelBatchSize}) is greater than the available CPUs (${recommendedParallelMax})`,
43
+ );
44
+ }
45
+ let runningScriptCount = 0;
46
+ let nextScriptIndex = 0;
47
+ const queueScript = (index) => {
48
+ if (runningScriptCount >= parallelMax) {
49
+ return;
50
+ }
30
51
  const scriptResult = {
31
52
  ...scripts[index],
32
- result: runScript(scripts[index]),
53
+ result: runScript({
54
+ ...scripts[index],
55
+ env: {
56
+ ...scripts[index].env,
57
+ _BW_PARALLEL_MAX: parallelMax.toString(),
58
+ },
59
+ }),
33
60
  };
34
61
  scriptResults[index] = scriptResult;
35
- scriptStartTriggers[index].trigger();
62
+ scriptTriggers[index].trigger();
63
+ runningScriptCount++;
64
+ nextScriptIndex++;
65
+ scriptResults[index].result.exit.then(() => {
66
+ runningScriptCount--;
67
+ if (nextScriptIndex < scripts.length) {
68
+ queueScript(nextScriptIndex);
69
+ }
70
+ });
36
71
  return scriptResult;
37
- }
38
- const awaitScriptResults = async () => {
72
+ };
73
+ const handleScriptProcesses = async () => {
39
74
  const outputReaders = [];
75
+ const scriptExits = [];
40
76
  let pendingScriptCount = scripts.length;
41
77
  while (pendingScriptCount > 0) {
42
78
  const { index } = await Promise.race(
43
- scriptStartTriggers.map((trigger) => trigger.promise),
79
+ scriptTriggers.map((trigger) => trigger.promise),
44
80
  );
45
81
  pendingScriptCount--;
46
- scriptStartTriggers[index].promise = new Promise(() => {
82
+ scriptTriggers[index].promise = new Promise(() => {
47
83
  void 0;
48
84
  });
49
85
  outputReaders.push(
@@ -58,18 +94,12 @@ import { runScript } from "./runScript.mjs"; // CONCATENATED MODULE: external ".
58
94
  );
59
95
  }
60
96
  await Promise.all(outputReaders);
97
+ await Promise.all(scriptExits);
61
98
  outputQueue.close();
62
99
  };
63
100
  const awaitSummary = async () => {
64
- if (parallel) {
65
- await Promise.all(
66
- scripts.map((_, index) => triggerScript(index).result.exit),
67
- );
68
- } else {
69
- for (let index = 0; index < scripts.length; index++) {
70
- await triggerScript(index).result.exit;
71
- }
72
- }
101
+ scripts.forEach((_, index) => queueScript(index));
102
+ await handleScriptProcesses();
73
103
  const scriptExitResults = await Promise.all(
74
104
  scripts.map((_, index) => scriptResults[index].result.exit),
75
105
  );
@@ -85,7 +115,6 @@ import { runScript } from "./runScript.mjs"; // CONCATENATED MODULE: external ".
85
115
  scriptResults: scriptExitResults,
86
116
  };
87
117
  };
88
- awaitScriptResults();
89
118
  return {
90
119
  output: outputQueue,
91
120
  summary: awaitSummary(),