bun-workspaces 1.8.2 → 1.10.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.
Files changed (47) hide show
  1. package/AGENTS.md +537 -0
  2. package/README.md +51 -13
  3. package/package.json +1 -1
  4. package/src/2392.mjs +184 -3
  5. package/src/5166.mjs +1 -0
  6. package/src/8529.mjs +10 -0
  7. package/src/affected/affectedBaseRef.mjs +12 -0
  8. package/src/affected/externalDependencyChanges.mjs +47 -0
  9. package/src/affected/fileAffectedWorkspaces.mjs +152 -54
  10. package/src/affected/gitAffectedFiles.mjs +44 -1
  11. package/src/affected/gitAffectedWorkspaces.mjs +73 -3
  12. package/src/affected/index.mjs +2 -0
  13. package/src/ai/mcp/serverState.mjs +1 -1
  14. package/src/cli/commands/commandHandlerUtils.mjs +12 -7
  15. package/src/cli/commands/commands.mjs +4 -1
  16. package/src/cli/commands/handleSimpleCommands.mjs +2 -2
  17. package/src/cli/commands/listAffected.mjs +184 -0
  18. package/src/cli/commands/runScript/handleRunAffected.mjs +99 -0
  19. package/src/cli/commands/runScript/handleRunScript.mjs +19 -202
  20. package/src/cli/commands/runScript/index.mjs +1 -0
  21. package/src/cli/commands/runScript/scriptRunFlow.mjs +213 -0
  22. package/src/cli/index.d.ts +749 -134
  23. package/src/config/public.d.ts +66 -2
  24. package/src/config/rootConfig/rootConfig.mjs +9 -9
  25. package/src/config/rootConfig/rootConfigSchema.mjs +3 -0
  26. package/src/config/workspaceConfig/mergeWorkspaceConfig.mjs +33 -19
  27. package/src/config/workspaceConfig/workspaceConfig.mjs +3 -0
  28. package/src/config/workspaceConfig/workspaceConfigSchema.mjs +26 -0
  29. package/src/index.d.ts +307 -5
  30. package/src/index.mjs +1 -0
  31. package/src/internal/bun/bunLock.mjs +33 -0
  32. package/src/internal/generated/aiDocs/docs.mjs +169 -9
  33. package/src/internal/generated/ajv/validateRootConfig.mjs +1 -1
  34. package/src/internal/generated/ajv/validateWorkspaceConfig.mjs +1 -1
  35. package/src/project/implementations/fileSystemProject/affectedWorkspaces.mjs +227 -0
  36. package/src/project/implementations/{fileSystemProject.mjs → fileSystemProject/fileSystemProject.mjs} +169 -12
  37. package/src/project/implementations/fileSystemProject/index.mjs +4 -0
  38. package/src/project/implementations/memoryProject.mjs +1 -0
  39. package/src/project/implementations/projectBase.mjs +11 -17
  40. package/src/project/index.mjs +1 -1
  41. package/src/rslib-runtime.mjs +0 -31
  42. package/src/workspaces/applyWorkspacePatternConfigs.mjs +16 -2
  43. package/src/workspaces/dependencyGraph/resolveDependencies.mjs +68 -18
  44. package/src/workspaces/dependencyGraph/validateDependencyRules.mjs +14 -7
  45. package/src/workspaces/findWorkspaces.mjs +3 -0
  46. package/src/workspaces/workspace.mjs +8 -2
  47. package/src/workspaces/workspacePattern.mjs +134 -46
@@ -17,7 +17,7 @@ const getTransitiveDeps = (workspaceName, workspaceMap, chain, visited) => {
17
17
  }
18
18
  return result;
19
19
  };
20
- const validateWorkspaceDependencyRules = ({ workspaceMap }) => {
20
+ const validateWorkspaceDependencyRules = ({ workspaceMap, rootWorkspace }) => {
21
21
  const violations = [];
22
22
  for (const [workspaceName, { config }] of Object.entries(workspaceMap)) {
23
23
  const rule = config.rules?.workspaceDependencies;
@@ -32,10 +32,15 @@ const validateWorkspaceDependencyRules = ({ workspaceMap }) => {
32
32
  const depWorkspace = workspaceMap[depName]?.workspace;
33
33
  if (!depWorkspace) continue;
34
34
  const chainStr = chain.join(" -> ");
35
+ // matchWorkspacesByPatterns can inject the root workspace when an
36
+ // "@root" pattern is present, even if it isn't in the input universe.
37
+ // We're only asking "does the single dep match?" so confirm by name.
35
38
  if (rule.allowPatterns) {
36
- const isAllowed =
37
- matchWorkspacesByPatterns(rule.allowPatterns, [depWorkspace]).length >
38
- 0;
39
+ const isAllowed = matchWorkspacesByPatterns(
40
+ rule.allowPatterns,
41
+ [depWorkspace],
42
+ rootWorkspace,
43
+ ).some((matched) => matched.name === depWorkspace.name);
39
44
  if (!isAllowed) {
40
45
  violations.push(
41
46
  `"${workspaceName}" violates workspaceDependencies rule: workspace "${depName}" is not permitted by allowPatterns (dependency chain: ${chainStr})`,
@@ -44,9 +49,11 @@ const validateWorkspaceDependencyRules = ({ workspaceMap }) => {
44
49
  }
45
50
  }
46
51
  if (rule.denyPatterns) {
47
- const isDenied =
48
- matchWorkspacesByPatterns(rule.denyPatterns, [depWorkspace]).length >
49
- 0;
52
+ const isDenied = matchWorkspacesByPatterns(
53
+ rule.denyPatterns,
54
+ [depWorkspace],
55
+ rootWorkspace,
56
+ ).some((matched) => matched.name === depWorkspace.name);
50
57
  if (isDenied) {
51
58
  violations.push(
52
59
  `"${workspaceName}" violates workspaceDependencies rule: workspace "${depName}" is denied by denyPatterns (dependency chain: ${chainStr})`,
@@ -144,6 +144,7 @@ const findWorkspaces = ({
144
144
  tags: workspaceConfig?.tags ?? [],
145
145
  dependencies: [],
146
146
  dependents: [],
147
+ externalDependencies: [],
147
148
  };
148
149
  if (workspace.isRoot) {
149
150
  logger.debug(`Found root workspace: ${workspace.name}`);
@@ -177,10 +178,12 @@ const findWorkspaces = ({
177
178
  workspaceMap,
178
179
  workspaceAliases,
179
180
  workspacePatternConfigs,
181
+ rootWorkspace,
180
182
  );
181
183
  }
182
184
  validateWorkspaceDependencyRules({
183
185
  workspaceMap,
186
+ rootWorkspace,
184
187
  });
185
188
  validateWorkspaceAliases(workspaces, workspaceAliases, rootWorkspace.name);
186
189
  logger.debug(
@@ -1,3 +1,9 @@
1
- /** Metadata about a nested package within a Bun monorepo */
1
+ /** Catalog reference info attached to an `ExternalDependency` that uses a `catalog:` ref */ /** The four `package.json` dependency maps a dep can be declared in */ const EXTERNAL_DEPENDENCY_SOURCES =
2
+ [
3
+ "dependencies",
4
+ "devDependencies",
5
+ "peerDependencies",
6
+ "optionalDependencies",
7
+ ];
2
8
 
3
- export {};
9
+ export { EXTERNAL_DEPENDENCY_SOURCES };
@@ -12,95 +12,183 @@ const WORKSPACE_PATTERN_NEGATION_PREFIXES = [
12
12
  WORKSPACE_PATTERN_NEGATION_SHORT_PREFIX,
13
13
  ];
14
14
  const WORKSPACE_PATTERN_SEPARATOR = ":";
15
+ const WORKSPACE_PATTERN_REGEX_PREFIX = "re:";
16
+ const validateRegexSource = (source, originalPattern) => {
17
+ try {
18
+ new RegExp(source);
19
+ } catch (cause) {
20
+ throw new WORKSPACE_PATTERN_ERRORS.InvalidWorkspacePattern(
21
+ `Invalid regex in workspace pattern "${originalPattern}": ${cause.message}`,
22
+ );
23
+ }
24
+ };
15
25
  const parseWorkspacePattern = (pattern) => {
16
26
  const negationPrefix = WORKSPACE_PATTERN_NEGATION_PREFIXES.find((prefix) =>
17
27
  pattern.startsWith(prefix),
18
28
  );
19
29
  const isNegated = !!negationPrefix;
20
- const patternValue = negationPrefix
30
+ const afterNegation = negationPrefix
21
31
  ? pattern.slice(negationPrefix.length)
22
32
  : pattern;
33
+ // The "@root" selector resolves to the project's root workspace. Recognized
34
+ // immediately after optional negation, so "not:@root" / "!@root" also work.
35
+ // A target-scoped value of "@root" (e.g. "name:@root") is treated as a literal,
36
+ // not a root selector.
37
+ if (afterNegation === /* inlined export .ROOT_WORKSPACE_SELECTOR */ "@root") {
38
+ return {
39
+ target: "default",
40
+ value: /* inlined export .ROOT_WORKSPACE_SELECTOR */ "@root",
41
+ isNegated,
42
+ isRegex: false,
43
+ isRootSelector: true,
44
+ };
45
+ }
46
+ // "re:" before any target consumes the rest as a regex against the default target.
47
+ // e.g. "re:path:foo" → default-target regex over literal source "path:foo".
48
+ if (afterNegation.startsWith(WORKSPACE_PATTERN_REGEX_PREFIX)) {
49
+ const value = afterNegation.slice(WORKSPACE_PATTERN_REGEX_PREFIX.length);
50
+ validateRegexSource(value, pattern);
51
+ return {
52
+ target: "default",
53
+ value,
54
+ isNegated,
55
+ isRegex: true,
56
+ isRootSelector: false,
57
+ };
58
+ }
23
59
  const target = TARGETS.find((target) =>
24
- patternValue.startsWith(target + WORKSPACE_PATTERN_SEPARATOR),
60
+ afterNegation.startsWith(target + WORKSPACE_PATTERN_SEPARATOR),
25
61
  );
26
62
  if (!target) {
27
63
  return {
28
64
  target: "default",
29
- value: patternValue,
65
+ value: afterNegation,
30
66
  isNegated,
67
+ isRegex: false,
68
+ isRootSelector: false,
31
69
  };
32
70
  }
33
- const value = patternValue.slice(
71
+ const afterTarget = afterNegation.slice(
34
72
  target.length + WORKSPACE_PATTERN_SEPARATOR.length,
35
73
  );
74
+ if (afterTarget.startsWith(WORKSPACE_PATTERN_REGEX_PREFIX)) {
75
+ const value = afterTarget.slice(WORKSPACE_PATTERN_REGEX_PREFIX.length);
76
+ validateRegexSource(value, pattern);
77
+ return {
78
+ target,
79
+ value,
80
+ isNegated,
81
+ isRegex: true,
82
+ isRootSelector: false,
83
+ };
84
+ }
36
85
  return {
37
86
  target,
38
- value,
87
+ value: afterTarget,
39
88
  isNegated,
89
+ isRegex: false,
90
+ isRootSelector: false,
40
91
  };
41
92
  };
42
- const stringifyWorkspacePattern = (pattern) => {
43
- return `${pattern.target}${WORKSPACE_PATTERN_SEPARATOR}${pattern.value}`;
44
- };
45
93
  const PATTERN_TARGET_HANDLERS = {
46
- default: (pattern, workspaces, wildcardRegex) => {
47
- return workspaces.filter((workspace) => {
48
- return (
49
- (pattern.value.includes("*")
50
- ? wildcardRegex.test(workspace.name)
51
- : workspace.name === pattern.value) ||
52
- workspace.aliases.some((alias) =>
53
- pattern.value.includes("*")
54
- ? wildcardRegex.test(alias)
55
- : alias === pattern.value,
56
- )
94
+ default: (pattern, workspaces) => {
95
+ // Plain string at the default target matches name OR alias. Wildcard and
96
+ // regex forms intentionally narrow to name only to avoid ambiguity — use
97
+ // an explicit "alias:" prefix to match aliases by wildcard/regex.
98
+ if (pattern.isRegex) {
99
+ const regex = new RegExp(pattern.value);
100
+ return workspaces.filter((workspace) => regex.test(workspace.name));
101
+ }
102
+ if (pattern.value.includes("*")) {
103
+ const wildcardRegex = createWildcardRegex(pattern.value);
104
+ return workspaces.filter((workspace) =>
105
+ wildcardRegex.test(workspace.name),
57
106
  );
58
- });
107
+ }
108
+ return workspaces.filter(
109
+ (workspace) =>
110
+ workspace.name === pattern.value ||
111
+ workspace.aliases.includes(pattern.value),
112
+ );
59
113
  },
60
- name: (pattern, workspaces, wildcardRegex) => {
61
- return workspaces.filter((workspace) => {
62
- return pattern.value.includes("*")
63
- ? wildcardRegex.test(workspace.name)
64
- : workspace.name === pattern.value;
65
- });
114
+ name: (pattern, workspaces) => {
115
+ if (pattern.isRegex) {
116
+ const regex = new RegExp(pattern.value);
117
+ return workspaces.filter((workspace) => regex.test(workspace.name));
118
+ }
119
+ if (pattern.value.includes("*")) {
120
+ const wildcardRegex = createWildcardRegex(pattern.value);
121
+ return workspaces.filter((workspace) =>
122
+ wildcardRegex.test(workspace.name),
123
+ );
124
+ }
125
+ return workspaces.filter((workspace) => workspace.name === pattern.value);
66
126
  },
67
- alias: (pattern, workspaces, wildcardRegex) => {
68
- return workspaces.filter((workspace) => {
69
- return pattern.value.includes("*")
70
- ? workspace.aliases.some((alias) => wildcardRegex.test(alias))
71
- : workspace.aliases.includes(pattern.value);
72
- });
127
+ alias: (pattern, workspaces) => {
128
+ if (pattern.isRegex) {
129
+ const regex = new RegExp(pattern.value);
130
+ return workspaces.filter((workspace) =>
131
+ workspace.aliases.some((alias) => regex.test(alias)),
132
+ );
133
+ }
134
+ if (pattern.value.includes("*")) {
135
+ const wildcardRegex = createWildcardRegex(pattern.value);
136
+ return workspaces.filter((workspace) =>
137
+ workspace.aliases.some((alias) => wildcardRegex.test(alias)),
138
+ );
139
+ }
140
+ return workspaces.filter((workspace) =>
141
+ workspace.aliases.includes(pattern.value),
142
+ );
73
143
  },
74
144
  path: (pattern, workspaces) => {
145
+ if (pattern.isRegex) {
146
+ const regex = new RegExp(pattern.value);
147
+ // Normalize backslashes so regex sources stay portable: a single
148
+ // forward-slash-based regex works on both Windows and POSIX paths.
149
+ return workspaces.filter((workspace) =>
150
+ regex.test(workspace.path.replaceAll("\\", "/")),
151
+ );
152
+ }
75
153
  return workspaces.filter((workspace) =>
76
154
  new bun.Glob(pattern.value.replace(/\/+$/, "")).match(workspace.path),
77
155
  );
78
156
  },
79
- tag: (pattern, workspaces, wildcardRegex) => {
157
+ tag: (pattern, workspaces) => {
158
+ if (pattern.isRegex) {
159
+ const regex = new RegExp(pattern.value);
160
+ return workspaces.filter((workspace) =>
161
+ workspace.tags.some((tag) => regex.test(tag)),
162
+ );
163
+ }
164
+ if (pattern.value.includes("*")) {
165
+ const wildcardRegex = createWildcardRegex(pattern.value);
166
+ return workspaces.filter((workspace) =>
167
+ workspace.tags.some((tag) => wildcardRegex.test(tag)),
168
+ );
169
+ }
80
170
  return workspaces.filter((workspace) =>
81
- pattern.value.includes("*")
82
- ? workspace.tags.some((tag) => wildcardRegex.test(tag))
83
- : workspace.tags.includes(pattern.value),
171
+ workspace.tags.includes(pattern.value),
84
172
  );
85
173
  },
86
174
  };
87
- const matchWorkspacesByPattern = (pattern, workspaces) =>
88
- PATTERN_TARGET_HANDLERS[pattern.target](
89
- pattern,
90
- workspaces,
91
- createWildcardRegex(pattern.value),
92
- );
93
- const matchWorkspacesByPatterns = (patterns, workspaces) => {
175
+ const matchWorkspacesByPattern = (pattern, workspaces, rootWorkspace) => {
176
+ if (pattern.isRootSelector) {
177
+ return rootWorkspace ? [rootWorkspace] : [];
178
+ }
179
+ return PATTERN_TARGET_HANDLERS[pattern.target](pattern, workspaces);
180
+ };
181
+ const matchWorkspacesByPatterns = (patterns, workspaces, rootWorkspace) => {
94
182
  const parsedPatterns = patterns.map(parseWorkspacePattern);
95
183
  const excludePatterns = parsedPatterns.filter((pattern) => pattern.isNegated);
96
184
  const includePatterns = parsedPatterns.filter(
97
185
  (pattern) => !pattern.isNegated,
98
186
  );
99
187
  const excludeWorkspaces = excludePatterns.flatMap((pattern) =>
100
- matchWorkspacesByPattern(pattern, workspaces),
188
+ matchWorkspacesByPattern(pattern, workspaces, rootWorkspace),
101
189
  );
102
190
  const includeWorkspaces = includePatterns.flatMap((pattern) =>
103
- matchWorkspacesByPattern(pattern, workspaces),
191
+ matchWorkspacesByPattern(pattern, workspaces, rootWorkspace),
104
192
  );
105
193
  return includeWorkspaces.filter(
106
194
  (workspace, index, arr) =>
@@ -118,8 +206,8 @@ export {
118
206
  WORKSPACE_PATTERN_NEGATION_PREFIX,
119
207
  WORKSPACE_PATTERN_NEGATION_PREFIXES,
120
208
  WORKSPACE_PATTERN_NEGATION_SHORT_PREFIX,
209
+ WORKSPACE_PATTERN_REGEX_PREFIX,
121
210
  WORKSPACE_PATTERN_SEPARATOR,
122
211
  matchWorkspacesByPatterns,
123
212
  parseWorkspacePattern,
124
- stringifyWorkspacePattern,
125
213
  };