bun-workspaces 1.5.2 → 1.7.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
@@ -1,5 +1,5 @@
1
1
  <a href="https://bunworkspaces.com">
2
- <img src="./packages/doc-website/src/pages/public/images/png/bwunster-bg-banner-wide_3000x900.png" alt="bun-workspaces" width="100%" />
2
+ <img src="./workspaces/web/documentation-website/src/pages/public/images/png/bwunster-bg-banner-wide_3000x900.png" alt="bun-workspaces" width="100%" />
3
3
  </a>
4
4
 
5
5
  # bun-workspaces
@@ -248,10 +248,104 @@ const runManyScripts = async () => {
248
248
  };
249
249
  ```
250
250
 
251
+ ### Configuration
252
+
253
+ `bun-workspaces` has no required configuration, but there are optional config files.
254
+
255
+ #### Workspace Config
256
+
257
+ Workspace configs can be placed in a workspace's directory at `bw.workspace.ts`.
258
+
259
+ [Workspace configuration documentation here](https://bunworkspaces.com/config/workspace)
260
+
261
+ ```typescript
262
+ // bw.workspace.ts — place in a workspace directory
263
+
264
+ // Also supported: bw.workspace.js, bw.workspace.json, bw.workspace.jsonc, or a "bw" key in package.json
265
+
266
+ import { defineWorkspaceConfig } from "bun-workspaces/config";
267
+
268
+ export default defineWorkspaceConfig({
269
+ alias: "my-web-app", // shorthand name; use array for multiple
270
+ tags: ["app", "frontend"],
271
+ scripts: {
272
+ // lower order runs first in sequenced script execution
273
+ build: { order: 1 },
274
+ test: { order: 2 },
275
+ },
276
+ rules: {
277
+ workspaceDependencies: {
278
+ // Only "my-workspace" or workspaces tagged "lib" are allowed as dependencies
279
+ allowPatterns: ["tag:lib", "my-workspace"],
280
+ // Workspaces tagged "backend" are forbidden as dependencies
281
+ denyPatterns: ["tag:backend"],
282
+ },
283
+ },
284
+ });
285
+ ```
286
+
287
+ #### Root Config
288
+
289
+ A root config can be placed in the project root directory at `bw.root.ts`,
290
+ which can also apply workspace configs in bulk by using workspace patterns.
291
+
292
+ [Root configuration documentation here](https://bunworkspaces.com/config/root)
293
+
294
+ [More on workspace pattern configs here](https://bunworkspaces.com/config/workspace-pattern-configs)
295
+
296
+ ```typescript
297
+ // bw.root.ts — place in your project root directory
298
+ // Also supported: bw.root.js, bw.root.json, bw.root.jsonc, or a "bw" key in package.json
299
+ import { defineRootConfig } from "bun-workspaces/config";
300
+
301
+ export default defineRootConfig({
302
+ defaults: {
303
+ // default value for --parallel option
304
+ parallelMax: 4,
305
+ // default value for --shell option
306
+ shell: "system",
307
+ // default value for global --include-root-workspace option
308
+ includeRootWorkspace: false,
309
+ },
310
+
311
+ // Apply workspace configs in bulk by workspace pattern, in order.
312
+ // Each entry merges into matching workspaces' accumulated config.
313
+ // Pattern matching reflects aliases and tags added by earlier entries.
314
+ workspacePatternConfigs: [
315
+ {
316
+ patterns: ["path:packages/apps/**/*"],
317
+ config: { tags: ["app"] },
318
+ },
319
+ {
320
+ patterns: ["path:packages/libs/**/*"],
321
+ config: { tags: ["lib"] },
322
+ },
323
+ {
324
+ // "tag:app" matches because the first entry added it
325
+ patterns: ["tag:app"],
326
+ config: {
327
+ rules: {
328
+ workspaceDependencies: {
329
+ allowPatterns: ["tag:lib"], // apps may only depend on libs
330
+ },
331
+ },
332
+ },
333
+ },
334
+ {
335
+ patterns: ["tag:app"],
336
+ // Factory form: receives static workspace data and accumulated config
337
+ config: (workspace, prevConfig) => ({
338
+ alias: workspace.name.replace(/^@my-scope\//, ""),
339
+ }),
340
+ },
341
+ ],
342
+ });
343
+ ```
344
+
251
345
  _`bun-workspaces` is independent from the [Bun](https://bun.sh) project and is not affiliated with or endorsed by Anthropic. This project aims to enhance the experience of Bun for its users._
252
346
 
253
347
  Developed By:
254
348
 
255
349
  <a href="https://smorsic.io" target="_blank" rel="noopener noreferrer">
256
- <img src="./packages/doc-website/src/pages/public/images/png/smorsic-banner_light_803x300.png" alt="Smorsic Labs logo" width="280" />
350
+ <img src="./workspaces/web/documentation-website/src/pages/public/images/png/smorsic-banner_light_803x300.png" alt="Smorsic Labs logo" width="280" />
257
351
  </a>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bun-workspaces",
3
- "version": "1.5.2",
3
+ "version": "1.7.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
  "exports": {
@@ -15,6 +15,10 @@
15
15
  "./config": {
16
16
  "types": "./src/config/public.d.ts",
17
17
  "default": "./src/config/public.mjs"
18
+ },
19
+ "./script": {
20
+ "types": "./src/runScript/public.d.ts",
21
+ "default": "./src/runScript/public.mjs"
18
22
  }
19
23
  },
20
24
  "types": "./src/index.d.ts",
package/src/1108.mjs CHANGED
@@ -24,7 +24,14 @@ const SCRIPT_RUNTIME_METADATA_CONFIG = {
24
24
  envVarName: "BW_WORKSPACE_NAME",
25
25
  },
26
26
  };
27
- const getScriptRuntimeMetadataConfig = (key) =>
28
- SCRIPT_RUNTIME_METADATA_CONFIG[key];
27
+ const validateScriptRuntimeMetadataKey = (key) => {
28
+ if (!(key in SCRIPT_RUNTIME_METADATA_CONFIG)) {
29
+ throw new Error(`Invalid script runtime metadata key: ${key}`);
30
+ }
31
+ };
32
+ const getScriptRuntimeMetadataConfig = (key) => {
33
+ validateScriptRuntimeMetadataKey(key);
34
+ return SCRIPT_RUNTIME_METADATA_CONFIG[key];
35
+ };
29
36
 
30
37
  export { getScriptRuntimeMetadataConfig };
@@ -215,6 +215,80 @@ const runManyScripts = async () => {
215
215
  }
216
216
  `.trim();
217
217
 
218
+ const ROOT_CONFIG_QUICKSTART = `
219
+ // bw.root.ts — place in your project root directory
220
+ // Also supported: bw.root.js, bw.root.json, bw.root.jsonc, or a "bw" key in package.json
221
+ import { defineRootConfig } from "bun-workspaces/config";
222
+
223
+ export default defineRootConfig({
224
+ defaults: {
225
+ // default value for --parallel option
226
+ parallelMax: 4,
227
+ // default value for --shell option
228
+ shell: "system",
229
+ // default value for global --include-root-workspace option
230
+ includeRootWorkspace: false,
231
+ },
232
+
233
+ // Apply workspace configs in bulk by workspace pattern, in order.
234
+ // Each entry merges into matching workspaces' accumulated config.
235
+ // Pattern matching reflects aliases and tags added by earlier entries.
236
+ workspacePatternConfigs: [
237
+ {
238
+ patterns: ["path:packages/apps/**/*"],
239
+ config: { tags: ["app"] },
240
+ },
241
+ {
242
+ patterns: ["path:packages/libs/**/*"],
243
+ config: { tags: ["lib"] },
244
+ },
245
+ {
246
+ // "tag:app" matches because the first entry added it
247
+ patterns: ["tag:app"],
248
+ config: {
249
+ rules: {
250
+ workspaceDependencies: {
251
+ allowPatterns: ["tag:lib"], // apps may only depend on libs
252
+ },
253
+ },
254
+ },
255
+ },
256
+ {
257
+ patterns: ["tag:app"],
258
+ // Factory form: receives static workspace data and accumulated config
259
+ config: (workspace, prevConfig) => ({
260
+ alias: workspace.name.replace(/^@my-scope\\//, ""),
261
+ }),
262
+ },
263
+ ],
264
+ });
265
+ `.trim();
266
+ const WORKSPACE_CONFIG_QUICKSTART = `
267
+ // bw.workspace.ts — place in a workspace directory
268
+
269
+ // Also supported: bw.workspace.js, bw.workspace.json, bw.workspace.jsonc, or a "bw" key in package.json
270
+
271
+ import { defineWorkspaceConfig } from "bun-workspaces/config";
272
+
273
+ export default defineWorkspaceConfig({
274
+ alias: "my-web-app", // shorthand name; use array for multiple
275
+ tags: ["app", "frontend"],
276
+ scripts: {
277
+ // lower order runs first in sequenced script execution
278
+ build: { order: 1 },
279
+ test: { order: 2 },
280
+ },
281
+ rules: {
282
+ workspaceDependencies: {
283
+ // Only "my-workspace" or workspaces tagged "lib" are allowed as dependencies
284
+ allowPatterns: ["tag:lib", "my-workspace"],
285
+ // Workspaces tagged "backend" are forbidden as dependencies
286
+ denyPatterns: ["tag:backend"],
287
+ },
288
+ },
289
+ });
290
+ `.trim();
291
+
218
292
  const SERVER_INSTRUCTIONS = `
219
293
  bun-workspaces MCP server: tools to query Bun monorepo workspace metadata and documentation resources for the bun-workspaces CLI and TypeScript API.
220
294
 
@@ -235,6 +309,18 @@ ${CLI_QUICKSTART}
235
309
  \`\`\`typescript
236
310
  ${API_QUICKSTART}
237
311
  \`\`\`
312
+
313
+ ## Root config quickstart
314
+
315
+ \`\`\`typescript
316
+ ${ROOT_CONFIG_QUICKSTART}
317
+ \`\`\`
318
+
319
+ ## Workspace config quickstart
320
+
321
+ \`\`\`typescript
322
+ ${WORKSPACE_CONFIG_QUICKSTART}
323
+ \`\`\`
238
324
  `.trim();
239
325
  const startBwMcpServer = async (project) => {
240
326
  const server = createMcpServer({
@@ -25,7 +25,18 @@ export type ParallelMaxValue =
25
25
  | (typeof PARALLEL_MAX_VALUES)[number]
26
26
  | PercentageValue;
27
27
  export type WorkspaceDependenciesRule = {
28
+ /**
29
+ * Workspace patterns that are permitted as dependencies.
30
+ * Only workspaces matching these patterns are allowed.
31
+ * When combined with `denyPatterns`, the deny list further
32
+ * filters this allowed subset.
33
+ */
28
34
  allowPatterns?: string[];
35
+ /**
36
+ * Workspace patterns that are forbidden as dependencies.
37
+ * When combined with `allowPatterns`, this further filters
38
+ * the subset of allowed workspaces.
39
+ */
29
40
  denyPatterns?: string[];
30
41
  };
31
42
  export type WorkspaceRules = {
@@ -41,12 +52,60 @@ export type ScriptConfig = {
41
52
  */
42
53
  order?: number;
43
54
  };
55
+ /** Configuration that applies to a specific workspace */
56
+ export type WorkspaceConfig = {
57
+ /**
58
+ * An alias or list of aliases for the workspace.
59
+ *
60
+ * These must be unique to other workspaces' aliases
61
+ * and package.json names.
62
+ */
63
+ alias?: string | string[];
64
+ /**
65
+ * Tags for the workspace.
66
+ *
67
+ * These can be used to group workspaces by a common tag.
68
+ */
69
+ tags?: string[];
70
+ /**
71
+ * Configuration that maps to a script name in the workspace's package.json.
72
+ */
73
+ scripts?: Record<string, ScriptConfig>;
74
+ /**
75
+ * Rules that validate the workspace.
76
+ */
77
+ rules?: WorkspaceRules;
78
+ };
44
79
  export type ResolvedWorkspaceConfig = {
45
80
  aliases: string[];
46
81
  tags: string[];
47
82
  scripts: Record<string, ScriptConfig>;
48
83
  rules: WorkspaceRules;
49
84
  };
85
+ /** Static workspace context passed to a {@link WorkspacePatternConfigFactory}.
86
+ * Contains only the immutable, package.json-derived fields — not config-derived fields like aliases or tags. */
87
+ export type RawWorkspace = {
88
+ name: string;
89
+ isRoot: boolean;
90
+ path: string;
91
+ matchPattern: string;
92
+ scripts: string[];
93
+ dependencies: string[];
94
+ dependents: string[];
95
+ };
96
+ /** A factory that returns a {@link WorkspaceConfig} to merge for a matched workspace.
97
+ * Receives the static workspace context and the workspace's accumulated resolved config at that point. */
98
+ export type WorkspacePatternConfigFactory = (
99
+ workspace: RawWorkspace,
100
+ prevConfig: ResolvedWorkspaceConfig,
101
+ ) => WorkspaceConfig;
102
+ /** A single entry in {@link RootConfig.workspacePatternConfigs} */
103
+ export type WorkspacePatternConfigEntry = {
104
+ /** Workspace patterns to match. Supports all workspace pattern specifiers (name, alias, tag, path, not:). */
105
+ patterns: string[];
106
+ /** Config to merge into all matching workspaces. May be a factory receiving the workspace context and accumulated config. */
107
+ config: WorkspaceConfig | WorkspacePatternConfigFactory;
108
+ };
50
109
  export type ResolvedRootConfig = {
51
110
  defaults: {
52
111
  parallelMax: number;
@@ -54,6 +113,7 @@ export type ResolvedRootConfig = {
54
113
  /** `undefined` means the value was not set in the input config */
55
114
  includeRootWorkspace: boolean | undefined;
56
115
  };
116
+ workspacePatternConfigs: WorkspacePatternConfigEntry[];
57
117
  };
58
118
  export type OutputStreamName = "stdout" | "stderr";
59
119
  export type ProcessOutputChunk<
@@ -251,7 +311,7 @@ export type CreateFileSystemProjectOptions = {
251
311
  export type InlineScriptOptions = {
252
312
  /** A name to act as a label for the inline script */
253
313
  scriptName?: string;
254
- /** Whether to run the script as an inline command */
314
+ /** Whether to use the system shell or Bun shell */
255
315
  shell?: ShellOption;
256
316
  };
257
317
  /** Arguments for `FileSystemProject.runWorkspaceScript` */
@@ -19,7 +19,18 @@ export type ParallelMaxValue =
19
19
  | (typeof PARALLEL_MAX_VALUES)[number]
20
20
  | PercentageValue;
21
21
  export type WorkspaceDependenciesRule = {
22
+ /**
23
+ * Workspace patterns that are permitted as dependencies.
24
+ * Only workspaces matching these patterns are allowed.
25
+ * When combined with `denyPatterns`, the deny list further
26
+ * filters this allowed subset.
27
+ */
22
28
  allowPatterns?: string[];
29
+ /**
30
+ * Workspace patterns that are forbidden as dependencies.
31
+ * When combined with `allowPatterns`, this further filters
32
+ * the subset of allowed workspaces.
33
+ */
23
34
  denyPatterns?: string[];
24
35
  };
25
36
  export type WorkspaceRules = {
@@ -65,6 +76,30 @@ export type ResolvedWorkspaceConfig = {
65
76
  scripts: Record<string, ScriptConfig>;
66
77
  rules: WorkspaceRules;
67
78
  };
79
+ /** Static workspace context passed to a {@link WorkspacePatternConfigFactory}.
80
+ * Contains only the immutable, package.json-derived fields — not config-derived fields like aliases or tags. */
81
+ export type RawWorkspace = {
82
+ name: string;
83
+ isRoot: boolean;
84
+ path: string;
85
+ matchPattern: string;
86
+ scripts: string[];
87
+ dependencies: string[];
88
+ dependents: string[];
89
+ };
90
+ /** A factory that returns a {@link WorkspaceConfig} to merge for a matched workspace.
91
+ * Receives the static workspace context and the workspace's accumulated resolved config at that point. */
92
+ export type WorkspacePatternConfigFactory = (
93
+ workspace: RawWorkspace,
94
+ prevConfig: ResolvedWorkspaceConfig,
95
+ ) => WorkspaceConfig;
96
+ /** A single entry in {@link RootConfig.workspacePatternConfigs} */
97
+ export type WorkspacePatternConfigEntry = {
98
+ /** Workspace patterns to match. Supports all workspace pattern specifiers (name, alias, tag, path, not:). */
99
+ patterns: string[];
100
+ /** Config to merge into all matching workspaces. May be a factory receiving the workspace context and accumulated config. */
101
+ config: WorkspaceConfig | WorkspacePatternConfigFactory;
102
+ };
68
103
  export type RootConfig = {
69
104
  defaults?: {
70
105
  /** The maximum number of scripts that can run in parallel. (default: "auto") */
@@ -74,6 +109,14 @@ export type RootConfig = {
74
109
  /** Whether to include the root workspace in the workspaces list by default. (default: false) */
75
110
  includeRootWorkspace?: boolean;
76
111
  };
112
+ /**
113
+ * Workspace configs applied by pattern, in order, merging left to right,
114
+ * using any workspaces' local configs as the starting config.
115
+ * Each entry's config is merged into all workspaces matching its patterns.
116
+ * Pattern matching reflects accumulated aliases and tags from previous entries.
117
+ * Factory functions are only supported in TypeScript/JavaScript config files.
118
+ */
119
+ workspacePatternConfigs?: WorkspacePatternConfigEntry[];
77
120
  };
78
121
  export type ResolvedRootConfig = {
79
122
  defaults: {
@@ -82,12 +125,27 @@ export type ResolvedRootConfig = {
82
125
  /** `undefined` means the value was not set in the input config */
83
126
  includeRootWorkspace: boolean | undefined;
84
127
  };
128
+ workspacePatternConfigs: WorkspacePatternConfigEntry[];
85
129
  };
86
130
  export declare const defineRootConfig: (
87
131
  config: RootConfig,
88
132
  ) => ResolvedRootConfig;
133
+ export type RootConfigFactory = (prev: RootConfig) => RootConfig;
134
+ export type RootConfigInput = RootConfig | RootConfigFactory;
135
+ /** Merge two or more root configs left to right, with each subsequent config taking precedence.
136
+ * Any argument may be a factory function receiving the accumulated config up to that point. */
137
+ export declare const mergeRootConfig: (
138
+ ...configs: RootConfigInput[]
139
+ ) => RootConfig;
89
140
  export declare const defineWorkspaceConfig: (
90
141
  config: WorkspaceConfig,
91
142
  ) => ResolvedWorkspaceConfig;
143
+ export type WorkspaceConfigFactory = (prev: WorkspaceConfig) => WorkspaceConfig;
144
+ export type WorkspaceConfigInput = WorkspaceConfig | WorkspaceConfigFactory;
145
+ /** Merge two or more workspace configs left to right, with each subsequent config taking precedence.
146
+ * Any argument may be a factory function receiving the accumulated config up to that point. */
147
+ export declare const mergeWorkspaceConfig: (
148
+ ...configs: WorkspaceConfigInput[]
149
+ ) => WorkspaceConfig;
92
150
 
93
151
  export {};
@@ -1,2 +1,4 @@
1
1
  export { defineRootConfig } from "./rootConfig/defineRootConfig.mjs";
2
+ export { mergeRootConfig } from "./rootConfig/mergeRootConfig.mjs";
2
3
  export { defineWorkspaceConfig } from "./workspaceConfig/defineWorkspaceConfig.mjs";
4
+ export { mergeWorkspaceConfig } from "./workspaceConfig/mergeWorkspaceConfig.mjs";
@@ -1,6 +1,7 @@
1
1
  export * from "./rootConfigSchema.mjs";
2
2
  export * from "./rootConfig.mjs";
3
3
  export * from "./loadRootConfig.mjs";
4
+ export * from "./mergeRootConfig.mjs";
4
5
  export * from "./errors.mjs";
5
6
 
6
7
  export {};
@@ -0,0 +1,24 @@
1
+ /** Merge two or more root configs left to right, with each subsequent config taking precedence.
2
+ * Any argument may be a factory function receiving the accumulated config up to that point. */ const mergeRootConfig =
3
+ (...configs) =>
4
+ configs.reduce((acc, configOrFactory) => {
5
+ const config =
6
+ typeof configOrFactory === "function"
7
+ ? configOrFactory(acc)
8
+ : configOrFactory;
9
+ const mergedPatternConfigs = [
10
+ ...(acc.workspacePatternConfigs ?? []),
11
+ ...(config.workspacePatternConfigs ?? []),
12
+ ];
13
+ return {
14
+ defaults: {
15
+ ...acc.defaults,
16
+ ...config.defaults,
17
+ },
18
+ ...(mergedPatternConfigs.length > 0 && {
19
+ workspacePatternConfigs: mergedPatternConfigs,
20
+ }),
21
+ };
22
+ }, {});
23
+
24
+ export { mergeRootConfig };
@@ -5,6 +5,7 @@ import {
5
5
  } from "../../runScript/index.mjs";
6
6
  import { getUserEnvVar } from "../userEnvVars/index.mjs";
7
7
  import { executeValidator } from "../util/validateConfig.mjs";
8
+ import { validateWorkspaceConfig } from "../workspaceConfig/workspaceConfig.mjs";
8
9
  import { ROOT_CONFIG_ERRORS } from "./errors.mjs";
9
10
 
10
11
  const rootConfig_validateRootConfig = (config) =>
@@ -17,6 +18,11 @@ const rootConfig_validateRootConfig = (config) =>
17
18
  const createDefaultRootConfig = () => resolveRootConfig({});
18
19
  const resolveRootConfig = (config) => {
19
20
  rootConfig_validateRootConfig(config);
21
+ for (const entry of config.workspacePatternConfigs ?? []) {
22
+ if (typeof entry.config !== "function") {
23
+ validateWorkspaceConfig(entry.config);
24
+ }
25
+ }
20
26
  return {
21
27
  defaults: {
22
28
  parallelMax: determineParallelMax(
@@ -28,6 +34,7 @@ const resolveRootConfig = (config) => {
28
34
  config.defaults?.includeRootWorkspace ??
29
35
  getUserEnvVar("includeRootWorkspaceDefault") === "true",
30
36
  },
37
+ workspacePatternConfigs: config.workspacePatternConfigs ?? [],
31
38
  };
32
39
  };
33
40
 
@@ -17,6 +17,25 @@ const ROOT_CONFIG_JSON_SCHEMA = {
17
17
  },
18
18
  },
19
19
  },
20
+ workspacePatternConfigs: {
21
+ type: "array",
22
+ items: {
23
+ type: "object",
24
+ additionalProperties: false,
25
+ required: ["patterns", "config"],
26
+ properties: {
27
+ patterns: {
28
+ type: "array",
29
+ items: {
30
+ type: "string",
31
+ },
32
+ },
33
+ // config may be a WorkspaceConfig object or a factory function (TS/JS configs only).
34
+ // Object form is validated separately via validateWorkspaceConfig after AJV.
35
+ config: {},
36
+ },
37
+ },
38
+ },
20
39
  },
21
40
  };
22
41
  let _validateSchemaType;
@@ -1,6 +1,7 @@
1
1
  export * from "./workspaceConfigSchema.mjs";
2
2
  export * from "./workspaceConfig.mjs";
3
3
  export * from "./loadWorkspaceConfig.mjs";
4
+ export * from "./mergeWorkspaceConfig.mjs";
4
5
  export * from "./errors.mjs";
5
6
 
6
7
  export {};
@@ -0,0 +1,75 @@
1
+ import { resolveOptionalArray } from "../../internal/core/index.mjs";
2
+
3
+ const uniqueArray = (arr) => [...new Set(arr)];
4
+ const concatPatterns = (a, b) => {
5
+ if (!a?.length && !b?.length) return undefined;
6
+ return uniqueArray([...(a ?? []), ...(b ?? [])]);
7
+ };
8
+ const mergeWorkspaceDependenciesRule = (base, override) => {
9
+ if (!base && !override) return undefined;
10
+ const allowPatterns = concatPatterns(
11
+ base?.allowPatterns,
12
+ override?.allowPatterns,
13
+ );
14
+ const denyPatterns = concatPatterns(
15
+ base?.denyPatterns,
16
+ override?.denyPatterns,
17
+ );
18
+ return {
19
+ ...(allowPatterns && {
20
+ allowPatterns,
21
+ }),
22
+ ...(denyPatterns && {
23
+ denyPatterns,
24
+ }),
25
+ };
26
+ };
27
+ const mergeWorkspaceRules = (base, override) => {
28
+ const workspaceDependencies = mergeWorkspaceDependenciesRule(
29
+ base?.workspaceDependencies,
30
+ override?.workspaceDependencies,
31
+ );
32
+ return {
33
+ ...(workspaceDependencies && {
34
+ workspaceDependencies,
35
+ }),
36
+ };
37
+ };
38
+ const mergeScripts = (base, override) => {
39
+ if (!base && !override) return {};
40
+ if (!base) return override ?? {};
41
+ if (!override) return base;
42
+ const merged = {
43
+ ...base,
44
+ };
45
+ for (const [key, value] of Object.entries(override)) {
46
+ merged[key] = base[key]
47
+ ? {
48
+ ...base[key],
49
+ ...value,
50
+ }
51
+ : value;
52
+ }
53
+ return merged;
54
+ };
55
+ const applyConfig = (acc, config) => ({
56
+ alias: uniqueArray([
57
+ ...resolveOptionalArray(acc.alias ?? []),
58
+ ...resolveOptionalArray(config.alias ?? []),
59
+ ]),
60
+ tags: uniqueArray([...(acc.tags ?? []), ...(config.tags ?? [])]),
61
+ scripts: mergeScripts(acc.scripts, config.scripts),
62
+ rules: mergeWorkspaceRules(acc.rules, config.rules),
63
+ });
64
+ /** Merge two or more workspace configs left to right, with each subsequent config taking precedence.
65
+ * Any argument may be a factory function receiving the accumulated config up to that point. */ const mergeWorkspaceConfig =
66
+ (...configs) =>
67
+ configs.reduce((acc, configOrFactory) => {
68
+ const config =
69
+ typeof configOrFactory === "function"
70
+ ? configOrFactory(acc)
71
+ : configOrFactory;
72
+ return applyConfig(acc, config);
73
+ }, {});
74
+
75
+ export { mergeWorkspaceConfig };
@@ -13,14 +13,6 @@ const workspaceConfig_validateWorkspaceConfig = (config) => {
13
13
  },
14
14
  WORKSPACE_CONFIG_ERRORS.InvalidWorkspaceConfig,
15
15
  );
16
- if (
17
- config.rules?.workspaceDependencies?.allowPatterns &&
18
- config.rules?.workspaceDependencies?.denyPatterns
19
- ) {
20
- throw new WORKSPACE_CONFIG_ERRORS.InvalidWorkspaceConfig(
21
- "Cannot use both allowPatterns and denyPatterns in workspaceDependencies rule",
22
- );
23
- }
24
16
  };
25
17
  const resolveWorkspaceConfig = (config) => {
26
18
  if (Array.isArray(config.aliases)) {
package/src/index.d.ts CHANGED
@@ -28,7 +28,18 @@ export type ParallelMaxValue =
28
28
  | (typeof PARALLEL_MAX_VALUES)[number]
29
29
  | PercentageValue;
30
30
  export type WorkspaceDependenciesRule = {
31
+ /**
32
+ * Workspace patterns that are permitted as dependencies.
33
+ * Only workspaces matching these patterns are allowed.
34
+ * When combined with `denyPatterns`, the deny list further
35
+ * filters this allowed subset.
36
+ */
31
37
  allowPatterns?: string[];
38
+ /**
39
+ * Workspace patterns that are forbidden as dependencies.
40
+ * When combined with `allowPatterns`, this further filters
41
+ * the subset of allowed workspaces.
42
+ */
32
43
  denyPatterns?: string[];
33
44
  };
34
45
  export type WorkspaceRules = {
@@ -74,6 +85,30 @@ export type ResolvedWorkspaceConfig = {
74
85
  scripts: Record<string, ScriptConfig>;
75
86
  rules: WorkspaceRules;
76
87
  };
88
+ /** Static workspace context passed to a {@link WorkspacePatternConfigFactory}.
89
+ * Contains only the immutable, package.json-derived fields — not config-derived fields like aliases or tags. */
90
+ export type RawWorkspace = {
91
+ name: string;
92
+ isRoot: boolean;
93
+ path: string;
94
+ matchPattern: string;
95
+ scripts: string[];
96
+ dependencies: string[];
97
+ dependents: string[];
98
+ };
99
+ /** A factory that returns a {@link WorkspaceConfig} to merge for a matched workspace.
100
+ * Receives the static workspace context and the workspace's accumulated resolved config at that point. */
101
+ export type WorkspacePatternConfigFactory = (
102
+ workspace: RawWorkspace,
103
+ prevConfig: ResolvedWorkspaceConfig,
104
+ ) => WorkspaceConfig;
105
+ /** A single entry in {@link RootConfig.workspacePatternConfigs} */
106
+ export type WorkspacePatternConfigEntry = {
107
+ /** Workspace patterns to match. Supports all workspace pattern specifiers (name, alias, tag, path, not:). */
108
+ patterns: string[];
109
+ /** Config to merge into all matching workspaces. May be a factory receiving the workspace context and accumulated config. */
110
+ config: WorkspaceConfig | WorkspacePatternConfigFactory;
111
+ };
77
112
  export type RootConfig = {
78
113
  defaults?: {
79
114
  /** The maximum number of scripts that can run in parallel. (default: "auto") */
@@ -83,6 +118,14 @@ export type RootConfig = {
83
118
  /** Whether to include the root workspace in the workspaces list by default. (default: false) */
84
119
  includeRootWorkspace?: boolean;
85
120
  };
121
+ /**
122
+ * Workspace configs applied by pattern, in order, merging left to right,
123
+ * using any workspaces' local configs as the starting config.
124
+ * Each entry's config is merged into all workspaces matching its patterns.
125
+ * Pattern matching reflects accumulated aliases and tags from previous entries.
126
+ * Factory functions are only supported in TypeScript/JavaScript config files.
127
+ */
128
+ workspacePatternConfigs?: WorkspacePatternConfigEntry[];
86
129
  };
87
130
  export type ResolvedRootConfig = {
88
131
  defaults: {
@@ -91,6 +134,7 @@ export type ResolvedRootConfig = {
91
134
  /** `undefined` means the value was not set in the input config */
92
135
  includeRootWorkspace: boolean | undefined;
93
136
  };
137
+ workspacePatternConfigs: WorkspacePatternConfigEntry[];
94
138
  };
95
139
  export type OutputStreamName = "stdout" | "stderr";
96
140
  export type ProcessOutputChunk<
@@ -288,7 +332,7 @@ export type CreateFileSystemProjectOptions = {
288
332
  export type InlineScriptOptions = {
289
333
  /** A name to act as a label for the inline script */
290
334
  scriptName?: string;
291
- /** Whether to run the script as an inline command */
335
+ /** Whether to use the system shell or Bun shell */
292
336
  shell?: ShellOption;
293
337
  };
294
338
  /** Arguments for `FileSystemProject.runWorkspaceScript` */
@@ -444,9 +488,23 @@ export declare const createMemoryProject: (
444
488
  export declare const defineRootConfig: (
445
489
  config: RootConfig,
446
490
  ) => ResolvedRootConfig;
491
+ export type RootConfigFactory = (prev: RootConfig) => RootConfig;
492
+ export type RootConfigInput = RootConfig | RootConfigFactory;
493
+ /** Merge two or more root configs left to right, with each subsequent config taking precedence.
494
+ * Any argument may be a factory function receiving the accumulated config up to that point. */
495
+ export declare const mergeRootConfig: (
496
+ ...configs: RootConfigInput[]
497
+ ) => RootConfig;
447
498
  export declare const defineWorkspaceConfig: (
448
499
  config: WorkspaceConfig,
449
500
  ) => ResolvedWorkspaceConfig;
501
+ export type WorkspaceConfigFactory = (prev: WorkspaceConfig) => WorkspaceConfig;
502
+ export type WorkspaceConfigInput = WorkspaceConfig | WorkspaceConfigFactory;
503
+ /** Merge two or more workspace configs left to right, with each subsequent config taking precedence.
504
+ * Any argument may be a factory function receiving the accumulated config up to that point. */
505
+ export declare const mergeWorkspaceConfig: (
506
+ ...configs: WorkspaceConfigInput[]
507
+ ) => WorkspaceConfig;
450
508
  declare const LOG_LEVELS: readonly ["debug", "info", "warn", "error"];
451
509
  export type LogLevel = (typeof LOG_LEVELS)[number];
452
510
  export type LogLevelSetting = LogLevel | "silent";
@@ -199,10 +199,9 @@ project.runScriptAcrossWorkspaces({
199
199
  \`\`\``;
200
200
  const DOC_CONFIG = `## Root config
201
201
 
202
- Optional project config can be placed in \`bw.root.jsonc\`/\`bw.root.json\` in the root directory.
202
+ Optional project config can be placed in \`bw.root.ts\`/\`bw.root.js\`/\`bw.root.jsonc\`/\`bw.root.json\` in the root directory, or in the \`"bw"\` key of \`package.json\`.
203
203
 
204
- Config defaults here take precedence over environment variables that can set defaults.
205
- Explicit arguments to the CLI or API take precedence over all other settings.
204
+ Config defaults here take precedence over environment variables. Explicit CLI arguments or API options take precedence over all other settings.
206
205
 
207
206
  \`\`\`jsonc
208
207
  {
@@ -211,16 +210,33 @@ Explicit arguments to the CLI or API take precedence over all other settings.
211
210
  "shell": "system", // "bun" or "system" (default "bun")
212
211
  "includeRootWorkspace": true, // treat root package.json as a normal workspace
213
212
  },
213
+ "workspacePatternConfigs": [
214
+ // see Workspace Pattern Configs section below
215
+ ],
214
216
  }
215
217
  \`\`\`
216
218
 
219
+ ### mergeRootConfig
220
+
221
+ \`mergeRootConfig\` merges multiple root configs left to right. Later configs take precedence for scalar fields. \`workspacePatternConfigs\` entries are concatenated. Any argument may be a factory function \`(prev: RootConfig) => RootConfig\`.
222
+
223
+ \`\`\`ts
224
+ import { mergeRootConfig } from "bun-workspaces/config";
225
+
226
+ export default mergeRootConfig(
227
+ { defaults: { parallelMax: 4 } },
228
+ { defaults: { shell: "system" } },
229
+ (prevConfig) => ({ defaults: { includeRootWorkspace: true } }),
230
+ );
231
+ \`\`\`
232
+
217
233
  ## Workspace config
218
234
 
219
- Optional config can be placed in \`bw.workspace.jsonc\`/\`bw.workspace.json\` in a workspace directory.
235
+ Optional config can be placed in \`bw.workspace.ts\`/\`bw.workspace.js\`/\`bw.workspace.jsonc\`/\`bw.workspace.json\` in a workspace directory, or in the \`"bw"\` key of \`package.json\`.
220
236
 
221
- Aliases must be unique to each workspace and to not clash with other workspaces' \`package.json\` names.
237
+ Aliases must be unique to each workspace and must not clash with other workspaces' \`package.json\` names.
222
238
 
223
- Tags are strings to group workspaces together that therefore don't need to be unique to each workspace.
239
+ Tags are strings to group workspaces together; they do not need to be unique.
224
240
 
225
241
  \`\`\`jsonc
226
242
  {
@@ -234,10 +250,11 @@ Tags are strings to group workspaces together that therefore don't need to be un
234
250
  },
235
251
  "rules": {
236
252
  "workspaceDependencies": {
237
- // use workspace patterns to allow or deny other workspaces as dependencies
253
+ // allowPatterns: only workspaces matching these patterns are permitted as dependencies
238
254
  "allowPatterns": ["my-allow-pattern-*"],
239
- // or
240
- // "denyPatterns": ["my-deny-pattern-*"],
255
+ // denyPatterns: workspaces matching these patterns are forbidden as dependencies.
256
+ // When combined with allowPatterns, deny filters within the allowed subset.
257
+ "denyPatterns": ["my-deny-pattern-*"],
241
258
  },
242
259
  },
243
260
  }
@@ -245,16 +262,77 @@ Tags are strings to group workspaces together that therefore don't need to be un
245
262
 
246
263
  ### Workspace Dependency Rules
247
264
 
248
- Using the \`rules.workspaceDependencies\` field, you can define rules for which workspaces are allowed to be dependencies,
249
- using either \`allowPatterns\` or \`denyPatterns\`.
265
+ Using the \`rules.workspaceDependencies\` field, you can define rules for which workspaces are allowed to be dependencies, using \`allowPatterns\`, \`denyPatterns\`, or both.
266
+
267
+ \`allowPatterns\` defines the permitted subset of dependencies. \`denyPatterns\` forbids specific dependencies. When both are present, \`denyPatterns\` further filters within the subset permitted by \`allowPatterns\`.
250
268
 
251
269
  Workspace Patterns are used to match workspaces.
252
270
 
253
- You can't use both \`allowPatterns\` and \`denyPatterns\` at the same time, but you can use
271
+ ### mergeWorkspaceConfig
254
272
 
255
- ## TypeScript/JSON Config Files
273
+ \`mergeWorkspaceConfig\` merges multiple workspace configs left to right. Arrays (\`alias\`, \`tags\`, \`allowPatterns\`, \`denyPatterns\`) are concatenated and deduplicated. Scalar fields later wins. \`scripts\` are deep-merged per key. Any argument may be a factory function \`(prev: WorkspaceConfig) => WorkspaceConfig\`.
274
+
275
+ \`\`\`ts
276
+ import { mergeWorkspaceConfig } from "bun-workspaces/config";
277
+
278
+ export default mergeWorkspaceConfig(
279
+ { alias: "a", tags: ["x"] },
280
+ { alias: "b", scripts: { build: { order: 1 } } },
281
+ (prevConfig) => ({ tags: ["y"] }),
282
+ );
283
+ // result: { alias: ["a", "b"], tags: ["x", "y"], scripts: { build: { order: 1 } } }
284
+ \`\`\`
285
+
286
+ ## Workspace Pattern Configs
287
+
288
+ The root config's \`workspacePatternConfigs\` field applies workspace configs to groups of workspaces matched by [workspace patterns](/concepts/workspace-patterns). Entries are applied in order, left to right.
289
+
290
+ Each entry's \`config\` is merged into the accumulated config of all matching workspaces using the same semantics as \`mergeWorkspaceConfig\`. The local workspace config (from \`bw.workspace.*\` or \`package.json\`) is always the starting base.
291
+
292
+ Pattern matching reflects the accumulated state: aliases and tags added by earlier entries are visible to later entries' patterns.
256
293
 
257
- You can use TypeScript/JSON config files to define your workspace configuration.
294
+ \`\`\`ts
295
+ import { defineRootConfig } from "bun-workspaces/config";
296
+
297
+ export default defineRootConfig({
298
+ workspacePatternConfigs: [
299
+ {
300
+ patterns: ["path:packages/apps/**/*"],
301
+ config: { tags: ["app"] },
302
+ },
303
+ {
304
+ // "tag:app" matches because the entry above added it
305
+ patterns: ["tag:app"],
306
+ config: {
307
+ rules: { workspaceDependencies: { allowPatterns: ["tag:lib"] } },
308
+ },
309
+ },
310
+ {
311
+ patterns: ["tag:app"],
312
+ // Factory form: JS/TS only — receives static workspace data and accumulated config
313
+ config: (workspace, prevConfig) => ({
314
+ alias: workspace.name.replace(/^@my-scope\\//, ""),
315
+ }),
316
+ },
317
+ ],
318
+ });
319
+ \`\`\`
320
+
321
+ ### Factory function context (\`RawWorkspace\`)
322
+
323
+ The factory \`(workspace: RawWorkspace, prevConfig: ResolvedWorkspaceConfig) => WorkspaceConfig\` receives:
324
+
325
+ - \`workspace.name\` — package name from package.json
326
+ - \`workspace.isRoot\` — whether this is the root workspace
327
+ - \`workspace.path\` — relative path from project root
328
+ - \`workspace.matchPattern\` — glob from root package.json \`workspaces\` field that matched
329
+ - \`workspace.scripts\` — sorted list of script names from package.json
330
+ - \`workspace.dependencies\` — names of workspace dependencies
331
+ - \`workspace.dependents\` — names of workspaces that depend on this one
332
+
333
+ \`prevConfig\` is the fully resolved workspace config at that point, including the local config and any configs applied by earlier pattern entries. It has \`aliases: string[]\`, \`tags: string[]\`, \`scripts: Record<string, ScriptConfig>\`, \`rules: WorkspaceRules\`.
334
+
335
+ ## TypeScript/JSON Config Files
258
336
 
259
337
  ### TypeScript
260
338
 
@@ -1 +1 @@
1
- "use strict";module.exports = validate11;module.exports.default = validate11;const schema12 = {"type":"object","additionalProperties":false,"properties":{"defaults":{"type":"object","additionalProperties":false,"properties":{"parallelMax":{"type":["number","string"]},"shell":{"type":"string"},"includeRootWorkspace":{"type":"boolean"}}}}};function validate11(data, {instancePath="", parentData, parentDataProperty, rootData=data}={}){let vErrors = null;let errors = 0;if(errors === 0){if(data && typeof data == "object" && !Array.isArray(data)){const _errs1 = errors;for(const key0 in data){if(!(key0 === "defaults")){validate11.errors = [{instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}];return false;break;}}if(_errs1 === errors){if(data.defaults !== undefined){let data0 = data.defaults;const _errs2 = errors;if(errors === _errs2){if(data0 && typeof data0 == "object" && !Array.isArray(data0)){const _errs4 = errors;for(const key1 in data0){if(!(((key1 === "parallelMax") || (key1 === "shell")) || (key1 === "includeRootWorkspace"))){validate11.errors = [{instancePath:instancePath+"/defaults",schemaPath:"#/properties/defaults/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key1},message:"must NOT have additional properties"}];return false;break;}}if(_errs4 === errors){if(data0.parallelMax !== undefined){let data1 = data0.parallelMax;const _errs5 = errors;if((!((typeof data1 == "number") && (isFinite(data1)))) && (typeof data1 !== "string")){validate11.errors = [{instancePath:instancePath+"/defaults/parallelMax",schemaPath:"#/properties/defaults/properties/parallelMax/type",keyword:"type",params:{type: schema12.properties.defaults.properties.parallelMax.type},message:"must be number,string"}];return false;}var valid1 = _errs5 === errors;}else {var valid1 = true;}if(valid1){if(data0.shell !== undefined){const _errs7 = errors;if(typeof data0.shell !== "string"){validate11.errors = [{instancePath:instancePath+"/defaults/shell",schemaPath:"#/properties/defaults/properties/shell/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}var valid1 = _errs7 === errors;}else {var valid1 = true;}if(valid1){if(data0.includeRootWorkspace !== undefined){const _errs9 = errors;if(typeof data0.includeRootWorkspace !== "boolean"){validate11.errors = [{instancePath:instancePath+"/defaults/includeRootWorkspace",schemaPath:"#/properties/defaults/properties/includeRootWorkspace/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}];return false;}var valid1 = _errs9 === errors;}else {var valid1 = true;}}}}}else {validate11.errors = [{instancePath:instancePath+"/defaults",schemaPath:"#/properties/defaults/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}}}}else {validate11.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}validate11.errors = vErrors;return errors === 0;}
1
+ "use strict";module.exports = validate11;module.exports.default = validate11;const schema12 = {"type":"object","additionalProperties":false,"properties":{"defaults":{"type":"object","additionalProperties":false,"properties":{"parallelMax":{"type":["number","string"]},"shell":{"type":"string"},"includeRootWorkspace":{"type":"boolean"}}},"workspacePatternConfigs":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["patterns","config"],"properties":{"patterns":{"type":"array","items":{"type":"string"}},"config":{}}}}}};function validate11(data, {instancePath="", parentData, parentDataProperty, rootData=data}={}){let vErrors = null;let errors = 0;if(errors === 0){if(data && typeof data == "object" && !Array.isArray(data)){const _errs1 = errors;for(const key0 in data){if(!((key0 === "defaults") || (key0 === "workspacePatternConfigs"))){validate11.errors = [{instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}];return false;break;}}if(_errs1 === errors){if(data.defaults !== undefined){let data0 = data.defaults;const _errs2 = errors;if(errors === _errs2){if(data0 && typeof data0 == "object" && !Array.isArray(data0)){const _errs4 = errors;for(const key1 in data0){if(!(((key1 === "parallelMax") || (key1 === "shell")) || (key1 === "includeRootWorkspace"))){validate11.errors = [{instancePath:instancePath+"/defaults",schemaPath:"#/properties/defaults/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key1},message:"must NOT have additional properties"}];return false;break;}}if(_errs4 === errors){if(data0.parallelMax !== undefined){let data1 = data0.parallelMax;const _errs5 = errors;if((!((typeof data1 == "number") && (isFinite(data1)))) && (typeof data1 !== "string")){validate11.errors = [{instancePath:instancePath+"/defaults/parallelMax",schemaPath:"#/properties/defaults/properties/parallelMax/type",keyword:"type",params:{type: schema12.properties.defaults.properties.parallelMax.type},message:"must be number,string"}];return false;}var valid1 = _errs5 === errors;}else {var valid1 = true;}if(valid1){if(data0.shell !== undefined){const _errs7 = errors;if(typeof data0.shell !== "string"){validate11.errors = [{instancePath:instancePath+"/defaults/shell",schemaPath:"#/properties/defaults/properties/shell/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}var valid1 = _errs7 === errors;}else {var valid1 = true;}if(valid1){if(data0.includeRootWorkspace !== undefined){const _errs9 = errors;if(typeof data0.includeRootWorkspace !== "boolean"){validate11.errors = [{instancePath:instancePath+"/defaults/includeRootWorkspace",schemaPath:"#/properties/defaults/properties/includeRootWorkspace/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}];return false;}var valid1 = _errs9 === errors;}else {var valid1 = true;}}}}}else {validate11.errors = [{instancePath:instancePath+"/defaults",schemaPath:"#/properties/defaults/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid0 = _errs2 === errors;}else {var valid0 = true;}if(valid0){if(data.workspacePatternConfigs !== undefined){let data4 = data.workspacePatternConfigs;const _errs11 = errors;if(errors === _errs11){if(Array.isArray(data4)){var valid2 = true;const len0 = data4.length;for(let i0=0; i0<len0; i0++){let data5 = data4[i0];const _errs13 = errors;if(errors === _errs13){if(data5 && typeof data5 == "object" && !Array.isArray(data5)){let missing0;if(((data5.patterns === undefined) && (missing0 = "patterns")) || ((data5.config === undefined) && (missing0 = "config"))){validate11.errors = [{instancePath:instancePath+"/workspacePatternConfigs/" + i0,schemaPath:"#/properties/workspacePatternConfigs/items/required",keyword:"required",params:{missingProperty: missing0},message:"must have required property '"+missing0+"'"}];return false;}else {const _errs15 = errors;for(const key2 in data5){if(!((key2 === "patterns") || (key2 === "config"))){validate11.errors = [{instancePath:instancePath+"/workspacePatternConfigs/" + i0,schemaPath:"#/properties/workspacePatternConfigs/items/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key2},message:"must NOT have additional properties"}];return false;break;}}if(_errs15 === errors){if(data5.patterns !== undefined){let data6 = data5.patterns;const _errs16 = errors;if(errors === _errs16){if(Array.isArray(data6)){var valid4 = true;const len1 = data6.length;for(let i1=0; i1<len1; i1++){const _errs18 = errors;if(typeof data6[i1] !== "string"){validate11.errors = [{instancePath:instancePath+"/workspacePatternConfigs/" + i0+"/patterns/" + i1,schemaPath:"#/properties/workspacePatternConfigs/items/properties/patterns/items/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}var valid4 = _errs18 === errors;if(!valid4){break;}}}else {validate11.errors = [{instancePath:instancePath+"/workspacePatternConfigs/" + i0+"/patterns",schemaPath:"#/properties/workspacePatternConfigs/items/properties/patterns/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}}}}}else {validate11.errors = [{instancePath:instancePath+"/workspacePatternConfigs/" + i0,schemaPath:"#/properties/workspacePatternConfigs/items/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid2 = _errs13 === errors;if(!valid2){break;}}}else {validate11.errors = [{instancePath:instancePath+"/workspacePatternConfigs",schemaPath:"#/properties/workspacePatternConfigs/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}var valid0 = _errs11 === errors;}else {var valid0 = true;}}}}else {validate11.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}validate11.errors = vErrors;return errors === 0;}
@@ -111,6 +111,7 @@ class _FileSystemProject extends ProjectBase {
111
111
  options.includeRootWorkspace ??
112
112
  rootConfig.defaults.includeRootWorkspace ??
113
113
  getUserEnvVar("includeRootWorkspaceDefault") === "true",
114
+ workspacePatternConfigs: rootConfig.workspacePatternConfigs,
114
115
  });
115
116
  this.rootWorkspace = rootWorkspace;
116
117
  this.workspaces = workspaces;
@@ -0,0 +1,40 @@
1
+ // Generated by dts-bundle-generator v9.5.1
2
+
3
+ declare const SCRIPT_RUNTIME_METADATA_CONFIG: {
4
+ readonly projectPath: {
5
+ readonly inlineName: "<projectPath>";
6
+ readonly envVarName: "BW_PROJECT_PATH";
7
+ };
8
+ readonly projectName: {
9
+ readonly inlineName: "<projectName>";
10
+ readonly envVarName: "BW_PROJECT_NAME";
11
+ };
12
+ readonly workspacePath: {
13
+ readonly inlineName: "<workspacePath>";
14
+ readonly envVarName: "BW_WORKSPACE_PATH";
15
+ };
16
+ readonly workspaceRelativePath: {
17
+ readonly inlineName: "<workspaceRelativePath>";
18
+ readonly envVarName: "BW_WORKSPACE_RELATIVE_PATH";
19
+ };
20
+ readonly scriptName: {
21
+ readonly inlineName: "<scriptName>";
22
+ readonly envVarName: "BW_SCRIPT_NAME";
23
+ };
24
+ readonly workspaceName: {
25
+ readonly inlineName: "<workspaceName>";
26
+ readonly envVarName: "BW_WORKSPACE_NAME";
27
+ };
28
+ };
29
+ export type ScriptRuntimeMetadataKey =
30
+ keyof typeof SCRIPT_RUNTIME_METADATA_CONFIG;
31
+ /**
32
+ * This is a utility to run from a workspace's script that was called via `bun-workspaces`.
33
+ *
34
+ * It gets the value of some metadata value about the project, workspace, or script that was invoked.
35
+ */
36
+ export declare const getWorkspaceScriptMetadata: (
37
+ key: ScriptRuntimeMetadataKey,
38
+ ) => string;
39
+
40
+ export {};
@@ -0,0 +1 @@
1
+ export { getWorkspaceScriptMetadata } from "./scriptRuntimeMetadata.mjs";
@@ -1,4 +1,4 @@
1
- import { IS_WINDOWS } from "../internal/core/index.mjs";
1
+ import { BunWorkspacesError, IS_WINDOWS } from "../internal/core/index.mjs";
2
2
  import { getScriptRuntimeMetadataConfig } from "../1108.mjs";
3
3
 
4
4
  const createScriptRuntimeEnvVars = (metadata) => {
@@ -39,5 +39,22 @@ const interpolateScriptRuntimeMetadata = (text, metadata, shell) => {
39
39
  return value;
40
40
  });
41
41
  };
42
+ /**
43
+ * This is a utility to run from a workspace's script that was called via `bun-workspaces`.
44
+ *
45
+ * It gets the value of some metadata value about the project, workspace, or script that was invoked.
46
+ */ const getWorkspaceScriptMetadata = (key) => {
47
+ const { envVarName } = getScriptRuntimeMetadataConfig(key);
48
+ if (!(envVarName in process.env)) {
49
+ throw new BunWorkspacesError(
50
+ `getScriptMetadata() called with key "${key}" but environment variable ${envVarName} is not set. getScriptMetadata() may not have been called in a workspace script running via bun-workspaces.`,
51
+ );
52
+ }
53
+ return process.env[envVarName];
54
+ };
42
55
 
43
- export { createScriptRuntimeEnvVars, interpolateScriptRuntimeMetadata };
56
+ export {
57
+ createScriptRuntimeEnvVars,
58
+ getWorkspaceScriptMetadata,
59
+ interpolateScriptRuntimeMetadata,
60
+ };
@@ -0,0 +1,58 @@
1
+ import {
2
+ mergeWorkspaceConfig,
3
+ resolveWorkspaceConfig,
4
+ } from "../config/index.mjs";
5
+ import { matchWorkspacesByPatterns } from "./workspacePattern.mjs";
6
+
7
+ const resolvedToWorkspaceConfig = ({ aliases, tags, scripts, rules }) => ({
8
+ alias: aliases,
9
+ tags,
10
+ scripts,
11
+ rules,
12
+ });
13
+ const makeContext = (workspace) => ({
14
+ name: workspace.name,
15
+ isRoot: workspace.isRoot,
16
+ path: workspace.path,
17
+ matchPattern: workspace.matchPattern,
18
+ scripts: workspace.scripts,
19
+ dependencies: workspace.dependencies,
20
+ dependents: workspace.dependents,
21
+ });
22
+ const applyWorkspacePatternConfigs = (
23
+ workspaces,
24
+ workspaceMap,
25
+ workspaceAliases,
26
+ patternConfigs,
27
+ ) => {
28
+ for (const entry of patternConfigs) {
29
+ const matched = matchWorkspacesByPatterns(entry.patterns, workspaces);
30
+ for (const workspace of matched) {
31
+ const mapEntry = workspaceMap[workspace.name];
32
+ const prevConfig = mapEntry.config;
33
+ const configToMerge =
34
+ typeof entry.config === "function"
35
+ ? entry.config(makeContext(workspace), prevConfig)
36
+ : entry.config;
37
+ const resolved = resolveWorkspaceConfig(
38
+ mergeWorkspaceConfig(
39
+ resolvedToWorkspaceConfig(prevConfig),
40
+ configToMerge,
41
+ ),
42
+ );
43
+ // Register any new aliases for validation
44
+ const previousAliases = new Set(workspace.aliases);
45
+ for (const alias of resolved.aliases) {
46
+ if (!previousAliases.has(alias)) {
47
+ workspaceAliases[alias] = workspace.name;
48
+ }
49
+ }
50
+ // Update workspace object so subsequent pattern entries see accumulated aliases/tags
51
+ workspace.aliases = resolved.aliases;
52
+ workspace.tags = resolved.tags;
53
+ mapEntry.config = resolved;
54
+ }
55
+ }
56
+ };
57
+
58
+ export { applyWorkspacePatternConfigs };
@@ -32,16 +32,6 @@ const validateWorkspaceDependencyRules = ({ workspaceMap }) => {
32
32
  const depWorkspace = workspaceMap[depName]?.workspace;
33
33
  if (!depWorkspace) continue;
34
34
  const chainStr = chain.join(" -> ");
35
- if (rule.denyPatterns) {
36
- const isDenied =
37
- matchWorkspacesByPatterns(rule.denyPatterns, [depWorkspace]).length >
38
- 0;
39
- if (isDenied) {
40
- violations.push(
41
- `"${workspaceName}" violates workspaceDependencies rule: workspace "${depName}" is denied by denyPatterns (dependency chain: ${chainStr})`,
42
- );
43
- }
44
- }
45
35
  if (rule.allowPatterns) {
46
36
  const isAllowed =
47
37
  matchWorkspacesByPatterns(rule.allowPatterns, [depWorkspace]).length >
@@ -50,6 +40,17 @@ const validateWorkspaceDependencyRules = ({ workspaceMap }) => {
50
40
  violations.push(
51
41
  `"${workspaceName}" violates workspaceDependencies rule: workspace "${depName}" is not permitted by allowPatterns (dependency chain: ${chainStr})`,
52
42
  );
43
+ continue;
44
+ }
45
+ }
46
+ if (rule.denyPatterns) {
47
+ const isDenied =
48
+ matchWorkspacesByPatterns(rule.denyPatterns, [depWorkspace]).length >
49
+ 0;
50
+ if (isDenied) {
51
+ violations.push(
52
+ `"${workspaceName}" violates workspaceDependencies rule: workspace "${depName}" is denied by denyPatterns (dependency chain: ${chainStr})`,
53
+ );
53
54
  }
54
55
  }
55
56
  }
@@ -8,6 +8,7 @@ import {
8
8
  import { BUN_LOCK_ERRORS, readBunLockfile } from "../internal/bun/index.mjs";
9
9
  import { BunWorkspacesError } from "../internal/core/index.mjs";
10
10
  import { logger } from "../internal/logger/logger.mjs";
11
+ import { applyWorkspacePatternConfigs } from "./applyWorkspacePatternConfigs.mjs";
11
12
  import {
12
13
  resolveWorkspaceDependencies,
13
14
  validateWorkspaceDependencyRules,
@@ -65,6 +66,7 @@ const findWorkspaces = ({
65
66
  rootDirectory,
66
67
  workspaceGlobs: _workspaceGlobs,
67
68
  includeRootWorkspace = false,
69
+ workspacePatternConfigs,
68
70
  }) => {
69
71
  rootDirectory = path.resolve(rootDirectory);
70
72
  logger.debug(`Finding workspaces in ${rootDirectory}`);
@@ -169,6 +171,14 @@ const findWorkspaces = ({
169
171
  bunCatalogs,
170
172
  ),
171
173
  );
174
+ if (workspacePatternConfigs?.length) {
175
+ applyWorkspacePatternConfigs(
176
+ workspaces,
177
+ workspaceMap,
178
+ workspaceAliases,
179
+ workspacePatternConfigs,
180
+ );
181
+ }
172
182
  validateWorkspaceDependencyRules({
173
183
  workspaceMap,
174
184
  });