bun-workspaces 0.2.0 → 1.0.0-alpha

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.
@@ -1,16 +1,17 @@
1
1
  import { type Command } from "commander";
2
- import { logger } from "../internal/logger";
2
+ import { BunWorkspacesError } from "../internal/error";
3
+ import { logger, createLogger } from "../internal/logger";
3
4
  import type { Project } from "../project";
4
5
  import type { Workspace } from "../workspaces";
5
6
 
6
7
  export interface ProjectCommandsContext {
7
8
  project: Project;
8
9
  program: Command;
9
- printLines: (...lines: string[]) => void;
10
10
  }
11
11
 
12
12
  const createWorkspaceInfoLines = (workspace: Workspace) => [
13
13
  `Workspace: ${workspace.name}`,
14
+ ` - Aliases: ${workspace.aliases.join(", ")}`,
14
15
  ` - Path: ${workspace.path}`,
15
16
  ` - Glob Match: ${workspace.matchPattern}`,
16
17
  ` - Scripts: ${Object.keys(workspace.packageJson.scripts).sort().join(", ")}`,
@@ -21,132 +22,194 @@ const createScriptInfoLines = (script: string, workspaces: Workspace[]) => [
21
22
  ...workspaces.map((workspace) => ` - ${workspace.name}`),
22
23
  ];
23
24
 
24
- const listWorkspaces = ({
25
- program,
26
- project,
27
- printLines,
28
- }: ProjectCommandsContext) => {
25
+ const createJsonLines = (data: unknown, options: { pretty: boolean }) =>
26
+ JSON.stringify(data, null, options.pretty ? 2 : undefined).split("\n");
27
+
28
+ export const commandOutputLogger = createLogger("");
29
+ commandOutputLogger.printLevel = "info";
30
+
31
+ const listWorkspaces = ({ program, project }: ProjectCommandsContext) => {
29
32
  program
30
33
  .command("list-workspaces [pattern]")
31
34
  .aliases(["ls", "list"])
32
35
  .description("List all workspaces")
33
36
  .option("--name-only", "Only show workspace names")
34
- .action((pattern, options) => {
35
- logger.debug("Command: List workspaces");
37
+ .option("--json", "Output as JSON")
38
+ .option("--pretty", "Pretty print JSON")
39
+ .action(
40
+ (
41
+ pattern,
42
+ options: { nameOnly: boolean; json: boolean; pretty: boolean },
43
+ ) => {
44
+ logger.debug(
45
+ `Command: List workspaces (options: ${JSON.stringify(options)})`,
46
+ );
36
47
 
37
- if (options.more) {
38
- logger.debug("Showing more metadata");
39
- }
48
+ const lines: string[] = [];
40
49
 
41
- const lines: string[] = [];
42
- (pattern
43
- ? project.findWorkspacesByPattern(pattern)
44
- : project.workspaces
45
- ).forEach((workspace) => {
46
- if (options.nameOnly) {
47
- lines.push(workspace.name);
50
+ const workspaces = pattern
51
+ ? project.findWorkspacesByPattern(pattern)
52
+ : project.workspaces;
53
+
54
+ if (options.json) {
55
+ lines.push(
56
+ ...createJsonLines(
57
+ options.nameOnly
58
+ ? workspaces.map(({ name }) => name)
59
+ : workspaces,
60
+ options,
61
+ ),
62
+ );
48
63
  } else {
49
- lines.push(...createWorkspaceInfoLines(workspace));
64
+ workspaces.forEach((workspace) => {
65
+ if (options.nameOnly) {
66
+ lines.push(workspace.name);
67
+ } else {
68
+ lines.push(...createWorkspaceInfoLines(workspace));
69
+ }
70
+ });
50
71
  }
51
- });
52
72
 
53
- if (!lines.length) {
54
- lines.push("No workspaces found");
55
- }
73
+ if (!lines.length) {
74
+ logger.info("No workspaces found");
75
+ }
56
76
 
57
- printLines(...lines);
58
- });
77
+ if (lines.length) commandOutputLogger.info(lines.join("\n"));
78
+ },
79
+ );
59
80
  };
60
81
 
61
- const listScripts = ({
62
- program,
63
- project,
64
- printLines,
65
- }: ProjectCommandsContext) => {
82
+ const listScripts = ({ program, project }: ProjectCommandsContext) => {
66
83
  program
67
84
  .command("list-scripts")
68
85
  .description("List all scripts available with their workspaces")
69
86
  .option("--name-only", "Only show script names")
70
- .action((options) => {
71
- logger.debug("Command: List scripts");
72
-
73
- const scripts = project.listScriptsWithWorkspaces();
74
- const lines: string[] = [];
75
- Object.values(scripts)
76
- .sort(({ name: nameA }, { name: nameB }) => nameA.localeCompare(nameB))
77
- .forEach(({ name, workspaces }) => {
78
- if (options.nameOnly) {
79
- lines.push(name);
80
- } else {
81
- lines.push(...createScriptInfoLines(name, workspaces));
82
- }
83
- });
87
+ .option("--json", "Output as JSON")
88
+ .option("--pretty", "Pretty print JSON")
89
+ .action(
90
+ (options: { nameOnly: boolean; json: boolean; pretty: boolean }) => {
91
+ logger.debug(
92
+ `Command: List scripts (options: ${JSON.stringify(options)})`,
93
+ );
84
94
 
85
- if (!lines.length) {
86
- lines.push("No scripts found");
87
- }
95
+ const scripts = project.listScriptsWithWorkspaces();
96
+ const lines: string[] = [];
97
+
98
+ if (options.json) {
99
+ lines.push(
100
+ ...createJsonLines(
101
+ options.nameOnly
102
+ ? Object.keys(scripts)
103
+ : Object.values(scripts).map(({ workspaces, ...rest }) => ({
104
+ ...rest,
105
+ workspaces: workspaces.map(({ name }) => name),
106
+ })),
107
+ options,
108
+ ),
109
+ );
110
+ } else {
111
+ Object.values(scripts)
112
+ .sort(({ name: nameA }, { name: nameB }) =>
113
+ nameA.localeCompare(nameB),
114
+ )
115
+ .forEach(({ name, workspaces }) => {
116
+ if (options.nameOnly) {
117
+ lines.push(name);
118
+ } else {
119
+ lines.push(...createScriptInfoLines(name, workspaces));
120
+ }
121
+ });
122
+
123
+ if (!lines.length) {
124
+ logger.info("No scripts found");
125
+ }
126
+ }
88
127
 
89
- printLines(...lines);
90
- });
128
+ if (lines.length) commandOutputLogger.info(lines.join("\n"));
129
+ },
130
+ );
91
131
  };
92
132
 
93
- const workspaceInfo = ({
94
- program,
95
- project,
96
- printLines,
97
- }: ProjectCommandsContext) => {
133
+ const workspaceInfo = ({ program, project }: ProjectCommandsContext) => {
98
134
  program
99
135
  .command("workspace-info <workspace>")
100
136
  .aliases(["info"])
101
137
  .description("Show information about a workspace")
102
- .action((workspaceName) => {
103
- logger.debug(`Command: Workspace info for ${workspaceName}`);
138
+ .option("--json", "Output as JSON")
139
+ .option("--pretty", "Pretty print JSON")
140
+ .action(
141
+ (workspaceName: string, options: { json: boolean; pretty: boolean }) => {
142
+ logger.debug(
143
+ `Command: Workspace info for ${workspaceName} (options: ${JSON.stringify(options)})`,
144
+ );
104
145
 
105
- const workspace = project.findWorkspaceByName(workspaceName);
106
- if (!workspace) {
107
- logger.error(`Workspace not found: ${JSON.stringify(workspaceName)}`);
108
- return;
109
- }
146
+ const workspace = project.findWorkspaceByName(workspaceName);
147
+ if (!workspace) {
148
+ logger.error(
149
+ `Workspace not found: (options: ${JSON.stringify(workspaceName)})`,
150
+ );
151
+ return;
152
+ }
110
153
 
111
- printLines(...createWorkspaceInfoLines(workspace));
112
- });
154
+ commandOutputLogger.info(
155
+ (options.json
156
+ ? createJsonLines(workspace, options)
157
+ : createWorkspaceInfoLines(workspace)
158
+ ).join("\n"),
159
+ );
160
+ },
161
+ );
113
162
  };
114
163
 
115
- const scriptInfo = ({
116
- program,
117
- project,
118
- printLines,
119
- }: ProjectCommandsContext) => {
164
+ const scriptInfo = ({ program, project }: ProjectCommandsContext) => {
120
165
  program
121
166
  .command("script-info <script>")
122
167
  .description("Show information about a script")
123
168
  .option("--workspaces-only", "Only show script's workspace names")
124
- .action((script, options) => {
125
- logger.debug(`Command: Script info for ${script}`);
126
-
127
- const scripts = project.listScriptsWithWorkspaces();
128
- const scriptMetadata = scripts[script];
129
- if (!scriptMetadata) {
130
- printLines(
131
- `Script not found: ${JSON.stringify(
132
- script,
133
- )} (available: ${Object.keys(scripts).join(", ")})`,
169
+ .option("--json", "Output as JSON")
170
+ .option("--pretty", "Pretty print JSON")
171
+ .action(
172
+ (
173
+ script,
174
+ options: { workspacesOnly: boolean; json: boolean; pretty: boolean },
175
+ ) => {
176
+ logger.debug(
177
+ `Command: Script info for ${script} (options: ${JSON.stringify(options)})`,
134
178
  );
135
- return;
136
- }
137
- printLines(
138
- ...(options.workspacesOnly
139
- ? scriptMetadata.workspaces.map(({ name }) => name)
140
- : createScriptInfoLines(script, scriptMetadata.workspaces)),
141
- );
142
- });
179
+
180
+ const scripts = project.listScriptsWithWorkspaces();
181
+ const scriptMetadata = scripts[script];
182
+ if (!scriptMetadata) {
183
+ logger.error(
184
+ `Script not found: ${JSON.stringify(
185
+ script,
186
+ )} (available: ${Object.keys(scripts).join(", ")})`,
187
+ );
188
+ return;
189
+ }
190
+ commandOutputLogger.info(
191
+ (options.json
192
+ ? createJsonLines(
193
+ options.workspacesOnly
194
+ ? scriptMetadata.workspaces.map(({ name }) => name)
195
+ : {
196
+ name: scriptMetadata.name,
197
+ workspaces: scriptMetadata.workspaces.map(
198
+ ({ name }) => name,
199
+ ),
200
+ },
201
+ options,
202
+ )
203
+ : options.workspacesOnly
204
+ ? scriptMetadata.workspaces.map(({ name }) => name)
205
+ : createScriptInfoLines(script, scriptMetadata.workspaces)
206
+ ).join("\n"),
207
+ );
208
+ },
209
+ );
143
210
  };
144
211
 
145
- const runScript = ({
146
- program,
147
- project,
148
- printLines,
149
- }: ProjectCommandsContext) => {
212
+ const runScript = ({ program, project }: ProjectCommandsContext) => {
150
213
  program
151
214
  .command("run <script> [workspaces...]")
152
215
  .description("Run a script in all workspaces")
@@ -201,31 +264,45 @@ const runScript = ({
201
264
  scriptName,
202
265
  workspace,
203
266
  }: (typeof scriptCommands)[number]) => {
267
+ const commandLogger = createLogger(`${workspace.name}:${scriptName}`);
268
+
204
269
  const splitCommand = command.command.split(/\s+/g);
205
270
 
206
- logger.debug(
271
+ commandLogger.debug(
207
272
  `Running script ${scriptName} in workspace ${workspace.name} (cwd: ${
208
273
  command.cwd
209
274
  }): ${splitCommand.join(" ")}`,
210
275
  );
211
276
 
212
- const silent = logger.level === "silent";
213
-
214
- if (!silent) {
215
- printLines(
216
- `Running script ${JSON.stringify(
217
- scriptName,
218
- )} in workspace ${JSON.stringify(workspace.name)}`,
219
- );
220
- }
277
+ const isSilent = logger.printLevel === "silent";
221
278
 
222
279
  const proc = Bun.spawn(command.command.split(/\s+/g), {
223
280
  cwd: command.cwd,
224
281
  env: process.env,
225
- stdout: silent ? "ignore" : "inherit",
226
- stderr: silent ? "ignore" : "inherit",
282
+ stdout: isSilent ? "ignore" : "pipe",
283
+ stderr: isSilent ? "ignore" : "pipe",
227
284
  });
228
285
 
286
+ const linePrefix = `[${workspace.name}:${scriptName}] `;
287
+
288
+ if (proc.stdout) {
289
+ for await (const chunk of proc.stdout) {
290
+ const line = new TextDecoder().decode(chunk).trim();
291
+ line.split("\n").forEach((line) => {
292
+ commandLogger.info(linePrefix + line);
293
+ });
294
+ }
295
+ }
296
+
297
+ if (proc.stderr) {
298
+ for await (const chunk of proc.stderr) {
299
+ const line = new TextDecoder().decode(chunk).trim();
300
+ line.split("\n").forEach((line) => {
301
+ commandLogger.error(linePrefix + line);
302
+ });
303
+ }
304
+ }
305
+
229
306
  await proc.exited;
230
307
 
231
308
  return {
@@ -233,30 +310,20 @@ const runScript = ({
233
310
  workspace,
234
311
  command,
235
312
  success: proc.exitCode === 0,
313
+ error:
314
+ proc.exitCode === 0
315
+ ? null
316
+ : new BunWorkspacesError(
317
+ `Script exited with code ${proc.exitCode}`,
318
+ ),
236
319
  };
237
320
  };
238
321
 
239
- const handleError = (error: unknown, workspace: string) => {
240
- logger.error(error);
241
- program.error(
242
- `Script failed in ${workspace} (error: ${JSON.stringify((error as Error).message ?? error)})`,
243
- );
244
- };
245
-
246
- const handleResult = ({
247
- scriptName,
248
- workspace,
249
- success,
250
- }: (typeof scriptCommands)[number] & { success: boolean }) => {
251
- logger.info(
252
- `${success ? "✅" : "❌"} ${workspace.name}: ${scriptName}`,
253
- );
254
- if (!success) {
255
- program.error(
256
- `Script ${scriptName} failed in workspace ${workspace.name}`,
257
- );
258
- }
259
- };
322
+ const results = [] as {
323
+ success: boolean;
324
+ workspaceName: string;
325
+ error: Error | null;
326
+ }[];
260
327
 
261
328
  if (options.parallel) {
262
329
  let i = 0;
@@ -264,9 +331,17 @@ const runScript = ({
264
331
  scriptCommands.map(runCommand),
265
332
  )) {
266
333
  if (result.status === "rejected") {
267
- handleError(result.reason, workspaces[i]);
334
+ results.push({
335
+ success: false,
336
+ workspaceName: workspaces[i],
337
+ error: result.reason,
338
+ });
268
339
  } else {
269
- handleResult(result.value);
340
+ results.push({
341
+ success: result.value.success,
342
+ workspaceName: workspaces[i],
343
+ error: result.value.error,
344
+ });
270
345
  }
271
346
  i++;
272
347
  }
@@ -275,12 +350,39 @@ const runScript = ({
275
350
  for (const command of scriptCommands) {
276
351
  try {
277
352
  const result = await runCommand(command);
278
- handleResult(result);
353
+ results.push({
354
+ success: result.success,
355
+ workspaceName: workspaces[i],
356
+ error: result.error,
357
+ });
279
358
  } catch (error) {
280
- handleError(error, workspaces[i]);
359
+ results.push({
360
+ success: false,
361
+ workspaceName: workspaces[i],
362
+ error: error as Error,
363
+ });
281
364
  }
365
+ i++;
282
366
  }
283
- i++;
367
+ }
368
+
369
+ let failCount = 0;
370
+ results.forEach(({ success, workspaceName }) => {
371
+ if (!success) failCount++;
372
+ commandOutputLogger.info(
373
+ `${success ? "✅" : "❌"} ${workspaceName}: ${script}`,
374
+ );
375
+ });
376
+
377
+ const s = results.length === 1 ? "" : "s";
378
+ if (failCount) {
379
+ const message = `${failCount} of ${results.length} script${s} failed`;
380
+ commandOutputLogger.info(message);
381
+ process.exit(1);
382
+ } else {
383
+ commandOutputLogger.info(
384
+ `${results.length} script${s} ran successfully`,
385
+ );
284
386
  }
285
387
  });
286
388
  };
@@ -0,0 +1,62 @@
1
+ import { validateLogLevel, type LogLevelSetting } from "../internal/logger";
2
+
3
+ export interface CliConfig {
4
+ logLevel?: LogLevelSetting;
5
+ }
6
+
7
+ export interface ProjectConfig {
8
+ /** A map of aliases to a workspace name */
9
+ workspaceAliases?: Record<string, string>;
10
+ }
11
+
12
+ export interface BunWorkspacesConfig {
13
+ cli?: CliConfig;
14
+ project?: ProjectConfig;
15
+ }
16
+
17
+ const validateCliConfig = (cliConfig: CliConfig) => {
18
+ if (typeof cliConfig !== "object" || Array.isArray(cliConfig)) {
19
+ throw new Error(`Config file: "cli" must be an object`);
20
+ }
21
+
22
+ if (cliConfig?.logLevel) {
23
+ validateLogLevel(cliConfig.logLevel);
24
+ }
25
+ };
26
+
27
+ const validateProjectConfig = (projectConfig: ProjectConfig) => {
28
+ if (typeof projectConfig !== "object" || Array.isArray(projectConfig)) {
29
+ throw new Error(`Config file: "project" must be an object`);
30
+ }
31
+
32
+ if (projectConfig?.workspaceAliases) {
33
+ if (
34
+ typeof projectConfig.workspaceAliases !== "object" ||
35
+ Array.isArray(projectConfig.workspaceAliases)
36
+ ) {
37
+ throw new Error(
38
+ `Config file: project.workspaceAliases must be an object`,
39
+ );
40
+ }
41
+ for (const alias of Object.values(projectConfig.workspaceAliases)) {
42
+ if (typeof alias !== "string") {
43
+ throw new Error(
44
+ `Config file: project.workspaceAliases must be an object with string keys and values`,
45
+ );
46
+ }
47
+ }
48
+ }
49
+ };
50
+
51
+ export const validateBunWorkspacesConfig = (config: BunWorkspacesConfig) => {
52
+ if (typeof config !== "object" || Array.isArray(config)) {
53
+ throw new Error(`Config file: must be an object`);
54
+ }
55
+
56
+ if (typeof config.cli !== "undefined") {
57
+ validateCliConfig(config.cli);
58
+ }
59
+ if (typeof config.project !== "undefined") {
60
+ validateProjectConfig(config.project);
61
+ }
62
+ };
@@ -0,0 +1,33 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import {
4
+ validateBunWorkspacesConfig,
5
+ type BunWorkspacesConfig,
6
+ } from "./bunWorkspacesConfig";
7
+
8
+ export const DEFAULT_CONFIG_FILE_PATH = "bw.json";
9
+
10
+ export const loadConfigFile = (filePath?: string, rootDir = ".") => {
11
+ if (!filePath) {
12
+ const defaultFilePath = path.resolve(rootDir, DEFAULT_CONFIG_FILE_PATH);
13
+ if (fs.existsSync(defaultFilePath)) {
14
+ filePath = defaultFilePath;
15
+ } else {
16
+ return null;
17
+ }
18
+ }
19
+
20
+ filePath = path.resolve(rootDir, filePath);
21
+
22
+ const configFile = fs.readFileSync(filePath, "utf8");
23
+
24
+ try {
25
+ const json = JSON.parse(configFile);
26
+ validateBunWorkspacesConfig(json);
27
+ return json as BunWorkspacesConfig;
28
+ } catch (error) {
29
+ throw new Error(
30
+ `Config file: "${filePath}" is not a valid JSON file: ${error}`,
31
+ );
32
+ }
33
+ };
@@ -0,0 +1,7 @@
1
+ export {
2
+ validateBunWorkspacesConfig,
3
+ type BunWorkspacesConfig,
4
+ type CliConfig,
5
+ type ProjectConfig,
6
+ } from "./bunWorkspacesConfig";
7
+ export { loadConfigFile, DEFAULT_CONFIG_FILE_PATH } from "./configFile";
@@ -8,12 +8,14 @@ export const BUILD_BUN_VERSION = rootPackageJson.custom.bunVersion.build;
8
8
  export const getRequiredBunVersion = (build?: boolean) =>
9
9
  build ? BUILD_BUN_VERSION : LIBRARY_CONSUMER_BUN_VERSION;
10
10
 
11
+ const _Bun = typeof Bun === "undefined" ? ({} as typeof Bun) : Bun;
12
+
11
13
  /**
12
14
  * Validates that the provided version satisfies the required Bun version
13
15
  * specified in the root `package.json`.
14
16
  */
15
17
  export const validateBunVersion = (version: string, build?: boolean) =>
16
- Bun.semver.satisfies(version, getRequiredBunVersion(build));
18
+ _Bun ? _Bun.semver.satisfies(version, getRequiredBunVersion(build)) : true;
17
19
 
18
20
  /**
19
21
  *
@@ -21,4 +23,4 @@ export const validateBunVersion = (version: string, build?: boolean) =>
21
23
  * required Bun version specified in the root `package.json`.
22
24
  */
23
25
  export const validateCurrentBunVersion = (build?: boolean) =>
24
- validateBunVersion(Bun.version, build);
26
+ validateBunVersion(_Bun?.version, build);
@@ -1 +1,25 @@
1
- export const IS_TEST = process.env.NODE_ENV === "test";
1
+ const RUNTIME_MODE_VALUES = ["development", "production", "test"] as const;
2
+
3
+ export type RuntimeMode = "development" | "production" | "test";
4
+
5
+ const _RUNTIME_MODE: RuntimeMode = ((process.env
6
+ ._BW_RUNTIME_MODE as RuntimeMode) ||
7
+ (process.env.NODE_ENV?.match(/test(ing)?/)
8
+ ? "test"
9
+ : process.env.NODE_ENV === "development"
10
+ ? "development"
11
+ : "production")) as RuntimeMode;
12
+
13
+ export const RUNTIME_MODE = RUNTIME_MODE_VALUES.includes(_RUNTIME_MODE)
14
+ ? _RUNTIME_MODE
15
+ : "production";
16
+
17
+ if (RUNTIME_MODE !== _RUNTIME_MODE) {
18
+ console.error(
19
+ `Env var RUNTIME_MODE has an invalid value: "${_RUNTIME_MODE}". Defaulting to "${RUNTIME_MODE}". Accepted values: ${RUNTIME_MODE_VALUES.join(", ")}.`,
20
+ );
21
+ }
22
+
23
+ export const IS_TEST = RUNTIME_MODE === "test";
24
+ export const IS_PRODUCTION = RUNTIME_MODE === "production";
25
+ export const IS_DEVELOPMENT = RUNTIME_MODE === "development";