bun-workspaces 1.1.1 → 1.2.0

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
@@ -6,7 +6,14 @@
6
6
 
7
7
  ### [**See Full Documentation Here**: _https://bunworkspaces.com_](https://bunworkspaces.com)
8
8
 
9
- 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.
9
+ **Big Recent Updates!**
10
+
11
+ - Version 1 is here after the initial alpha! 🍔🍔👁️🍔🍔
12
+ - You can demo the CLI [directly in the browser](https://bunworkspaces.com/web-cli)
13
+ - There's now [an official blog](https://bunworkspaces.com/blog/bun-workspaces-v1) to cover noteworthy releases and more!
14
+ <hr/>
15
+
16
+ This is a CLI and TypeScript API to enhance your monorepo development with Bun's [native workspaces](https://bun.sh/docs/install/workspaces) feature for nested JavaScript/TypeScript packages.
10
17
 
11
18
  - Works right away, with no boilerplate required 🍔🍴
12
19
  - Get metadata about your monorepo 🤖
@@ -65,9 +72,6 @@ bw run lint my-workspace # Run for a single workspace
65
72
  bw run lint my-workspace-a my-workspace-b # Run for multiple workspaces
66
73
  bw run lint my-alias-a my-alias-b # Run by alias (set by optional config)
67
74
 
68
- bw run lint "my-workspace-*" # Run for matching workspace names
69
- bw run lint "alias:my-alias-pattern-*" "path:my-glob/**/*" # Use matching specifiers
70
-
71
75
  # A workspace's script will wait until any workspaces it depends on have completed
72
76
  # Similar to Bun's --filter behavior
73
77
  bw run lint --dep-order
@@ -75,6 +79,9 @@ bw run lint --dep-order
75
79
  # Continue running scripts even if a dependency fails
76
80
  bw run lint --dep-order --ignore-dep-failure
77
81
 
82
+ bw run lint "my-workspace-*" # Run for matching workspace names
83
+ bw run lint "alias:my-alias-pattern-*" "path:my-glob/**/*" # Use matching specifiers
84
+
78
85
  bw run lint --args="--my-appended-args" # Add args to each script call
79
86
  bw run lint --args="--my-arg=<workspaceName>" # Use the workspace name in args
80
87
 
@@ -140,14 +147,22 @@ const runSingleScript = async () => {
140
147
  const { output, exit } = project.runWorkspaceScript({
141
148
  workspaceNameOrAlias: "my-workspace",
142
149
  script: "my-script",
143
- args: "--my --appended --args", // optional, arguments to add to the command
150
+
151
+ // Optional. Arguments to add to the command
152
+ // Can be a string or an array of strings
153
+ // If string, the argv will be parsed POSIX-style
154
+ args: ["--my", "--appended", "--args"],
155
+
156
+ // Optional. Whether to ignore all output from the script.
157
+ // This saves memory when you don't need script output.
158
+ ignoreOutput: false,
144
159
  });
145
160
 
146
161
  // Get a stream of the script subprocess's output
147
162
  for await (const { chunk, metadata } of output.text()) {
148
- // console.log(chunk); // the content (string)
149
- // console.log(metadata.streamName); // "stdout" or "stderr"
150
- // console.log(metadata.workspace); // the workspace that the output came from
163
+ // console.log(chunk); // The output chunk's content (string)
164
+ // console.log(metadata.streamName); // The output stream, "stdout" or "stderr"
165
+ // console.log(metadata.workspace); // The target Workspace
151
166
  }
152
167
 
153
168
  // Get data about the script execution after it exits
@@ -173,8 +188,8 @@ const runManyScripts = async () => {
173
188
  // Required. The package.json "scripts" field name to run
174
189
  script: "my-script",
175
190
 
176
- // Optional. Arguments to add to the command
177
- args: "--my --appended --args",
191
+ // Optional. Arguments to add to the command (same as for runWorkspaceScript)
192
+ args: ["--my", "--appended", "--args"],
178
193
 
179
194
  // Optional. Whether to run the scripts in parallel (default: true)
180
195
  parallel: true,
@@ -187,6 +202,10 @@ const runManyScripts = async () => {
187
202
  // continue running scripts even if a dependency fails
188
203
  ignoreDependencyFailure: false,
189
204
 
205
+ // Optional. Whether to ignore all output from the scripts.
206
+ // This saves memory when you don't need script output.
207
+ ignoreOutput: false,
208
+
190
209
  // Optional, callback when script starts, skips, or exits
191
210
  onScriptEvent: (event, { workspace, exitResult }) => {
192
211
  // event: "start", "skip", "exit"
@@ -195,9 +214,9 @@ const runManyScripts = async () => {
195
214
 
196
215
  // Get a stream of script output
197
216
  for await (const { chunk, metadata } of output.text()) {
198
- // console.log(chunk); // the content (string)
217
+ // console.log(chunk); // the output chunk's content (string)
199
218
  // console.log(metadata.streamName); // "stdout" or "stderr"
200
- // console.log(metadata.workspace); // the workspace that the output came from
219
+ // console.log(metadata.workspace); // the Workspace that the output came from
201
220
  }
202
221
 
203
222
  // Get final summary data and script exit details after all scripts have completed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bun-workspaces",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "A monorepo management tool for Bun, with a CLI and API to enhance Bun's native workspaces.",
5
5
  "license": "MIT",
6
6
  "main": "src/index.mjs",
@@ -32,6 +32,7 @@
32
32
  }
33
33
  },
34
34
  "dependencies": {
35
- "commander": "^12.1.0"
35
+ "commander": "^12.1.0",
36
+ "shell-quote": "^1.8.3"
36
37
  }
37
38
  }
@@ -148,7 +148,7 @@ export declare const CLI_COMMANDS_CONFIG: {
148
148
  readonly outputStyle: {
149
149
  readonly flags: ["-o", "--output-style <style>"];
150
150
  readonly description: "The output style to use";
151
- readonly values: ["grouped", "prefixed", "plain"];
151
+ readonly values: ["grouped", "prefixed", "plain", "none"];
152
152
  };
153
153
  readonly groupedLines: {
154
154
  readonly flags: ["-L", "--grouped-lines <count>"];
@@ -309,7 +309,7 @@ export declare const getCliCommandConfig: (commandName: CliCommandName) =>
309
309
  readonly outputStyle: {
310
310
  readonly flags: ["-o", "--output-style <style>"];
311
311
  readonly description: "The output style to use";
312
- readonly values: ["grouped", "prefixed", "plain"];
312
+ readonly values: ["grouped", "prefixed", "plain", "none"];
313
313
  };
314
314
  readonly groupedLines: {
315
315
  readonly flags: ["-L", "--grouped-lines <count>"];
@@ -58,7 +58,7 @@ const runScript = handleProjectCommand(
58
58
  process.exit(1);
59
59
  }
60
60
  const scriptArgs = postTerminatorArgs.length
61
- ? postTerminatorArgs.join(" ")
61
+ ? postTerminatorArgs
62
62
  : options.args;
63
63
  if (positionalWorkspacePatterns.length && options.workspacePatterns) {
64
64
  logger.error(
@@ -73,6 +73,10 @@ const runScript = handleProjectCommand(
73
73
  `Command: Run ${options.inline ? "inline " : ""}script ${JSON.stringify(script)} for ${workspacePatterns.length ? "workspaces " + workspacePatterns.join(", ") : "all workspaces"}`,
74
74
  );
75
75
  logger.debug(`Options: ${JSON.stringify(options)}`);
76
+ const outputStyle = options.outputStyle
77
+ ? validateOutputStyle(options.outputStyle)
78
+ : getDefaultOutputStyle();
79
+ logger.debug(`Effective output style: ${outputStyle}`);
76
80
  const scriptEventTarget = createScriptEventTarget();
77
81
  const { output, summary, workspaces } = project.runScriptAcrossWorkspaces({
78
82
  workspacePatterns: workspacePatterns.length
@@ -90,7 +94,7 @@ const runScript = handleProjectCommand(
90
94
  args: scriptArgs,
91
95
  dependencyOrder: options.depOrder,
92
96
  ignoreDependencyFailure: options.ignoreDepFailure,
93
- ignoreOutput: logger.printLevel === "silent",
97
+ ignoreOutput: outputStyle === "none",
94
98
  onScriptEvent: (event, { workspace, exitResult }) => {
95
99
  setTimeout(() =>
96
100
  // place at end of call stack so listeners in render func receive event
@@ -168,11 +172,10 @@ const runScript = handleProjectCommand(
168
172
  prefix: false,
169
173
  stripDisruptiveControls,
170
174
  }),
175
+ none: async () => {
176
+ // no-op
177
+ },
171
178
  };
172
- const outputStyle = options.outputStyle
173
- ? validateOutputStyle(options.outputStyle)
174
- : getDefaultOutputStyle();
175
- logger.debug(`Effective output style: ${outputStyle}`);
176
179
  await outputStyleHandlers[outputStyle]();
177
180
  const exitResults = await summary;
178
181
  exitResults.scriptResults.forEach(
@@ -2,6 +2,7 @@ export declare const OUTPUT_STYLE_VALUES: readonly [
2
2
  "grouped",
3
3
  "prefixed",
4
4
  "plain",
5
+ "none",
5
6
  ];
6
7
  export type OutputStyleName = (typeof OUTPUT_STYLE_VALUES)[number];
7
8
  export declare const getDefaultOutputStyle: () => OutputStyleName;
@@ -3,7 +3,7 @@ import { IS_TTY } from "../../../../internal/core/runtime/terminal.mjs"; // CONC
3
3
  // CONCATENATED MODULE: external "../../../../internal/core/runtime/terminal.mjs"
4
4
  // CONCATENATED MODULE: ./src/cli/commands/runScript/output/outputStyle.ts
5
5
 
6
- const OUTPUT_STYLE_VALUES = ["grouped", "prefixed", "plain"];
6
+ const OUTPUT_STYLE_VALUES = ["grouped", "prefixed", "plain", "none"];
7
7
  const getDefaultOutputStyle = () => (IS_TTY ? "grouped" : "prefixed");
8
8
  const validateOutputStyle = (style) => {
9
9
  if (!OUTPUT_STYLE_VALUES.includes(style)) {
@@ -39,8 +39,8 @@ export type RunWorkspaceScriptOptions = {
39
39
  script: string;
40
40
  /** Whether to run the script as an inline command */
41
41
  inline?: boolean | InlineScriptOptions;
42
- /** The arguments to append to the script command */
43
- args?: string;
42
+ /** The arguments to append to the script command. If passed as a string, the argv will be parsed POSIX-style */
43
+ args?: string | string[];
44
44
  /** Set to `true` to ignore all output from the script. This saves memory when you don't need script output. */
45
45
  ignoreOutput?: boolean;
46
46
  };
@@ -89,8 +89,8 @@ export type RunScriptAcrossWorkspacesOptions = {
89
89
  script: string;
90
90
  /** Whether to run the script as an inline command */
91
91
  inline?: boolean | InlineScriptOptions;
92
- /** The arguments to append to the script command. `<workspaceName>` will be replaced with the workspace name */
93
- args?: string;
92
+ /** The arguments to append to the script command. If passed as a string, the argv will be parsed POSIX-style */
93
+ args?: string | string[];
94
94
  /** Whether to run the scripts in parallel (default: `true`). Pass `false` to run in series. */
95
95
  parallel?: ParallelOption;
96
96
  /** When `true`, run scripts so that dependent workspaces run only after their dependencies */
@@ -1,11 +1,15 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
+ import { parse, quote } from "shell-quote/";
3
4
  import { loadRootConfig } from "../../config/index.mjs";
4
5
  import { getUserEnvVar } from "../../config/userEnvVars/index.mjs";
5
6
  import {
6
7
  DEFAULT_TEMP_DIR,
8
+ IS_WINDOWS,
9
+ InvalidJSTypeError,
7
10
  expandHomePath,
8
11
  isPlainObject,
12
+ validateJSArray,
9
13
  validateJSTypes,
10
14
  } from "../../internal/core/index.mjs";
11
15
  import { logger } from "../../internal/logger/index.mjs";
@@ -27,6 +31,7 @@ import {
27
31
  resolveWorkspacePath,
28
32
  } from "./projectBase.mjs"; // CONCATENATED MODULE: external "fs"
29
33
  // CONCATENATED MODULE: external "path"
34
+ // CONCATENATED MODULE: external "shell-quote/"
30
35
  // CONCATENATED MODULE: external "../../config/index.mjs"
31
36
  // CONCATENATED MODULE: external "../../config/userEnvVars/index.mjs"
32
37
  // CONCATENATED MODULE: external "../../internal/core/index.mjs"
@@ -40,6 +45,41 @@ import {
40
45
  // CONCATENATED MODULE: external "./projectBase.mjs"
41
46
  // CONCATENATED MODULE: ./src/project/implementations/fileSystemProject.ts
42
47
 
48
+ const quoteArg = (arg, shell) =>
49
+ IS_WINDOWS && shell === "system"
50
+ ? `"${arg.replace(/"/g, '""')}"`
51
+ : quote([arg]);
52
+ const serializeArgs = (args, metadata, shell) => {
53
+ if (!args || args.length === 0) return "";
54
+ if (Array.isArray(args)) {
55
+ return args
56
+ .map((arg) =>
57
+ quoteArg(interpolateScriptRuntimeMetadata(arg, metadata, shell), shell),
58
+ )
59
+ .join(" ");
60
+ }
61
+ const interpolated = interpolateScriptRuntimeMetadata(args, metadata, shell);
62
+ // Escape backslashes in interpolated values before POSIX parse on Windows,
63
+ // so that path separators survive parse's escape processing (\\→\)
64
+ const parseInput =
65
+ IS_WINDOWS && shell === "system"
66
+ ? interpolated.replace(/\\/g, "\\\\")
67
+ : interpolated;
68
+ return parse(parseInput)
69
+ .flatMap((entry) => {
70
+ if (typeof entry === "string") {
71
+ return [quoteArg(entry, shell)];
72
+ }
73
+ if ("comment" in entry) {
74
+ return [];
75
+ }
76
+ if ("pattern" in entry) {
77
+ return [entry.pattern];
78
+ }
79
+ return [entry.op];
80
+ })
81
+ .join(" ");
82
+ };
43
83
  class _FileSystemProject extends ProjectBase {
44
84
  rootDirectory;
45
85
  workspaces;
@@ -122,11 +162,6 @@ class _FileSystemProject extends ProjectBase {
122
162
  typeofName: ["boolean", "object"],
123
163
  optional: true,
124
164
  },
125
- "args option": {
126
- value: options.args,
127
- typeofName: "string",
128
- optional: true,
129
- },
130
165
  "ignoreOutput option": {
131
166
  value: options.ignoreOutput,
132
167
  typeofName: "boolean",
@@ -137,6 +172,23 @@ class _FileSystemProject extends ProjectBase {
137
172
  throw: true,
138
173
  },
139
174
  );
175
+ if (options.args !== undefined) {
176
+ if (typeof options.args !== "string" && !Array.isArray(options.args)) {
177
+ throw new InvalidJSTypeError(
178
+ `Type error: args option expects type string | string[], received ${typeof options.args}`,
179
+ );
180
+ }
181
+ if (Array.isArray(options.args)) {
182
+ const argsError = validateJSArray({
183
+ value: options.args,
184
+ valueLabel: "args option",
185
+ itemOptions: {
186
+ typeofName: "string",
187
+ },
188
+ });
189
+ if (argsError) throw argsError;
190
+ }
191
+ }
140
192
  if (isPlainObject(options.inline)) {
141
193
  validateJSTypes(
142
194
  {
@@ -185,11 +237,7 @@ class _FileSystemProject extends ProjectBase {
185
237
  workspaceName: workspace.name,
186
238
  scriptName: options.inline ? inlineScriptName : options.script,
187
239
  };
188
- const args = interpolateScriptRuntimeMetadata(
189
- options.args ?? "",
190
- scriptRuntimeMetadata,
191
- shell,
192
- );
240
+ const args = serializeArgs(options.args, scriptRuntimeMetadata, shell);
193
241
  const script = options.inline
194
242
  ? interpolateScriptRuntimeMetadata(
195
243
  options.script,
@@ -243,11 +291,6 @@ class _FileSystemProject extends ProjectBase {
243
291
  typeofName: ["boolean", "object"],
244
292
  optional: true,
245
293
  },
246
- "args option": {
247
- value: options.args,
248
- typeofName: "string",
249
- optional: true,
250
- },
251
294
  "parallel option": {
252
295
  value: options.parallel,
253
296
  typeofName: ["boolean", "object"],
@@ -297,6 +340,23 @@ class _FileSystemProject extends ProjectBase {
297
340
  },
298
341
  );
299
342
  }
343
+ if (options.args !== undefined) {
344
+ if (typeof options.args !== "string" && !Array.isArray(options.args)) {
345
+ throw new InvalidJSTypeError(
346
+ `Type error: args option expects type string | string[], received ${typeof options.args}`,
347
+ );
348
+ }
349
+ if (Array.isArray(options.args)) {
350
+ const argsError = validateJSArray({
351
+ value: options.args,
352
+ valueLabel: "args option",
353
+ itemOptions: {
354
+ typeofName: "string",
355
+ },
356
+ });
357
+ if (argsError) throw argsError;
358
+ }
359
+ }
300
360
  if (isPlainObject(options.parallel)) {
301
361
  validateJSTypes(
302
362
  {
@@ -384,11 +444,7 @@ class _FileSystemProject extends ProjectBase {
384
444
  workspaceName: workspace.name,
385
445
  scriptName: options.inline ? inlineScriptName : options.script,
386
446
  };
387
- const args = interpolateScriptRuntimeMetadata(
388
- options.args ?? "",
389
- scriptRuntimeMetadata,
390
- shell,
391
- );
447
+ const args = serializeArgs(options.args, scriptRuntimeMetadata, shell);
392
448
  const script = options.inline
393
449
  ? interpolateScriptRuntimeMetadata(
394
450
  options.script,
@@ -125,8 +125,9 @@ const findWorkspaces = ({
125
125
  path.dirname(packageJsonPath),
126
126
  );
127
127
  const matchPattern =
128
- workspaceGlobs.find((glob) => new bun.Glob(glob).match(relativePath)) ??
129
- "";
128
+ workspaceGlobs.find((glob) =>
129
+ new bun.Glob(glob.replace(/\/+$/, "")).match(relativePath),
130
+ ) ?? "";
130
131
  const isRootWorkspace = workspacePath === rootDirectory;
131
132
  if (!matchPattern && !isRootWorkspace) {
132
133
  logger.debug(`No match pattern found for ${relativePath}`);