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 +2 -1
- package/package.json +1 -1
- package/src/cli/commands/commandHandlerUtils.d.ts +1 -0
- package/src/cli/commands/runScript/handleRunScript.mjs +2 -1
- package/src/cli/commands/runScript/output/renderGroupedOutput.d.ts +1 -0
- package/src/cli/commands/runScript/output/renderGroupedOutput.mjs +50 -27
- package/src/cli/createCli.mjs +4 -2
- package/src/cli/globalOptions/globalOptions.d.ts +2 -1
- package/src/cli/globalOptions/globalOptions.mjs +60 -9
- package/src/cli/globalOptions/globalOptionsConfig.d.ts +10 -1
- package/src/cli/globalOptions/globalOptionsConfig.mjs +10 -1
- package/src/cli/middleware.d.ts +11 -0
- package/src/cli/middleware.mjs +1 -0
- package/src/internal/core/runtime/os.d.ts +1 -1
- package/src/internal/core/runtime/os.mjs +2 -2
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
|
-
-
|
|
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
|
@@ -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, {
|
|
@@ -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(
|
|
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,
|
|
176
|
+
" ".repeat(Math.max(0, workspaceBoxWidth - visibleLength - padding)) +
|
|
135
177
|
textOps[BORDER_COLOR](" │")
|
|
136
178
|
);
|
|
137
179
|
};
|
|
138
180
|
linesToWrite.push({
|
|
139
|
-
text: borderText(
|
|
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(
|
|
190
|
+
"└" + "─".repeat(workspaceBoxWidth - 2) + "┘",
|
|
168
191
|
),
|
|
169
192
|
type: "border",
|
|
170
193
|
});
|
package/src/cli/createCli.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
46
|
-
addGlobalOption(program, "cwd"
|
|
47
|
+
const getWorkingDirectoryFromArgs = (program, args) => {
|
|
48
|
+
addGlobalOption(program, "cwd");
|
|
49
|
+
addGlobalOption(program, "workspaceRoot");
|
|
47
50
|
program.parseOptions(args);
|
|
48
|
-
|
|
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
|
|
51
|
-
|
|
52
|
-
|
|
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 (!
|
|
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,
|
|
144
|
+
const initializeWithGlobalOptions = (program, args, middleware) => {
|
|
94
145
|
program.allowUnknownOption(true);
|
|
95
|
-
const { cwd } = defineGlobalOptions(program, args,
|
|
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];
|
package/src/cli/middleware.d.ts
CHANGED
|
@@ -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 */
|
package/src/cli/middleware.mjs
CHANGED
|
@@ -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
|
|
5
|
+
const IS_POSIX = IS_MACOS || IS_LINUX;
|
|
6
6
|
|
|
7
|
-
export { IS_LINUX, IS_MACOS,
|
|
7
|
+
export { IS_LINUX, IS_MACOS, IS_POSIX, IS_WINDOWS };
|