bun-workspaces 1.0.1 → 1.1.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
@@ -10,7 +10,7 @@ A CLI and API to enhance your monorepo development with Bun's [native workspaces
10
10
 
11
11
  - Works right away, with no boilerplate required 🍔🍴
12
12
  - Get metadata about your monorepo 🤖
13
- - Run package.json scripts across workspaces 📋
13
+ - Orchestrate your workspaces' `package.json` scripts 📋
14
14
  - Run inline [Bun Shell](https://bun.com/docs/runtime/shell) scripts in workspaces 🐚
15
15
 
16
16
  This is a tool to help manage a Bun monorepo, offering features beyond what [Bun's --filter feature](https://bun.com/docs/pm/filter) can do. It can be used to get a variety of metadata about your project and run scripts across your workspaces with advanced control.
@@ -84,6 +84,7 @@ bw run "bun build" --inline # Run an inline command via the Bun shell
84
84
  bw run lint --parallel=false # Run in series
85
85
  bw run lint --parallel=2 # Run in parallel with a max of 2 concurrent scripts
86
86
  bw run lint --parallel=auto # Default, based on number of available logical CPUs
87
+ bw run lint --parallel=50% # Run in parallel with a max of 50% of the "auto" limit
87
88
 
88
89
  # Use the grouped output style (default when on a TTY)
89
90
  bw run my-script --output-style=grouped
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bun-workspaces",
3
- "version": "1.0.1",
3
+ "version": "1.1.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",
@@ -13,6 +13,7 @@ export type GlobalCommandContext = {
13
13
  postTerminatorArgs: string[];
14
14
  middleware: CliMiddleware;
15
15
  outputWriters: Required<WriteOutputOptions>;
16
+ terminalWidth: number;
16
17
  };
17
18
  export type ProjectCommandContext = GlobalCommandContext & {
18
19
  project: FileSystemProject;
@@ -28,7 +28,7 @@ import { renderPlainOutput } from "./output/renderPlainOutput.mjs"; // CONCATENA
28
28
  const runScript = handleProjectCommand(
29
29
  "runScript",
30
30
  async (
31
- { project, postTerminatorArgs, outputWriters },
31
+ { project, postTerminatorArgs, outputWriters, terminalWidth },
32
32
  positionalScript,
33
33
  positionalWorkspacePatterns,
34
34
  options,
@@ -147,6 +147,7 @@ const runScript = handleProjectCommand(
147
147
  scriptEventTarget,
148
148
  groupedLines,
149
149
  outputWriters,
150
+ terminalWidth,
150
151
  ),
151
152
  prefixed: () =>
152
153
  renderPlainOutput(output, outputWriters, {
@@ -69,5 +69,6 @@ export declare const renderGroupedOutput: (
69
69
  scriptEventTarget: ScriptEventTarget,
70
70
  activeScriptLines: number | "all",
71
71
  outputWriters: Required<WriteOutputOptions>,
72
+ terminalWidth: number,
72
73
  ) => Promise<void>;
73
74
  export {};
@@ -71,6 +71,7 @@ const renderGroupedOutput = async (
71
71
  scriptEventTarget,
72
72
  activeScriptLines,
73
73
  outputWriters,
74
+ terminalWidth,
74
75
  ) => {
75
76
  const workspaceState = workspaces.reduce((acc, workspace) => {
76
77
  acc[workspace.name] = {
@@ -99,7 +100,7 @@ const renderGroupedOutput = async (
99
100
  isReset = true;
100
101
  logger.debug("Resetting TUI state");
101
102
  outputWriters.stdout(cursorOps.show());
102
- process.stdin.unref();
103
+ process.stdin.unref?.();
103
104
  process.stdin.setRawMode?.(false);
104
105
  };
105
106
  let previousHeight = 0;
@@ -111,18 +112,59 @@ const renderGroupedOutput = async (
111
112
  if (isFinal) {
112
113
  didFinalRender = true;
113
114
  }
114
- const width = Math.max(2, process.stdout.columns);
115
+ const width = Math.max(2, terminalWidth || process.stdout.columns);
115
116
  const linesToWrite = [];
117
+ const workspaceBoxContents = {};
116
118
  workspaces.forEach((workspace) => {
117
119
  const state = workspaceState[workspace.name];
120
+ let statusText = state.status;
121
+ const hasExitCode = state.exitCode && state.exitCode !== -1;
122
+ const exitState =
123
+ hasExitCode && state.signal
124
+ ? "exitAndSignal"
125
+ : hasExitCode
126
+ ? "exit"
127
+ : state.signal
128
+ ? "signal"
129
+ : null;
130
+ if (exitState === "exitAndSignal") {
131
+ statusText += ` (exit code: ${state.exitCode}, signal: ${state.signal})`;
132
+ } else if (exitState === "exit") {
133
+ statusText += ` (exit code: ${state.exitCode})`;
134
+ } else if (exitState === "signal") {
135
+ statusText += ` (signal: ${state.signal})`;
136
+ }
137
+ const workspaceLine = "Workspace: " + textOps.bold(workspace.name);
138
+ const statusLine =
139
+ " Status: " + textOps[STATUS_COLORS[state.status]](statusText);
140
+ workspaceBoxContents[workspace.name] = {
141
+ name: workspaceLine,
142
+ status: statusLine,
143
+ };
144
+ });
145
+ const padding = 4; // left border, spaces, right border
146
+ const workspaceBoxWidth = Math.min(
147
+ width,
148
+ Math.max(
149
+ ...Object.values(workspaceBoxContents).map((content) =>
150
+ Math.max(
151
+ calculateVisibleLength(content.name),
152
+ calculateVisibleLength(content.status),
153
+ ),
154
+ ),
155
+ ) + padding,
156
+ );
157
+ workspaces.forEach((workspace) => {
158
+ const state = workspaceState[workspace.name];
159
+ const { name: workspaceNameContent, status: statusTextContent } =
160
+ workspaceBoxContents[workspace.name];
118
161
  linesToWrite.push({
119
162
  text: textOps[BORDER_COLOR](
120
- "┌" + "─".repeat(Math.max(0, width - 2)) + "┐",
163
+ "┌" + "─".repeat(workspaceBoxWidth - 2) + "┐",
121
164
  ),
122
165
  type: "border",
123
166
  });
124
167
  const borderText = (text) => {
125
- const padding = 4; // left border, spaces, right border
126
168
  const visibleLength = calculateVisibleLength(text);
127
169
  const truncated =
128
170
  visibleLength > width - padding
@@ -131,40 +173,21 @@ const renderGroupedOutput = async (
131
173
  return (
132
174
  textOps[BORDER_COLOR]("│ ") +
133
175
  truncated +
134
- " ".repeat(Math.max(0, width - visibleLength - padding)) +
176
+ " ".repeat(Math.max(0, workspaceBoxWidth - visibleLength - padding)) +
135
177
  textOps[BORDER_COLOR](" │")
136
178
  );
137
179
  };
138
180
  linesToWrite.push({
139
- text: borderText("Workspace: " + textOps.bold(workspace.name)),
181
+ text: borderText(workspaceNameContent),
140
182
  type: "borderedContent",
141
183
  });
142
- let statusText = state.status;
143
- const hasExitCode = state.exitCode && state.exitCode !== -1;
144
- const exitState =
145
- hasExitCode && state.signal
146
- ? "exitAndSignal"
147
- : hasExitCode
148
- ? "exit"
149
- : state.signal
150
- ? "signal"
151
- : null;
152
- if (exitState === "exitAndSignal") {
153
- statusText += ` (exit code: ${state.exitCode}, signal: ${state.signal})`;
154
- } else if (exitState === "exit") {
155
- statusText += ` (exit code: ${state.exitCode})`;
156
- } else if (exitState === "signal") {
157
- statusText += ` (signal: ${state.signal})`;
158
- }
159
184
  linesToWrite.push({
160
- text: borderText(
161
- " Status: " + textOps[STATUS_COLORS[state.status]](statusText),
162
- ),
185
+ text: borderText(statusTextContent),
163
186
  type: "borderedContent",
164
187
  });
165
188
  linesToWrite.push({
166
189
  text: textOps[BORDER_COLOR](
167
- "└" + "─".repeat(Math.max(0, width - 2)) + "┘",
190
+ "└" + "─".repeat(workspaceBoxWidth - 2) + "┘",
168
191
  ),
169
192
  type: "border",
170
193
  });
@@ -29,7 +29,7 @@ const createCli = ({ defaultCwd = process.cwd(), defaultMiddleware } = {}) => {
29
29
  programmatic,
30
30
  middleware: _runMiddleware,
31
31
  writeOutput,
32
- terminalWidth,
32
+ terminalWidth = process.stdout.columns,
33
33
  } = {}) => {
34
34
  const middleware = resolveMiddleware(
35
35
  defaultMiddleware ?? {},
@@ -93,7 +93,7 @@ const createCli = ({ defaultCwd = process.cwd(), defaultMiddleware } = {}) => {
93
93
  const { project, projectError } = initializeWithGlobalOptions(
94
94
  program,
95
95
  args,
96
- defaultCwd,
96
+ middleware,
97
97
  );
98
98
  middleware.findProject({
99
99
  ...defaultContext,
@@ -111,12 +111,14 @@ const createCli = ({ defaultCwd = process.cwd(), defaultMiddleware } = {}) => {
111
111
  postTerminatorArgs,
112
112
  middleware,
113
113
  outputWriters,
114
+ terminalWidth,
114
115
  });
115
116
  defineGlobalCommands({
116
117
  program,
117
118
  postTerminatorArgs,
118
119
  middleware,
119
120
  outputWriters,
121
+ terminalWidth,
120
122
  });
121
123
  logger.debug(`Commands initialized. Parsing args...`);
122
124
  middleware.preParse({
@@ -1,8 +1,9 @@
1
1
  import { type Command } from "commander";
2
+ import type { CliMiddleware } from "../middleware";
2
3
  export declare const initializeWithGlobalOptions: (
3
4
  program: Command,
4
5
  args: string[],
5
- defaultCwd: string,
6
+ middleware: CliMiddleware,
6
7
  ) => {
7
8
  project: import("../../internal/core").Simplify<{
8
9
  readonly rootDirectory: string;
@@ -19,6 +19,8 @@ import { getCliGlobalOptionConfig } from "./globalOptionsConfig.mjs"; // CONCATE
19
19
  const ERRORS = defineErrors(
20
20
  "WorkingDirectoryNotFound",
21
21
  "WorkingDirectoryNotADirectory",
22
+ "NoCwdAndWorkspaceRoot",
23
+ "ProjectRootNotFound",
22
24
  );
23
25
  const addGlobalOption = (program, optionName, defaultOverride) => {
24
26
  const { mainOption, shortOption, description, param, values, defaultValue } =
@@ -42,19 +44,68 @@ const addGlobalOption = (program, optionName, defaultOverride) => {
42
44
  );
43
45
  }
44
46
  };
45
- const getWorkingDirectoryFromArgs = (program, args, defaultCwd) => {
46
- addGlobalOption(program, "cwd", defaultCwd);
47
+ const getWorkingDirectoryFromArgs = (program, args) => {
48
+ addGlobalOption(program, "cwd");
49
+ addGlobalOption(program, "workspaceRoot");
47
50
  program.parseOptions(args);
48
- return program.opts().cwd;
51
+ const { cwd, workspaceRoot } = program.opts();
52
+ if (cwd && workspaceRoot) {
53
+ throw new ERRORS.NoCwdAndWorkspaceRoot(
54
+ `Cannot use both ${getCliGlobalOptionConfig("cwd").mainOption} (${getCliGlobalOptionConfig("cwd").shortOption}) and ${getCliGlobalOptionConfig("workspaceRoot").mainOption} (${getCliGlobalOptionConfig("workspaceRoot").shortOption}) options together`,
55
+ );
56
+ }
57
+ return {
58
+ cwdOption: cwd,
59
+ workspaceRootOption: workspaceRoot,
60
+ };
49
61
  };
50
- const defineGlobalOptions = (program, args, defaultCwd) => {
51
- const cwd = getWorkingDirectoryFromArgs(program, args, defaultCwd);
52
- if (!fs.existsSync(cwd)) {
62
+ const findRootFromCwd = () => {
63
+ let currentDirectory = process.cwd();
64
+ while (true) {
65
+ const packageJsonPath = path.join(currentDirectory, "package.json");
66
+ if (fs.existsSync(packageJsonPath)) {
67
+ try {
68
+ const packageJsonContent = JSON.parse(
69
+ fs.readFileSync(packageJsonPath, "utf8"),
70
+ );
71
+ if (packageJsonContent.workspaces) {
72
+ return currentDirectory;
73
+ }
74
+ } catch {
75
+ continue;
76
+ }
77
+ }
78
+ const parentDirectory = path.dirname(currentDirectory);
79
+ if (parentDirectory === currentDirectory) {
80
+ break;
81
+ }
82
+ currentDirectory = parentDirectory;
83
+ }
84
+ throw new ERRORS.ProjectRootNotFound(
85
+ `${getCliGlobalOptionConfig("workspaceRoot").shortOption}|${getCliGlobalOptionConfig("workspaceRoot").mainOption} option: Project root not found from current working directory "${process.cwd()}"`,
86
+ );
87
+ };
88
+ const defineGlobalOptions = (program, args, middleware) => {
89
+ const { cwdOption, workspaceRootOption } = getWorkingDirectoryFromArgs(
90
+ program,
91
+ args,
92
+ );
93
+ const cwd =
94
+ cwdOption || (workspaceRootOption ? findRootFromCwd() : process.cwd());
95
+ const exists = fs.existsSync(cwd);
96
+ const isDirectory = exists ? fs.statSync(cwd).isDirectory() : false;
97
+ middleware.processWorkingDirectory({
98
+ commanderProgram: program,
99
+ workingDirectory: cwd,
100
+ exists,
101
+ isDirectory,
102
+ });
103
+ if (!exists) {
53
104
  throw new ERRORS.WorkingDirectoryNotFound(
54
105
  `Working directory not found at path "${cwd}"`,
55
106
  );
56
107
  }
57
- if (!fs.statSync(cwd).isDirectory()) {
108
+ if (!isDirectory) {
58
109
  throw new ERRORS.WorkingDirectoryNotADirectory(
59
110
  `Working directory is not a directory at path "${cwd}"`,
60
111
  );
@@ -90,9 +141,9 @@ const applyGlobalOptions = (options) => {
90
141
  projectError: error,
91
142
  };
92
143
  };
93
- const initializeWithGlobalOptions = (program, args, defaultCwd) => {
144
+ const initializeWithGlobalOptions = (program, args, middleware) => {
94
145
  program.allowUnknownOption(true);
95
- const { cwd } = defineGlobalOptions(program, args, defaultCwd);
146
+ const { cwd } = defineGlobalOptions(program, args, middleware);
96
147
  program.parseOptions(args);
97
148
  program.allowUnknownOption(false);
98
149
  const options = program.opts();
@@ -3,6 +3,7 @@ export interface CliGlobalOptions {
3
3
  logLevel: LogLevelSetting;
4
4
  cwd: string;
5
5
  includeRoot: boolean;
6
+ workspaceRoot: boolean;
6
7
  }
7
8
  export interface CliGlobalOptionConfig {
8
9
  mainOption: string;
@@ -28,7 +29,7 @@ export declare const getCliGlobalOptionConfig: (
28
29
  readonly mainOption: "--cwd";
29
30
  readonly shortOption: "-d";
30
31
  readonly description: "Working directory";
31
- readonly defaultValue: ".";
32
+ readonly defaultValue: "";
32
33
  readonly values: null;
33
34
  readonly param: "path";
34
35
  }
@@ -39,5 +40,13 @@ export declare const getCliGlobalOptionConfig: (
39
40
  readonly defaultValue: "";
40
41
  readonly values: null;
41
42
  readonly param: "";
43
+ }
44
+ | {
45
+ readonly mainOption: "--workspace-root";
46
+ readonly shortOption: "-w";
47
+ readonly description: "Run from the project root above the current working directory";
48
+ readonly defaultValue: "";
49
+ readonly values: null;
50
+ readonly param: "";
42
51
  };
43
52
  export declare const getCliGlobalOptionNames: () => CliGlobalOptionName[];
@@ -14,7 +14,7 @@ const CLI_GLOBAL_OPTIONS_CONFIG = {
14
14
  mainOption: "--cwd",
15
15
  shortOption: "-d",
16
16
  description: "Working directory",
17
- defaultValue: ".",
17
+ defaultValue: "",
18
18
  values: null,
19
19
  param: "path",
20
20
  },
@@ -26,6 +26,15 @@ const CLI_GLOBAL_OPTIONS_CONFIG = {
26
26
  values: null,
27
27
  param: "",
28
28
  },
29
+ workspaceRoot: {
30
+ mainOption: "--workspace-root",
31
+ shortOption: "-w",
32
+ description:
33
+ "Run from the project root above the current working directory",
34
+ defaultValue: "",
35
+ values: null,
36
+ param: "",
37
+ },
29
38
  };
30
39
  const getCliGlobalOptionConfig = (optionName) =>
31
40
  CLI_GLOBAL_OPTIONS_CONFIG[optionName];
@@ -10,9 +10,16 @@ export type InitProgramContext = {
10
10
  argv: string[];
11
11
  };
12
12
  export type ProcessArgvContext = {
13
+ commanderProgram: CommanderProgram;
13
14
  args: string[];
14
15
  postTerminatorArgs: string[];
15
16
  };
17
+ export type ProcessWorkingDirectoryContext = {
18
+ commanderProgram: CommanderProgram;
19
+ workingDirectory: string;
20
+ exists: boolean;
21
+ isDirectory: boolean;
22
+ };
16
23
  export type FindProjectContext = {
17
24
  commanderProgram: CommanderProgram;
18
25
  project: FileSystemProject;
@@ -52,6 +59,10 @@ export type CliMiddleware = {
52
59
  initProgram: (context: InitProgramContext) => CommanderProgram;
53
60
  /** Before the true parsing, just splitting the argv into args and post-terminator args */
54
61
  processArgv: (context: ProcessArgvContext) => CommanderProgram;
62
+ /** Before the working directory is changed */
63
+ processWorkingDirectory: (
64
+ context: ProcessWorkingDirectoryContext,
65
+ ) => CommanderProgram;
55
66
  /** After the project has been initialized from global options */
56
67
  findProject: (context: FindProjectContext) => CommanderProgram;
57
68
  /** Before the Commander program parses the args */
@@ -9,6 +9,7 @@ const resolveMiddleware = (defaultMiddleware, runMiddleware) =>
9
9
  catchError: null,
10
10
  initProgram: null,
11
11
  processArgv: null,
12
+ processWorkingDirectory: null,
12
13
  findProject: null,
13
14
  preParse: null,
14
15
  postParse: null,
@@ -1,4 +1,4 @@
1
1
  export declare const IS_WINDOWS: boolean;
2
2
  export declare const IS_MACOS: boolean;
3
3
  export declare const IS_LINUX: boolean;
4
- export declare const IS_UNIX: boolean;
4
+ export declare const IS_POSIX: boolean;
@@ -2,6 +2,6 @@
2
2
  const IS_WINDOWS = process.platform === "win32";
3
3
  const IS_MACOS = process.platform === "darwin";
4
4
  const IS_LINUX = process.platform === "linux";
5
- const IS_UNIX = IS_MACOS || IS_LINUX;
5
+ const IS_POSIX = IS_MACOS || IS_LINUX;
6
6
 
7
- export { IS_LINUX, IS_MACOS, IS_UNIX, IS_WINDOWS };
7
+ export { IS_LINUX, IS_MACOS, IS_POSIX, IS_WINDOWS };