bun-workspaces 1.0.0-alpha.13 → 1.0.0-alpha.15

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.
Files changed (31) hide show
  1. package/package.json +4 -3
  2. package/src/cli/createCli.d.ts +2 -0
  3. package/src/cli/createCli.mjs +14 -10
  4. package/src/cli/fatalErrorLogger.d.ts +1 -0
  5. package/src/cli/fatalErrorLogger.mjs +4 -0
  6. package/src/cli/globalOptions/globalOptions.mjs +18 -10
  7. package/src/cli/globalOptions/globalOptionsConfig.d.ts +1 -0
  8. package/src/cli/globalOptions/globalOptionsConfig.mjs +2 -1
  9. package/src/cli/projectCommands/projectCommandHandlers.d.ts +1 -0
  10. package/src/cli/projectCommands/projectCommandHandlers.mjs +58 -46
  11. package/src/cli/projectCommands/projectCommandsConfig.d.ts +5 -4
  12. package/src/cli/projectCommands/projectCommandsConfig.mjs +4 -3
  13. package/src/config/bunWorkspacesConfig.mjs +8 -6
  14. package/src/config/configFile.mjs +7 -4
  15. package/src/config/errors.d.ts +1 -0
  16. package/src/config/errors.mjs +3 -0
  17. package/src/internal/logger.d.ts +1 -1
  18. package/src/internal/logger.mjs +3 -1
  19. package/src/project/implementations/fileSystemProject.d.ts +6 -0
  20. package/src/project/implementations/fileSystemProject.mjs +19 -0
  21. package/src/project/implementations/memoryProject.d.ts +8 -0
  22. package/src/project/implementations/memoryProject.mjs +14 -0
  23. package/src/project/{project.d.ts → implementations/projectBase.d.ts} +14 -6
  24. package/src/project/{project.mjs → implementations/projectBase.mjs} +8 -26
  25. package/src/project/index.d.ts +2 -1
  26. package/src/project/index.mjs +2 -2
  27. package/src/workspaces/findWorkspaces.mjs +1 -2
  28. package/src/workspaces/index.d.ts +1 -1
  29. package/src/workspaces/packageJson.d.ts +1 -1
  30. package/src/workspaces/packageJson.mjs +1 -1
  31. package/src/workspaces/workspace.d.ts +3 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bun-workspaces",
3
- "version": "1.0.0-alpha.13",
3
+ "version": "1.0.0-alpha.15",
4
4
  "license": "MIT",
5
5
  "main": "src/index.mjs",
6
6
  "types": "src/index.d.ts",
@@ -10,11 +10,12 @@
10
10
  "url": "https://github.com/ScottMorse/bun-workspaces.git"
11
11
  },
12
12
  "bin": {
13
- "bun-workspaces": "bin/cli.js"
13
+ "bun-workspaces": "bin/cli.js",
14
+ "bw": "bun-workspaces"
14
15
  },
15
16
  "custom": {
16
17
  "bunVersion": {
17
- "build": "1.3.0",
18
+ "build": "1.3.1",
18
19
  "libraryConsumer": "^1.1.x"
19
20
  }
20
21
  },
@@ -1,6 +1,8 @@
1
1
  import { type Command } from "commander";
2
2
  export interface RunCliOptions {
3
3
  argv?: string | string[];
4
+ /** Should be `true` if args do not include the binary name (e.g. `bunx bun-workspaces`) */
5
+ programmatic?: true;
4
6
  }
5
7
  export interface CliProgram {
6
8
  run: (options?: RunCliOptions) => Promise<void>;
@@ -1,25 +1,23 @@
1
1
  import { createCommand } from "commander";
2
2
  import package_0 from "../../package.json";
3
3
  import { getRequiredBunVersion, validateCurrentBunVersion } from "../internal/bunVersion.mjs";
4
+ import { BunWorkspacesError } from "../internal/error.mjs";
4
5
  import { logger } from "../internal/logger.mjs";
6
+ import { fatalErrorLogger } from "./fatalErrorLogger.mjs";
5
7
  import { initializeWithGlobalOptions } from "./globalOptions/index.mjs";
6
8
  import { defineProjectCommands } from "./projectCommands/index.mjs";
7
9
  const createCli = ({ handleError, postInit, defaultCwd = process.cwd() } = {})=>{
8
- const run = async ({ argv = process.argv } = {})=>{
10
+ const run = async ({ argv = process.argv, programmatic } = {})=>{
9
11
  const errorListener = handleError ?? ((error)=>{
10
- logger.error(error);
11
- logger.error("Unhandled rejection");
12
+ fatalErrorLogger.error(error);
12
13
  process.exit(1);
13
14
  });
14
15
  process.on("unhandledRejection", errorListener);
15
16
  try {
16
- const program = createCommand("bunx bun-workspaces").description("CLI for utilities for Bun workspaces").version(package_0.version).configureOutput({
17
- writeOut: (s)=>logger.info(s),
18
- writeErr: (s)=>s.startsWith("Usage") ? logger.info(s) : logger.error(s)
19
- });
17
+ const program = createCommand("bunx bun-workspaces").description("A CLI on top of native Bun workspaces").version(package_0.version).showHelpAfterError(true);
20
18
  postInit?.(program);
21
19
  if (!validateCurrentBunVersion()) {
22
- logger.error(`Bun version mismatch. Required: ${getRequiredBunVersion()}, Found: ${Bun.version}`);
20
+ fatalErrorLogger.error(`Bun version mismatch. Required: ${getRequiredBunVersion()}, Found: ${Bun.version}`);
23
21
  process.exit(1);
24
22
  }
25
23
  const args = tempFixCamelCaseOptions("string" == typeof argv ? argv.split(/s+/) : argv);
@@ -29,9 +27,15 @@ const createCli = ({ handleError, postInit, defaultCwd = process.cwd() } = {})=>
29
27
  program,
30
28
  project
31
29
  });
32
- await program.parseAsync(args);
30
+ await program.parseAsync(args, {
31
+ from: programmatic ? "user" : "node"
32
+ });
33
33
  } catch (error) {
34
- errorListener(error);
34
+ if (error instanceof BunWorkspacesError) {
35
+ logger.debug(error);
36
+ fatalErrorLogger.error(error.message);
37
+ process.exit(1);
38
+ } else errorListener(error);
35
39
  } finally{
36
40
  process.off("unhandledRejection", errorListener);
37
41
  }
@@ -0,0 +1 @@
1
+ export declare const fatalErrorLogger: import("../internal/logger").Logger;
@@ -0,0 +1,4 @@
1
+ import { createLogger } from "../internal/logger.mjs";
2
+ const fatalErrorLogger = createLogger("fatalError");
3
+ fatalErrorLogger.printLevel = "error";
4
+ export { fatalErrorLogger };
@@ -1,27 +1,35 @@
1
- import path from "path";
1
+ import node_fs from "node:fs";
2
+ import node_path from "node:path";
2
3
  import { Option } from "commander";
3
4
  import { loadConfigFile } from "../../config/index.mjs";
5
+ import { defineErrors } from "../../internal/error.mjs";
4
6
  import { logger } from "../../internal/logger.mjs";
5
- import { createProject } from "../../project/index.mjs";
7
+ import { createFileSystemProject } from "../../project/index.mjs";
6
8
  import { getCliGlobalOptionConfig } from "./globalOptionsConfig.mjs";
9
+ const ERRORS = defineErrors("WorkingDirectoryNotFound", "WorkingDirectoryNotADirectory");
7
10
  const addGlobalOption = (program, optionName, defaultOverride)=>{
8
11
  const { mainOption, shortOption, description, param, values, defaultValue } = getCliGlobalOptionConfig(optionName);
9
- const option = new Option(`${shortOption} ${mainOption}${param ? ` <${param}>` : ""}`, description).default(defaultOverride ?? defaultValue);
10
- program.addOption(values?.length ? option.choices(values) : option);
12
+ let option = new Option(`${shortOption} ${mainOption}${param ? ` <${param}>` : ""}`, description);
13
+ const effectiveDefaultValue = defaultOverride ?? defaultValue;
14
+ if (effectiveDefaultValue) option = option.default(effectiveDefaultValue);
15
+ if (values?.length) option = option.choices(values);
16
+ program.addOption(option);
11
17
  };
12
- const getWorkingDirectory = (program, args, defaultCwd)=>{
18
+ const getWorkingDirectoryFromArgs = (program, args, defaultCwd)=>{
13
19
  addGlobalOption(program, "cwd", defaultCwd);
14
20
  program.parseOptions(args);
15
21
  return program.opts().cwd;
16
22
  };
17
- const getConfig = (program, args)=>{
23
+ const getConfigFileFromArgs = (program, args)=>{
18
24
  addGlobalOption(program, "configFile");
19
25
  program.parseOptions(args);
20
26
  return program.opts().configFile;
21
27
  };
22
28
  const defineGlobalOptions = (program, args, defaultCwd)=>{
23
- const cwd = getWorkingDirectory(program, args, defaultCwd);
24
- const configFilePath = getConfig(program, args);
29
+ const cwd = getWorkingDirectoryFromArgs(program, args, defaultCwd);
30
+ if (!node_fs.existsSync(cwd)) throw new ERRORS.WorkingDirectoryNotFound(`Working directory not found at path "${cwd}"`);
31
+ if (!node_fs.statSync(cwd).isDirectory()) throw new ERRORS.WorkingDirectoryNotADirectory(`Working directory is not a directory at path "${cwd}"`);
32
+ const configFilePath = getConfigFileFromArgs(program, args);
25
33
  const config = loadConfigFile(configFilePath, cwd);
26
34
  addGlobalOption(program, "logLevel");
27
35
  return {
@@ -32,12 +40,12 @@ const defineGlobalOptions = (program, args, defaultCwd)=>{
32
40
  const applyGlobalOptions = (options, config)=>{
33
41
  logger.printLevel = options.logLevel;
34
42
  logger.debug("Log level: " + options.logLevel);
35
- const project = createProject({
43
+ const project = createFileSystemProject({
36
44
  rootDir: options.cwd,
37
45
  workspaceAliases: config?.project?.workspaceAliases ?? {}
38
46
  });
39
47
  logger.debug(`Project: ${JSON.stringify(project.name)} (${project.workspaces.length} workspace${1 === project.workspaces.length ? "" : "s"})`);
40
- logger.debug("Project root: " + path.resolve(project.rootDir));
48
+ logger.debug("Project root: " + node_path.resolve(project.rootDir));
41
49
  return {
42
50
  project
43
51
  };
@@ -35,3 +35,4 @@ export declare const getCliGlobalOptionConfig: (optionName: CliGlobalOptionName)
35
35
  readonly values: null;
36
36
  readonly param: "path";
37
37
  };
38
+ export declare const getCliGlobalOptionNames: () => CliGlobalOptionName[];
@@ -29,4 +29,5 @@ const CLI_GLOBAL_OPTIONS_CONFIG = {
29
29
  }
30
30
  };
31
31
  const getCliGlobalOptionConfig = (optionName)=>CLI_GLOBAL_OPTIONS_CONFIG[optionName];
32
- export { getCliGlobalOptionConfig };
32
+ const getCliGlobalOptionNames = ()=>Object.keys(CLI_GLOBAL_OPTIONS_CONFIG);
33
+ export { getCliGlobalOptionConfig, getCliGlobalOptionNames };
@@ -1,5 +1,6 @@
1
1
  import { type Command } from "commander";
2
2
  import type { Project } from "../../project";
3
+ /** @todo DRY use of output text in cases such as having no workspaces/scripts */
3
4
  export interface ProjectCommandContext {
4
5
  project: Project;
5
6
  program: Command;
@@ -6,7 +6,7 @@ const createWorkspaceInfoLines = (workspace)=>[
6
6
  ` - Aliases: ${workspace.aliases.join(", ")}`,
7
7
  ` - Path: ${workspace.path}`,
8
8
  ` - Glob Match: ${workspace.matchPattern}`,
9
- ` - Scripts: ${Object.keys(workspace.packageJson.scripts).sort().join(", ")}`
9
+ ` - Scripts: ${workspace.scripts.join(", ")}`
10
10
  ];
11
11
  const createScriptInfoLines = (script, workspaces)=>[
12
12
  `Script: ${script}`,
@@ -36,37 +36,42 @@ const listWorkspaces = handleCommand("listWorkspaces", ({ project }, pattern, op
36
36
  if (options.nameOnly) lines.push(workspace.name);
37
37
  else lines.push(...createWorkspaceInfoLines(workspace));
38
38
  });
39
- if (!lines.length) logger.info("No workspaces found");
39
+ if (!lines.length && !options.nameOnly) logger.info("No workspaces found");
40
40
  if (lines.length) commandOutputLogger.info(lines.join("\n"));
41
41
  });
42
42
  const listScripts = handleCommand("listScripts", ({ project }, options)=>{
43
43
  logger.debug(`Command: List scripts (options: ${JSON.stringify(options)})`);
44
44
  const scripts = project.listScriptsWithWorkspaces();
45
45
  const lines = [];
46
+ if (!project.workspaces.length && !options.nameOnly) return void logger.info("No workspaces found");
47
+ if (!Object.keys(scripts).length && !options.nameOnly) return void logger.info("No scripts found");
46
48
  if (options.json) lines.push(...createJsonLines(options.nameOnly ? Object.keys(scripts) : Object.values(scripts).map(({ workspaces, ...rest })=>({
47
49
  ...rest,
48
50
  workspaces: workspaces.map(({ name })=>name)
49
51
  })), options));
50
- else {
51
- Object.values(scripts).sort(({ name: nameA }, { name: nameB })=>nameA.localeCompare(nameB)).forEach(({ name, workspaces })=>{
52
- if (options.nameOnly) lines.push(name);
53
- else lines.push(...createScriptInfoLines(name, workspaces));
54
- });
55
- if (!lines.length) logger.info("No scripts found");
56
- }
52
+ else Object.values(scripts).sort(({ name: nameA }, { name: nameB })=>nameA.localeCompare(nameB)).forEach(({ name, workspaces })=>{
53
+ if (options.nameOnly) lines.push(name);
54
+ else lines.push(...createScriptInfoLines(name, workspaces));
55
+ });
57
56
  if (lines.length) commandOutputLogger.info(lines.join("\n"));
58
57
  });
59
58
  const workspaceInfo = handleCommand("workspaceInfo", ({ project }, workspaceName, options)=>{
60
59
  logger.debug(`Command: Workspace info for ${workspaceName} (options: ${JSON.stringify(options)})`);
61
60
  const workspace = project.findWorkspaceByNameOrAlias(workspaceName);
62
- if (!workspace) return void logger.error(`Workspace not found: (options: ${JSON.stringify(workspaceName)})`);
61
+ if (!workspace) {
62
+ logger.error(`Workspace ${JSON.stringify(workspaceName)} not found`);
63
+ process.exit(1);
64
+ }
63
65
  commandOutputLogger.info((options.json ? createJsonLines(workspace, options) : createWorkspaceInfoLines(workspace)).join("\n"));
64
66
  });
65
67
  const scriptInfo = handleCommand("scriptInfo", ({ project }, script, options)=>{
66
68
  logger.debug(`Command: Script info for ${script} (options: ${JSON.stringify(options)})`);
67
69
  const scripts = project.listScriptsWithWorkspaces();
68
70
  const scriptMetadata = scripts[script];
69
- if (!scriptMetadata) return void logger.error(`Script not found: ${JSON.stringify(script)} (available: ${Object.keys(scripts).join(", ")})`);
71
+ if (!scriptMetadata) {
72
+ logger.error(`Script not found: ${JSON.stringify(script)}`);
73
+ process.exit(1);
74
+ }
70
75
  commandOutputLogger.info((options.json ? createJsonLines(options.workspacesOnly ? scriptMetadata.workspaces.map(({ name })=>name) : {
71
76
  name: scriptMetadata.name,
72
77
  workspaces: scriptMetadata.workspaces.map(({ name })=>name)
@@ -75,98 +80,105 @@ const scriptInfo = handleCommand("scriptInfo", ({ project }, script, options)=>{
75
80
  const runScript = handleCommand("runScript", async ({ project }, script, _workspaces, options)=>{
76
81
  logger.debug(`Command: Run script ${JSON.stringify(script)} for ${_workspaces.length ? "workspaces " + _workspaces.join(", ") : "all workspaces"} (parallel: ${!!options.parallel}, args: ${JSON.stringify(options.args)})`);
77
82
  const workspaces = _workspaces.length ? _workspaces.flatMap((workspacePattern)=>{
78
- if (workspacePattern.includes("*")) return project.findWorkspacesByPattern(workspacePattern).filter(({ packageJson: { scripts } })=>scripts?.[script]).map(({ name })=>name);
83
+ if (workspacePattern.includes("*")) return project.findWorkspacesByPattern(workspacePattern).filter(({ scripts })=>scripts.includes(script)).map(({ name })=>name);
79
84
  return [
80
85
  workspacePattern
81
86
  ];
82
- }).filter((workspace)=>project.workspaces.some(({ name })=>name === workspace)) : project.listWorkspacesWithScript(script).map(({ name })=>name);
87
+ }).map((workspaceName)=>project.findWorkspaceByNameOrAlias(workspaceName)).filter(Boolean) : project.listWorkspacesWithScript(script);
83
88
  if (!workspaces.length) {
84
- if (1 === _workspaces.length && !_workspaces[0].includes("*")) {
85
- const message = `Workspace not found: ${JSON.stringify(_workspaces[0])}`;
86
- logger.error(message);
87
- throw new Error(message);
88
- }
89
- const message = `No ${_workspaces.length ? "matching " : ""}workspaces found for script ${JSON.stringify(script)}`;
90
- logger.error(message);
91
- throw new Error(message);
89
+ if (1 !== _workspaces.length || _workspaces[0].includes("*")) logger.error(`No ${_workspaces.length ? "matching " : ""}workspaces found with script ${JSON.stringify(script)}`);
90
+ else logger.error(`Workspace not found: ${JSON.stringify(_workspaces[0])}`);
91
+ process.exit(1);
92
92
  }
93
- const scriptCommands = workspaces.map((workspaceName)=>project.createScriptCommand({
93
+ const scriptCommands = workspaces.map((workspace)=>project.createScriptCommand({
94
94
  scriptName: script,
95
- workspaceName,
95
+ workspaceName: workspace.name,
96
96
  method: "cd",
97
- args: options.args?.replace(/<workspace>/g, workspaceName) ?? ""
97
+ args: options.args?.replace(/<workspace>/g, workspace.name) ?? ""
98
98
  }));
99
99
  const runCommand = async ({ command, scriptName, workspace })=>{
100
- const commandLogger = createLogger(`${workspace.name}:${scriptName}`);
101
100
  const splitCommand = command.command.split(/\s+/g);
102
- commandLogger.debug(`Running script ${scriptName} in workspace ${workspace.name} (cwd: ${command.cwd}): ${splitCommand.join(" ")}`);
103
- const isSilent = "silent" === logger.printLevel;
101
+ logger.debug(`Running script ${scriptName} in workspace ${workspace.name} (cwd: ${command.cwd}): ${splitCommand.join(" ")}`);
104
102
  const proc = Bun.spawn(command.command.split(/\s+/g), {
105
103
  cwd: command.cwd,
106
104
  env: {
107
105
  ...process.env,
108
106
  FORCE_COLOR: "1"
109
107
  },
110
- stdout: isSilent ? "ignore" : "pipe",
111
- stderr: isSilent ? "ignore" : "pipe"
108
+ stdout: "pipe",
109
+ stderr: "pipe"
112
110
  });
113
111
  const linePrefix = options.noPrefix ? "" : `[${workspace.name}:${scriptName}] `;
114
- if (proc.stdout) for await (const chunk of proc.stdout)commandLogger.logOutput(chunk, "info", process.stdout, linePrefix);
115
- if (proc.stderr) for await (const chunk of proc.stderr)commandLogger.logOutput(chunk, "error", process.stderr, linePrefix);
116
- await proc.exited;
112
+ const pipeOutput = async (streamName)=>{
113
+ const stream = proc[streamName];
114
+ if (stream && "silent" !== logger.printLevel) for await (const chunk of stream)commandOutputLogger.logOutput(chunk, "info", process[streamName], linePrefix);
115
+ };
116
+ await Promise.all([
117
+ pipeOutput("stdout"),
118
+ pipeOutput("stderr"),
119
+ proc.exited
120
+ ]);
117
121
  return {
118
122
  scriptName,
119
123
  workspace,
120
124
  command,
121
125
  success: 0 === proc.exitCode,
126
+ exitCode: proc.exitCode,
122
127
  error: 0 === proc.exitCode ? null : new BunWorkspacesError(`Script exited with code ${proc.exitCode}`)
123
128
  };
124
129
  };
125
130
  const results = [];
126
- if (options.parallel) {
131
+ const runParallel = async ()=>{
127
132
  let i = 0;
128
133
  for await (const result of (await Promise.allSettled(scriptCommands.map(runCommand)))){
129
134
  if ("rejected" === result.status) results.push({
130
135
  success: false,
131
- workspaceName: workspaces[i],
132
- error: result.reason
136
+ workspaceName: workspaces[i].name,
137
+ error: result.reason,
138
+ exitCode: null
133
139
  });
134
140
  else results.push({
135
141
  success: result.value.success,
136
- workspaceName: workspaces[i],
137
- error: result.value.error
142
+ workspaceName: workspaces[i].name,
143
+ error: result.value.error,
144
+ exitCode: result.value.exitCode
138
145
  });
139
146
  i++;
140
147
  }
141
- } else {
148
+ };
149
+ const runSeries = async ()=>{
142
150
  let i = 0;
143
151
  for (const command of scriptCommands){
144
152
  try {
145
153
  const result = await runCommand(command);
146
154
  results.push({
147
155
  success: result.success,
148
- workspaceName: workspaces[i],
149
- error: result.error
156
+ workspaceName: workspaces[i].name,
157
+ error: result.error,
158
+ exitCode: result.exitCode
150
159
  });
151
160
  } catch (error) {
152
161
  results.push({
153
162
  success: false,
154
- workspaceName: workspaces[i],
155
- error: error
163
+ workspaceName: workspaces[i].name,
164
+ error: error,
165
+ exitCode: null
156
166
  });
157
167
  }
158
168
  i++;
159
169
  }
160
- }
170
+ };
171
+ if (options.parallel) await runParallel();
172
+ else await runSeries();
161
173
  let failCount = 0;
162
- results.forEach(({ success, workspaceName })=>{
174
+ results.forEach(({ success, workspaceName, exitCode })=>{
163
175
  if (!success) failCount++;
164
- logger.info(`${success ? "✅" : "❌"} ${workspaceName}: ${script}`);
176
+ logger.info(`${success ? "✅" : "❌"} ${workspaceName}: ${script}${exitCode ? ` (exited with code ${exitCode})` : ""}`);
165
177
  });
166
178
  const s = 1 === results.length ? "" : "s";
167
179
  if (failCount) {
168
180
  const message = `${failCount} of ${results.length} script${s} failed`;
169
- logger.error(message);
181
+ logger.info(message);
170
182
  process.exit(1);
171
183
  } else logger.info(`${results.length} script${s} ran successfully`);
172
184
  });
@@ -1,4 +1,4 @@
1
- export interface ProjectCommandConfig {
1
+ export interface CliProjectCommandConfig {
2
2
  command: string;
3
3
  aliases: string[];
4
4
  description: string;
@@ -7,8 +7,8 @@ export interface ProjectCommandConfig {
7
7
  description: string;
8
8
  }>;
9
9
  }
10
- export type ProjectCommandName = keyof typeof PROJECT_COMMANDS_CONFIG;
11
- declare const PROJECT_COMMANDS_CONFIG: {
10
+ export type CliProjectCommandName = keyof typeof CLI_PROJECT_COMMANDS_CONFIG;
11
+ declare const CLI_PROJECT_COMMANDS_CONFIG: {
12
12
  readonly listWorkspaces: {
13
13
  readonly command: "list-workspaces [pattern]";
14
14
  readonly aliases: ["ls", "list"];
@@ -101,7 +101,7 @@ declare const PROJECT_COMMANDS_CONFIG: {
101
101
  };
102
102
  };
103
103
  };
104
- export declare const getProjectCommandConfig: (commandName: ProjectCommandName) => {
104
+ export declare const getProjectCommandConfig: (commandName: CliProjectCommandName) => {
105
105
  readonly command: "list-workspaces [pattern]";
106
106
  readonly aliases: ["ls", "list"];
107
107
  readonly description: "List all workspaces";
@@ -188,4 +188,5 @@ export declare const getProjectCommandConfig: (commandName: ProjectCommandName)
188
188
  };
189
189
  };
190
190
  };
191
+ export declare const getCliProjectCommandNames: () => CliProjectCommandName[];
191
192
  export {};
@@ -1,4 +1,4 @@
1
- const PROJECT_COMMANDS_CONFIG = {
1
+ const CLI_PROJECT_COMMANDS_CONFIG = {
2
2
  listWorkspaces: {
3
3
  command: "list-workspaces [pattern]",
4
4
  aliases: [
@@ -100,5 +100,6 @@ const PROJECT_COMMANDS_CONFIG = {
100
100
  }
101
101
  }
102
102
  };
103
- const getProjectCommandConfig = (commandName)=>PROJECT_COMMANDS_CONFIG[commandName];
104
- export { getProjectCommandConfig };
103
+ const getProjectCommandConfig = (commandName)=>CLI_PROJECT_COMMANDS_CONFIG[commandName];
104
+ const getCliProjectCommandNames = ()=>Object.keys(CLI_PROJECT_COMMANDS_CONFIG);
105
+ export { getCliProjectCommandNames, getProjectCommandConfig };
@@ -1,17 +1,19 @@
1
1
  import { validateLogLevel } from "../internal/logger.mjs";
2
+ import { ERRORS } from "./errors.mjs";
3
+ const isJsonObject = (value)=>"object" == typeof value && null !== value && !Array.isArray(value);
2
4
  const validateCliConfig = (cliConfig)=>{
3
- if ("object" != typeof cliConfig || Array.isArray(cliConfig)) throw new Error('Config file: "cli" must be an object');
5
+ if (!isJsonObject(cliConfig)) throw new ERRORS.InvalidConfigFile('Config file: "cli" must be an object');
4
6
  if (cliConfig?.logLevel) validateLogLevel(cliConfig.logLevel);
5
7
  };
6
8
  const validateProjectConfig = (projectConfig)=>{
7
- if ("object" != typeof projectConfig || Array.isArray(projectConfig)) throw new Error('Config file: "project" must be an object');
8
- if (projectConfig?.workspaceAliases) {
9
- if ("object" != typeof projectConfig.workspaceAliases || Array.isArray(projectConfig.workspaceAliases)) throw new Error("Config file: project.workspaceAliases must be an object");
10
- for (const alias of Object.values(projectConfig.workspaceAliases))if ("string" != typeof alias) throw new Error("Config file: project.workspaceAliases must be an object with string keys and values");
9
+ if (!isJsonObject(projectConfig)) throw new ERRORS.InvalidConfigFile('Config file: "project" must be an object');
10
+ if (projectConfig?.workspaceAliases !== void 0) {
11
+ if (!isJsonObject(projectConfig.workspaceAliases)) throw new ERRORS.InvalidConfigFile("Config file: project.workspaceAliases must be an object");
12
+ for (const alias of Object.values(projectConfig.workspaceAliases))if ("string" != typeof alias) throw new ERRORS.InvalidConfigFile("Config file: project.workspaceAliases must be an object with string keys and values");
11
13
  }
12
14
  };
13
15
  const validateBunWorkspacesConfig = (config)=>{
14
- if ("object" != typeof config || Array.isArray(config)) throw new Error("Config file: must be an object");
16
+ if (!isJsonObject(config)) throw new ERRORS.InvalidConfigFile("Config file: must be an object");
15
17
  if (void 0 !== config.cli) validateCliConfig(config.cli);
16
18
  if (void 0 !== config.project) validateProjectConfig(config.project);
17
19
  };
@@ -1,6 +1,7 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
3
  import { validateBunWorkspacesConfig } from "./bunWorkspacesConfig.mjs";
4
+ import { ERRORS } from "./errors.mjs";
4
5
  const DEFAULT_CONFIG_FILE_PATH = "bw.json";
5
6
  const loadConfigFile = (filePath, rootDir = ".")=>{
6
7
  if (!filePath) {
@@ -9,13 +10,15 @@ const loadConfigFile = (filePath, rootDir = ".")=>{
9
10
  filePath = defaultFilePath;
10
11
  }
11
12
  filePath = path.resolve(rootDir, filePath);
13
+ if (!fs.existsSync(filePath)) throw new ERRORS.ConfigFileNotFound(`Config file not found at path "${filePath}"`);
12
14
  const configFile = fs.readFileSync(filePath, "utf8");
15
+ let json = null;
13
16
  try {
14
- const json = JSON.parse(configFile);
15
- validateBunWorkspacesConfig(json);
16
- return json;
17
+ json = JSON.parse(configFile);
17
18
  } catch (error) {
18
- throw new Error(`Config file: "${filePath}" is not a valid JSON file: ${error}`);
19
+ throw new ERRORS.InvalidConfigFileFormat(`Failed to parse config file at path "${filePath}": ${error.message}`);
19
20
  }
21
+ if (json) validateBunWorkspacesConfig(json);
22
+ return json;
20
23
  };
21
24
  export { DEFAULT_CONFIG_FILE_PATH, loadConfigFile };
@@ -0,0 +1 @@
1
+ export declare const ERRORS: import("../internal/error").DefinedErrors<"ConfigFileNotFound" | "InvalidConfigFile" | "InvalidConfigFileFormat">;
@@ -0,0 +1,3 @@
1
+ import { defineErrors } from "../internal/error.mjs";
2
+ const ERRORS = defineErrors("ConfigFileNotFound", "InvalidConfigFile", "InvalidConfigFileFormat");
3
+ export { ERRORS };
@@ -2,7 +2,7 @@ export declare const LOG_LEVELS: readonly ["debug", "info", "warn", "error"];
2
2
  export type LogLevel = (typeof LOG_LEVELS)[number];
3
3
  export type LogLevelSetting = LogLevel | "silent";
4
4
  export declare const validateLogLevel: (level: LogLevelSetting) => void;
5
- export type LogMetadata = Record<string, any>;
5
+ export type LogMetadata = Record<string, unknown>;
6
6
  export interface Log<Message extends string | Error = string, Metadata extends LogMetadata = LogMetadata> {
7
7
  message: Message;
8
8
  level: LogLevel;
@@ -1,4 +1,5 @@
1
1
  import { IS_PRODUCTION, IS_TEST } from "./env.mjs";
2
+ import { defineErrors } from "./error.mjs";
2
3
  const LOG_LEVELS = [
3
4
  "debug",
4
5
  "info",
@@ -6,9 +7,10 @@ const LOG_LEVELS = [
6
7
  "error"
7
8
  ];
8
9
  const getLevelNumber = (level)=>LOG_LEVELS.indexOf(level);
10
+ const ERRORS = defineErrors("InvalidLogLevel");
9
11
  const validateLogLevel = (level)=>{
10
12
  if ("silent" === level) return;
11
- if (!LOG_LEVELS.includes(level)) throw new Error(`Invalid log level: "${level}". Accepted values: ${LOG_LEVELS.join(", ")}`);
13
+ if (!LOG_LEVELS.includes(level)) throw new ERRORS.InvalidLogLevel(`Invalid log level: "${level}". Accepted values: ${LOG_LEVELS.join(", ")}`);
12
14
  };
13
15
  const createLogger = (name)=>new _Logger(name);
14
16
  class _Logger {
@@ -0,0 +1,6 @@
1
+ import { type Project } from "./projectBase";
2
+ export interface CreateFileSystemProjectOptions {
3
+ rootDir: string;
4
+ workspaceAliases?: Record<string, string>;
5
+ }
6
+ export declare const createFileSystemProject: (options: CreateFileSystemProjectOptions) => Project;
@@ -0,0 +1,19 @@
1
+ import { findWorkspacesFromPackage } from "../../workspaces/index.mjs";
2
+ import { ProjectBase } from "./projectBase.mjs";
3
+ class FileSystemProject extends ProjectBase {
4
+ rootDir;
5
+ workspaces;
6
+ name;
7
+ constructor(options){
8
+ super();
9
+ this.rootDir = options.rootDir;
10
+ const { name, workspaces } = findWorkspacesFromPackage({
11
+ rootDir: options.rootDir,
12
+ workspaceAliases: options.workspaceAliases
13
+ });
14
+ this.name = name;
15
+ this.workspaces = workspaces;
16
+ }
17
+ }
18
+ const createFileSystemProject = (options)=>new FileSystemProject(options);
19
+ export { createFileSystemProject };
@@ -0,0 +1,8 @@
1
+ import { type Workspace } from "../../workspaces";
2
+ import { type Project } from "./projectBase";
3
+ export interface CreateMemoryProjectOptions {
4
+ name: string;
5
+ workspaces: Workspace[];
6
+ rootDir: string;
7
+ }
8
+ export declare const createMemoryProject: (options: CreateMemoryProjectOptions) => Project;
@@ -0,0 +1,14 @@
1
+ import { ProjectBase } from "./projectBase.mjs";
2
+ class MemoryProject extends ProjectBase {
3
+ rootDir;
4
+ workspaces;
5
+ name;
6
+ constructor(options){
7
+ super();
8
+ this.rootDir = options.rootDir;
9
+ this.name = options.name;
10
+ this.workspaces = options.workspaces;
11
+ }
12
+ }
13
+ const createMemoryProject = (options)=>new MemoryProject(options);
14
+ export { createMemoryProject };
@@ -1,5 +1,5 @@
1
- import { type Workspace } from "../workspaces";
2
- import { type CreateScriptCommandOptions, type ScriptCommand } from "./scriptCommand";
1
+ import { type Workspace } from "../../workspaces";
2
+ import { type CreateScriptCommandOptions, type ScriptCommand } from "../scriptCommand";
3
3
  export interface ScriptMetadata {
4
4
  name: string;
5
5
  workspaces: Workspace[];
@@ -24,8 +24,16 @@ export interface Project {
24
24
  findWorkspacesByPattern(workspaceName: string): Workspace[];
25
25
  createScriptCommand(options: CreateProjectScriptCommandOptions): CreateProjectScriptCommandResult;
26
26
  }
27
- export interface CreateProjectOptions {
28
- rootDir: string;
29
- workspaceAliases?: Record<string, string>;
27
+ export declare abstract class ProjectBase implements Project {
28
+ abstract readonly name: string;
29
+ abstract readonly rootDir: string;
30
+ abstract readonly workspaces: Workspace[];
31
+ listWorkspacesWithScript(scriptName: string): Workspace[];
32
+ listScriptsWithWorkspaces(): Record<string, ScriptMetadata>;
33
+ findWorkspaceByName(workspaceName: string): Workspace | null;
34
+ findWorkspaceByAlias(alias: string): Workspace | null;
35
+ findWorkspaceByNameOrAlias(nameOrAlias: string): Workspace | null;
36
+ /** Accepts wildcard for finding a list of workspaces */
37
+ findWorkspacesByPattern(workspacePattern: string): Workspace[];
38
+ createScriptCommand(options: CreateProjectScriptCommandOptions): CreateProjectScriptCommandResult;
30
39
  }
31
- export declare const createProject: (options: CreateProjectOptions) => Project;
@@ -1,32 +1,15 @@
1
1
  import path from "path";
2
- import { createWildcardRegex } from "../internal/regex.mjs";
3
- import { findWorkspacesFromPackage } from "../workspaces/index.mjs";
4
- import { ERRORS } from "./errors.mjs";
5
- import { createScriptCommand } from "./scriptCommand.mjs";
6
- class _Project {
7
- options;
8
- rootDir;
9
- workspaceAliases;
10
- workspaces;
11
- name;
12
- constructor(options){
13
- this.options = options;
14
- this.rootDir = options.rootDir;
15
- this.workspaceAliases = options.workspaceAliases;
16
- const { name, workspaces } = findWorkspacesFromPackage({
17
- rootDir: options.rootDir,
18
- workspaceAliases: options.workspaceAliases
19
- });
20
- this.name = name;
21
- this.workspaces = workspaces;
22
- }
2
+ import { createWildcardRegex } from "../../internal/regex.mjs";
3
+ import { ERRORS } from "../errors.mjs";
4
+ import { createScriptCommand } from "../scriptCommand.mjs";
5
+ class ProjectBase {
23
6
  listWorkspacesWithScript(scriptName) {
24
- return this.workspaces.filter((workspace)=>workspace.packageJson.scripts?.[scriptName]);
7
+ return this.workspaces.filter((workspace)=>workspace.scripts.includes(scriptName));
25
8
  }
26
9
  listScriptsWithWorkspaces() {
27
10
  const scripts = new Set();
28
11
  this.workspaces.forEach((workspace)=>{
29
- Object.keys(workspace.packageJson.scripts ?? {}).forEach((script)=>scripts.add(script));
12
+ workspace.scripts.forEach((script)=>scripts.add(script));
30
13
  });
31
14
  return Array.from(scripts).sort((a, b)=>a.localeCompare(b)).map((name)=>({
32
15
  name,
@@ -56,7 +39,7 @@ class _Project {
56
39
  createScriptCommand(options) {
57
40
  const workspace = this.findWorkspaceByNameOrAlias(options.workspaceName);
58
41
  if (!workspace) throw new ERRORS.ProjectWorkspaceNotFound(`Workspace not found: ${JSON.stringify(options.workspaceName)}`);
59
- if (!workspace.packageJson.scripts?.[options.scriptName]) throw new ERRORS.WorkspaceScriptDoesNotExist(`Script not found in workspace ${JSON.stringify(workspace.name)}: ${JSON.stringify(options.scriptName)} (available: ${Object.keys(workspace.packageJson.scripts).join(", ") || "none"}`);
42
+ if (!workspace.scripts.includes(options.scriptName)) throw new ERRORS.WorkspaceScriptDoesNotExist(`Script not found in workspace ${JSON.stringify(workspace.name)}: ${JSON.stringify(options.scriptName)} (available: ${workspace.scripts.join(", ") || "none"})`);
60
43
  return {
61
44
  workspace,
62
45
  scriptName: options.scriptName,
@@ -68,5 +51,4 @@ class _Project {
68
51
  };
69
52
  }
70
53
  }
71
- const createProject = (options)=>new _Project(options);
72
- export { createProject };
54
+ export { ProjectBase };
@@ -1 +1,2 @@
1
- export { type Project, type CreateProjectOptions, type CreateProjectScriptCommandOptions, createProject, } from "./project";
1
+ export { type Project, type CreateProjectScriptCommandOptions, type ScriptMetadata, } from "./implementations/projectBase";
2
+ export { createFileSystemProject as createFileSystemProject, type CreateFileSystemProjectOptions, } from "./implementations/fileSystemProject";
@@ -1,2 +1,2 @@
1
- import { createProject } from "./project.mjs";
2
- export { createProject };
1
+ import { createFileSystemProject } from "./implementations/fileSystemProject.mjs";
2
+ export { createFileSystemProject };
@@ -19,7 +19,6 @@ const findWorkspaces = ({ rootDir, workspaceGlobs, workspaceAliases })=>{
19
19
  const packageJsonPath = resolvePackageJsonPath(item);
20
20
  if (packageJsonPath) excludedWorkspacePaths.push(path.relative(rootDir, path.dirname(packageJsonPath)));
21
21
  }
22
- console.log(negativePatterns, excludedWorkspacePaths);
23
22
  for (const pattern of positivePatterns)for (const item of scanWorkspaceGlob(pattern.replace(/^!/, ""), rootDir)){
24
23
  const packageJsonPath = resolvePackageJsonPath(item);
25
24
  if (packageJsonPath) {
@@ -31,7 +30,7 @@ const findWorkspaces = ({ rootDir, workspaceGlobs, workspaceAliases })=>{
31
30
  name: packageJsonContent.name ?? "",
32
31
  matchPattern: pattern,
33
32
  path: path.relative(rootDir, path.dirname(packageJsonPath)),
34
- packageJson: packageJsonContent,
33
+ scripts: Object.keys(packageJsonContent.scripts ?? {}).sort(),
35
34
  aliases: Object.entries(workspaceAliases ?? {}).filter(([_, value])=>value === packageJsonContent.name).map(([key])=>key)
36
35
  };
37
36
  if (!excludedWorkspacePaths.includes(workspace.path) && validateWorkspace(workspace, workspaces)) workspaces.push(workspace);
@@ -1,3 +1,3 @@
1
1
  export { findWorkspaces, findWorkspacesFromPackage, type FindWorkspacesOptions, } from "./findWorkspaces";
2
2
  export { type Workspace } from "./workspace";
3
- export type { ResolvedPackageJsonContent } from "./packageJson";
3
+ export type { resolvePackageJsonContent, resolvePackageJsonPath, scanWorkspaceGlob, ResolvedPackageJsonContent, } from "./packageJson";
@@ -4,5 +4,5 @@ export type ResolvedPackageJsonContent = {
4
4
  workspaces: string[];
5
5
  scripts: Record<string, string>;
6
6
  } & Record<string, unknown>;
7
- export declare const scanWorkspaceGlob: (pattern: string, rootDir: string) => Generator<string, void, void>;
7
+ export declare const scanWorkspaceGlob: (globPattern: string, rootDir: string) => Generator<string, void, void>;
8
8
  export declare const resolvePackageJsonContent: (packageJsonPath: string, rootDir: string, validations: ("workspaces" | "name" | "scripts")[]) => ResolvedPackageJsonContent;
@@ -8,7 +8,7 @@ const resolvePackageJsonPath = (directoryItem)=>{
8
8
  if (fs.existsSync(path.join(directoryItem, "package.json"))) return path.join(directoryItem, "package.json");
9
9
  return "";
10
10
  };
11
- const scanWorkspaceGlob = (pattern, rootDir)=>new Glob(pattern, {
11
+ const scanWorkspaceGlob = (globPattern, rootDir)=>new Glob(globPattern, {
12
12
  absolute: true,
13
13
  cwd: rootDir
14
14
  }).iterateSync();
@@ -1,4 +1,4 @@
1
- import type { ResolvedPackageJsonContent } from "./packageJson";
1
+ /** Metadata about a nested package within a Bun monorepo */
2
2
  export interface Workspace {
3
3
  /** The name of the workspace from its `package.json` */
4
4
  name: string;
@@ -6,8 +6,8 @@ export interface Workspace {
6
6
  path: string;
7
7
  /** The pattern from `"workspaces"` in the root `package.json`that this workspace was matched from*/
8
8
  matchPattern: string;
9
- /** The contents of the workspace's package.json, with `"workspaces"` and `"scripts"` resolved */
10
- packageJson: ResolvedPackageJsonContent;
9
+ /** The scripts available in package.json */
10
+ scripts: string[];
11
11
  /** Aliases assigned to the workspace via the `"workspaceAliases"` field in the config */
12
12
  aliases: string[];
13
13
  }