bun-workspaces 1.9.0 → 1.11.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 (40) hide show
  1. package/AGENTS.md +537 -0
  2. package/README.md +4 -2
  3. package/package.json +1 -1
  4. package/src/2392.mjs +10 -1
  5. package/src/5166.mjs +1 -0
  6. package/src/affected/fileAffectedWorkspaces.mjs +7 -1
  7. package/src/affected/gitAffectedFiles.mjs +26 -1
  8. package/src/ai/mcp/bwMcpServer.mjs +5 -1
  9. package/src/ai/mcp/serverState.mjs +10 -1
  10. package/src/ai/mcp/tools.mjs +1 -1
  11. package/src/cli/commands/commandHandlerUtils.mjs +11 -10
  12. package/src/cli/commands/commands.mjs +1 -1
  13. package/src/cli/commands/handleSimpleCommands.mjs +7 -6
  14. package/src/cli/commands/listAffected.mjs +17 -13
  15. package/src/cli/commands/mcp.mjs +11 -1
  16. package/src/cli/commands/runScript/output/renderGroupedOutput.mjs +3 -2
  17. package/src/cli/commands/runScript/output/renderPlainOutput.mjs +4 -1
  18. package/src/cli/commands/runScript/scriptRunFlow.mjs +8 -3
  19. package/src/cli/createCli.mjs +8 -2
  20. package/src/cli/globalOptions/globalOptions.mjs +5 -4
  21. package/src/cli/index.d.ts +35 -2
  22. package/src/config/rootConfig/loadRootConfig.mjs +2 -1
  23. package/src/config/rootConfig/rootConfig.mjs +5 -9
  24. package/src/config/userEnvVars/userEnvVars.mjs +12 -1
  25. package/src/config/util/loadConfig.mjs +23 -2
  26. package/src/config/workspaceConfig/loadWorkspaceConfig.mjs +2 -1
  27. package/src/index.d.ts +11 -0
  28. package/src/internal/core/language/string/index.mjs +1 -0
  29. package/src/internal/core/language/string/sanitizeOutput.mjs +15 -0
  30. package/src/internal/core/runtime/tempFile.mjs +20 -2
  31. package/src/internal/generated/aiDocs/docs.mjs +19 -8
  32. package/src/project/implementations/fileSystemProject/affectedWorkspaces.mjs +2 -0
  33. package/src/project/implementations/fileSystemProject/fileSystemProject.mjs +39 -14
  34. package/src/project/implementations/projectBase.mjs +11 -17
  35. package/src/runScript/scriptExecution.mjs +1 -1
  36. package/src/runScript/workspaceScriptMetadata.mjs +24 -1
  37. package/src/workspaces/applyWorkspacePatternConfigs.mjs +6 -1
  38. package/src/workspaces/dependencyGraph/validateDependencyRules.mjs +14 -7
  39. package/src/workspaces/findWorkspaces.mjs +4 -0
  40. package/src/workspaces/workspacePattern.mjs +134 -46
@@ -5,13 +5,14 @@ import {
5
5
  } from "./workspaceConfig.mjs";
6
6
  import { WORKSPACE_CONFIG_FILE_NAME } from "../../8529.mjs";
7
7
 
8
- const loadWorkspaceConfig = (workspacePath) => {
8
+ const loadWorkspaceConfig = (workspacePath, loadOptions = {}) => {
9
9
  const config = loadConfig(
10
10
  "workspace",
11
11
  workspacePath,
12
12
  WORKSPACE_CONFIG_FILE_NAME,
13
13
  /* inlined export .WORKSPACE_CONFIG_PACKAGE_JSON_KEY */ "bw",
14
14
  (content) => resolveWorkspaceConfig(content),
15
+ loadOptions,
15
16
  );
16
17
  return config ?? createDefaultWorkspaceConfig();
17
18
  };
package/src/index.d.ts CHANGED
@@ -600,6 +600,17 @@ export type CreateFileSystemProjectOptions = {
600
600
  name?: string;
601
601
  /** Whether to include the root workspace as a normal workspace. This overrides any config or env var settings. */
602
602
  includeRootWorkspace?: boolean;
603
+ /**
604
+ * When true, skip discovery of `.ts`/`.js` config files (`bw.root.{ts,js}`,
605
+ * `bw.workspace.{ts,js}`) so no executable code is loaded from the project,
606
+ * for untrusted contexts.
607
+ *
608
+ * `.jsonc`/`.json` configs and the `package.json` `bw` key still resolve.
609
+ *
610
+ * When omitted, the `BW_DISABLE_EXECUTABLE_CONFIGS_DEFAULT` user env var is
611
+ * consulted (`"true"` or `"false"`). If neither is set, defaults to false.
612
+ */
613
+ disableExecutableConfigs?: boolean;
603
614
  };
604
615
  export type InlineScriptOptions = {
605
616
  /** A name to act as a label for the inline script */
@@ -1,3 +1,4 @@
1
1
  export * from "./id.mjs";
2
+ export * from "./sanitizeOutput.mjs";
2
3
 
3
4
  export {};
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Strip ANSI escape sequences and other terminal-disruptive control
3
+ * characters from a string before rendering it. Use this for values
4
+ * sourced from package.json, config files, or other untrusted inputs to
5
+ * prevent terminal-escape injection in bw's CLI output (e.g., a workspace
6
+ * name containing `\x1b[2J` clearing the user's screen during `bw info`).
7
+ *
8
+ * Preserves `\n` and `\t`. Strips ANSI sequences plus C0/C1 controls
9
+ * other than newline and tab.
10
+ */ // eslint-disable-next-line no-control-regex
11
+ const DISRUPTIVE_CONTROLS_REGEX = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/g;
12
+ const sanitizeOutput = (value) =>
13
+ Bun.stripANSI(value).replace(DISRUPTIVE_CONTROLS_REGEX, "");
14
+
15
+ export { sanitizeOutput };
@@ -7,7 +7,21 @@ import { BUN_WORKSPACES_VERSION } from "../../version.mjs";
7
7
  import { createShortId } from "../language/string/id.mjs";
8
8
  import { runOnExit } from "./onExit.mjs";
9
9
 
10
- const getTempBasePackageDir = () => path.join(os.tmpdir(), "bun-workspaces");
10
+ /**
11
+ * Per-user suffix on the temp base dir prevents a different local user from
12
+ * pre-creating or symlink-squatting `/tmp/bun-workspaces` to steer our writes.
13
+ * On platforms without a numeric uid (Windows), `os.tmpdir()` is already
14
+ * per-user so the suffix becomes inert.
15
+ */ const getUserSuffix = () => {
16
+ try {
17
+ const { uid } = os.userInfo();
18
+ return uid >= 0 ? `-${uid}` : "";
19
+ } catch {
20
+ return "";
21
+ }
22
+ };
23
+ const getTempBasePackageDir = () =>
24
+ path.join(os.tmpdir(), `bun-workspaces${getUserSuffix()}`);
11
25
  const getTempParentDir = () =>
12
26
  path.join(getTempBasePackageDir(), BUN_WORKSPACES_VERSION);
13
27
  class TempDir {
@@ -18,10 +32,14 @@ class TempDir {
18
32
  }
19
33
  initialize(clean = false) {
20
34
  if (fs.existsSync(this.dir)) return;
35
+ // Pass mode at creation time so the dir is never briefly readable by
36
+ // other local users between mkdir and a subsequent chmod (closes the
37
+ // TOCTOU window where another process could enter the dir before the
38
+ // mode is tightened).
21
39
  fs.mkdirSync(this.dir, {
22
40
  recursive: true,
41
+ mode: 448,
23
42
  });
24
- fs.chmodSync(this.dir, 448);
25
43
  if (clean) {
26
44
  for (const dir of fs.readdirSync(path.resolve(getTempBasePackageDir()))) {
27
45
  if (dir !== BUN_WORKSPACES_VERSION) {
@@ -1,4 +1,4 @@
1
- // This file is generated by scripts/generateMcpDocs.ts. Do not edit manually.
1
+ // This file is generated by scripts/createPublicAgentDocs.ts. Do not edit manually.
2
2
  const DOC_OVERVIEW = `## Project Overview
3
3
 
4
4
  bun-workspaces is a CLI and TypeScript API to help manage Bun monorepos. It reads \`bun.lock\` to find all workspaces in the project. It is referred to as "bw" for short, which is also the recommended CLI alias. The overall goal is a monorepo tool that is more lightweight than others, with still powerful comparable features, requiring no special config to get started, only a standard Bun repo using workspaces.
@@ -9,12 +9,15 @@ Three main domain terms to know:
9
9
  - Workspace: a nested package within a project. The root package.json can count as a workspace as well, but by default, only nested packages are considered workspaces.
10
10
  - Script: an entry in the \`scripts\` field of a workspace's \`package.json\` file. bw can also run one-off commands known as "inline scripts," which can use the Bun shell or system shell (\`sh -c\` or \`cmd /d /s /c\` for windows).
11
11
 
12
- bw also supports **affected workspace** detection: given a set of changed files (from a git diff or an explicit list), it determines which workspaces are meaningfully changed. This drives \`bw list-affected\`/\`bw run-affected\` for orchestrating builds, tests, etc. across only the workspaces that need them.`;
12
+ bw also supports **affected workspace** detection: given a set of changed files (from a git diff or an explicit list), it determines which workspaces are meaningfully changed. This drives \`bw list-affected\`/\`bw run-affected\` for orchestrating builds, tests, etc. across only the workspaces that need them.
13
+ `;
13
14
  const DOC_CONCEPTS = `## Concepts
14
15
 
15
16
  ### Workspace patterns
16
17
 
17
- Many features accept a list of workspace patterns to match a subset of workspaces.
18
+ Many features accept a list of workspace patterns to match a subset of workspaces:
19
+
20
+ \`[not:][(name|alias|path|tag):][re:]<value>\`
18
21
 
19
22
  By default, a pattern matches the workspace name or alias: \`my-workspace-name\` or \`my-alias-name\`. Aliases are defined in config explained below.
20
23
 
@@ -24,8 +27,12 @@ Patterns can include a wildcard to match only by workspace name: \`my-workspace-
24
27
  - Path pattern specifier (supports glob): \`path:packages/**/*\`.
25
28
  - Name pattern specifier: \`name:my-workspace-*\`.
26
29
  - Tag pattern specifier: \`tag:my-tag\`.
27
- - Special root workspace selector: \`@root\`.
28
30
  - Any pattern can start with \`not:\` to negate the pattern. (e.g. "not:my-workspace-name", "not:tag:my-tag-\\*") This excludes workspaces that match any other present patterns from a result.
31
+ - Regex pattern modifier can be applied before the pattern value: \`re:\` (e.g. "re:^my-workspace-.+" or "not:alias:re:^my-alias-.+")
32
+
33
+ #### Special selectors
34
+
35
+ - Special root workspace selector: \`@root\`. This is a reference to the root workspace, whether it's included in a Project's workspace list or not.
29
36
 
30
37
  ### Workspace Script Metadata
31
38
 
@@ -82,7 +89,8 @@ There are two diff sources:
82
89
  - **git** (default): diff \`HEAD\` against the configured base ref (default \`main\`, configurable via \`affectedBaseRef\` in the root config or \`BW_AFFECTED_BASE_REF_DEFAULT\` env var). Uncommitted changes (staged, unstaged, untracked) are included by default. Gitignored files never participate.
83
90
  - **fileList**: pass changed files explicitly (paths, dirs, or globs) — bypasses git entirely.
84
91
 
85
- Use \`--explain\` for a per-workspace summary of changed inputs and dep cascade reasons, and \`--explain --detailed\` for full per-file/edge breakdowns including the affected-dep chain.`;
92
+ Use \`--explain\` for a per-workspace summary of changed inputs and dep cascade reasons, and \`--explain --detailed\` for full per-file/edge breakdowns including the affected-dep chain.
93
+ `;
86
94
  const DOC_CLI = `### CLI examples:
87
95
 
88
96
  \`\`\`bash
@@ -208,7 +216,8 @@ bw --no-include-root ls # override config/env var setting
208
216
  # Log level (debug|info|warn|error|silent, default info)
209
217
  bw --log-level=silent ls
210
218
  bw -l silent ls
211
- \`\`\``;
219
+ \`\`\`
220
+ `;
212
221
  const DOC_API = `### API examples:
213
222
 
214
223
  The API is held in close parity with the CLI. It is developed first so that the CLI is a thin wrapper around the API.
@@ -337,7 +346,8 @@ project.runAffectedWorkspaceScript({
337
346
  },
338
347
  ],
339
348
  }
340
- \`\`\``;
349
+ \`\`\`
350
+ `;
341
351
  const DOC_CONFIG = `## Root config
342
352
 
343
353
  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\`.
@@ -522,6 +532,7 @@ export default defineRootConfig({
522
532
  parallelMax: 5,
523
533
  },
524
534
  });
525
- \`\`\``;
535
+ \`\`\`
536
+ `;
526
537
 
527
538
  export { DOC_API, DOC_CLI, DOC_CONCEPTS, DOC_CONFIG, DOC_OVERVIEW };
@@ -167,6 +167,7 @@ const determineAffectedWorkspaces = async (project, options) => {
167
167
  workspacesOptions: {
168
168
  workspaceInputs,
169
169
  workspaces: project.workspaces,
170
+ rootWorkspace: project.rootWorkspace,
170
171
  ignoreWorkspaceDependencies,
171
172
  ignoreExternalDependencies,
172
173
  },
@@ -209,6 +210,7 @@ const determineAffectedWorkspaces = async (project, options) => {
209
210
  rootDirectory: project.rootDirectory,
210
211
  workspaceInputs,
211
212
  changedFilePaths: expandedChangedFilePaths,
213
+ rootWorkspace: project.rootWorkspace,
212
214
  externalDepChangesByWorkspace,
213
215
  ignoreWorkspaceDependencies,
214
216
  });
@@ -1,7 +1,10 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
3
  import { loadRootConfig } from "../../../config/index.mjs";
4
- import { getUserEnvVar } from "../../../config/userEnvVars/index.mjs";
4
+ import {
5
+ getUserBoolEnvVar,
6
+ getUserEnvVar,
7
+ } from "../../../config/userEnvVars/index.mjs";
5
8
  import { parse, quote } from "../../../internal/bundledDeps/shellQuote.mjs";
6
9
  import {
7
10
  DEFAULT_TEMP_DIR,
@@ -70,17 +73,22 @@ const serializeArgs = (args, metadata, shell) => {
70
73
  return args
71
74
  .map((arg) =>
72
75
  quoteArg(
73
- interpolateWorkspaceScriptMetadata(arg, metadata, shell),
76
+ interpolateWorkspaceScriptMetadata({
77
+ text: arg,
78
+ metadata,
79
+ shell,
80
+ }),
74
81
  shell,
75
82
  ),
76
83
  )
77
84
  .join(" ");
78
85
  }
79
- const interpolated = interpolateWorkspaceScriptMetadata(
80
- args,
86
+ const interpolated = interpolateWorkspaceScriptMetadata({
87
+ text: args,
81
88
  metadata,
82
89
  shell,
83
- );
90
+ quoteValues: true,
91
+ });
84
92
  // Escape backslashes in interpolated values before POSIX parse on Windows,
85
93
  // so that path separators survive parse's escape processing (\\→\)
86
94
  const parseInput =
@@ -128,6 +136,11 @@ class _FileSystemProject extends ProjectBase {
128
136
  typeofName: "boolean",
129
137
  optional: true,
130
138
  },
139
+ "disableExecutableConfigs option": {
140
+ value: options.disableExecutableConfigs,
141
+ typeofName: "boolean",
142
+ optional: true,
143
+ },
131
144
  },
132
145
  {
133
146
  throw: true,
@@ -141,7 +154,16 @@ class _FileSystemProject extends ProjectBase {
141
154
  process.cwd(),
142
155
  expandHomePath(options.rootDirectory ?? ""),
143
156
  );
144
- const rootConfig = loadRootConfig(this.rootDirectory);
157
+ // Root config can't supply a default for this — the config file itself
158
+ // is what we're deciding whether to evaluate. Precedence is therefore
159
+ // option > BW_DISABLE_EXECUTABLE_CONFIGS_DEFAULT env var > false.
160
+ const loadConfigOptions = {
161
+ disableExecutableConfigs:
162
+ options.disableExecutableConfigs ??
163
+ getUserBoolEnvVar("disableExecutableConfigsDefault") ??
164
+ false,
165
+ };
166
+ const rootConfig = loadRootConfig(this.rootDirectory, loadConfigOptions);
145
167
  const { workspaces, workspaceMap, rootWorkspace } = findWorkspaces({
146
168
  rootDirectory: this.rootDirectory,
147
169
  includeRootWorkspace:
@@ -149,6 +171,7 @@ class _FileSystemProject extends ProjectBase {
149
171
  rootConfig.defaults.includeRootWorkspace ??
150
172
  getUserEnvVar("includeRootWorkspaceDefault") === "true",
151
173
  workspacePatternConfigs: rootConfig.workspacePatternConfigs,
174
+ loadConfigOptions,
152
175
  });
153
176
  this.rootWorkspace = rootWorkspace;
154
177
  this.workspaces = workspaces;
@@ -262,11 +285,12 @@ class _FileSystemProject extends ProjectBase {
262
285
  };
263
286
  const args = serializeArgs(options.args, workspaceScriptMetadata, shell);
264
287
  const script = options.inline
265
- ? interpolateWorkspaceScriptMetadata(
266
- options.script,
267
- workspaceScriptMetadata,
288
+ ? interpolateWorkspaceScriptMetadata({
289
+ text: options.script,
290
+ metadata: workspaceScriptMetadata,
268
291
  shell,
269
- ) + (args ? " " + args : "")
292
+ quoteValues: true,
293
+ }) + (args ? " " + args : "")
270
294
  : options.script;
271
295
  if (!options.inline && checkIsRecursiveScript(workspace.name, script)) {
272
296
  throw new PROJECT_ERRORS.RecursiveWorkspaceScript(
@@ -473,11 +497,12 @@ class _FileSystemProject extends ProjectBase {
473
497
  shell,
474
498
  );
475
499
  const script = options.inline
476
- ? interpolateWorkspaceScriptMetadata(
477
- options.script,
478
- workspaceScriptMetadata,
500
+ ? interpolateWorkspaceScriptMetadata({
501
+ text: options.script,
502
+ metadata: workspaceScriptMetadata,
479
503
  shell,
480
- ) + (args ? " " + args : "")
504
+ quoteValues: true,
505
+ }) + (args ? " " + args : "")
481
506
  : options.script;
482
507
  const scriptCommand = options.inline
483
508
  ? {
@@ -130,24 +130,18 @@ class ProjectBase {
130
130
  return this.workspaces.filter((workspace) => workspace.tags.includes(tag));
131
131
  }
132
132
  findWorkspacesByPattern(...workspacePatterns) {
133
- const workspaces = [];
134
- if (
135
- workspacePatterns.includes(
136
- /* inlined export .ROOT_WORKSPACE_SELECTOR */ "@root",
137
- )
138
- ) {
139
- workspaces.push(this.rootWorkspace);
140
- workspacePatterns = workspacePatterns.filter(
141
- (pattern) =>
142
- pattern !== /* inlined export .ROOT_WORKSPACE_SELECTOR */ "@root",
143
- );
144
- }
145
- workspaces.push(
146
- ...sortWorkspaces(
147
- matchWorkspacesByPatterns(workspacePatterns, this.workspaces),
148
- ),
133
+ const matched = matchWorkspacesByPatterns(
134
+ workspacePatterns,
135
+ this.workspaces,
136
+ this.rootWorkspace,
137
+ );
138
+ // Preserve historical ordering: root workspace first, then sorted others.
139
+ const rootName = this.rootWorkspace.name;
140
+ const rootMatch = matched.find((workspace) => workspace.name === rootName);
141
+ const rest = sortWorkspaces(
142
+ matched.filter((workspace) => workspace.name !== rootName),
149
143
  );
150
- return workspaces;
144
+ return rootMatch ? [rootMatch, ...rest] : rest;
151
145
  }
152
146
  createScriptCommand(options) {
153
147
  validateJSTypes(
@@ -19,7 +19,7 @@ const createShellScript = (command) => {
19
19
  return DEFAULT_TEMP_DIR.createFile({
20
20
  name: fileName,
21
21
  content: command,
22
- mode: 493,
22
+ mode: 448,
23
23
  });
24
24
  };
25
25
  const createScriptExecutor = (command, shell) => {
@@ -1,6 +1,16 @@
1
+ import { quote } from "../internal/bundledDeps/shellQuote.mjs";
1
2
  import { BunWorkspacesError, IS_WINDOWS } from "../internal/core/index.mjs";
2
3
  import { getWorkspaceScriptMetadataConfig } from "../3725.mjs";
3
4
 
5
+ /**
6
+ * Wrap a value so that, when it is concatenated into a shell command for
7
+ * `shell`, the receiving shell parses it as a single literal token. Used
8
+ * when substituted metadata values land in a shell-interpretable context
9
+ * (inline command bodies, string-form `--args` before POSIX parse).
10
+ */ const quoteShellValue = (value, shell) =>
11
+ IS_WINDOWS && shell === "system"
12
+ ? `"${value.replace(/"/g, '""')}"`
13
+ : quote([value]);
4
14
  const createScriptRuntimeEnvVars = (metadata) => {
5
15
  const keys = [
6
16
  "projectPath",
@@ -16,7 +26,12 @@ const createScriptRuntimeEnvVars = (metadata) => {
16
26
  return acc;
17
27
  }, {});
18
28
  };
19
- const interpolateWorkspaceScriptMetadata = (text, metadata, shell) => {
29
+ const interpolateWorkspaceScriptMetadata = ({
30
+ text,
31
+ metadata,
32
+ shell,
33
+ quoteValues = false,
34
+ }) => {
20
35
  const keys = [
21
36
  "projectPath",
22
37
  "projectName",
@@ -33,6 +48,13 @@ const interpolateWorkspaceScriptMetadata = (text, metadata, shell) => {
33
48
  (k) => getWorkspaceScriptMetadataConfig(k).inlineName === match,
34
49
  );
35
50
  const value = metadata[key];
51
+ if (quoteValues) {
52
+ // Preserve "empty substitution is invisible" — quoting an empty value
53
+ // would inject a literal `''` shell token (an empty positional arg),
54
+ // which changes argv length for commands like `echo <scriptName>`
55
+ // when no inline scriptName is set.
56
+ return value === "" ? "" : quoteShellValue(value, shell);
57
+ }
36
58
  if (IS_WINDOWS && shell === "bun") {
37
59
  return value.replace(/\\/g, "\\\\");
38
60
  }
@@ -57,4 +79,5 @@ export {
57
79
  createScriptRuntimeEnvVars,
58
80
  getWorkspaceScriptMetadata,
59
81
  interpolateWorkspaceScriptMetadata,
82
+ quoteShellValue,
60
83
  };
@@ -33,9 +33,14 @@ const applyWorkspacePatternConfigs = (
33
33
  workspaceMap,
34
34
  workspaceAliases,
35
35
  patternConfigs,
36
+ rootWorkspace,
36
37
  ) => {
37
38
  for (const entry of patternConfigs) {
38
- const matched = matchWorkspacesByPatterns(entry.patterns, workspaces);
39
+ const matched = matchWorkspacesByPatterns(
40
+ entry.patterns,
41
+ workspaces,
42
+ rootWorkspace,
43
+ );
39
44
  for (const workspace of matched) {
40
45
  const mapEntry = workspaceMap[workspace.name];
41
46
  const prevConfig = mapEntry.config;
@@ -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})`,
@@ -67,6 +67,7 @@ const findWorkspaces = ({
67
67
  workspaceGlobs: _workspaceGlobs,
68
68
  includeRootWorkspace = false,
69
69
  workspacePatternConfigs,
70
+ loadConfigOptions,
70
71
  }) => {
71
72
  rootDirectory = path.resolve(rootDirectory);
72
73
  logger.debug(`Finding workspaces in ${rootDirectory}`);
@@ -109,6 +110,7 @@ const findWorkspaces = ({
109
110
  );
110
111
  const workspaceConfig = loadWorkspaceConfig(
111
112
  path.dirname(packageJsonPath),
113
+ loadConfigOptions,
112
114
  );
113
115
  if (workspaceConfig) {
114
116
  for (const alias of workspaceConfig.aliases) {
@@ -178,10 +180,12 @@ const findWorkspaces = ({
178
180
  workspaceMap,
179
181
  workspaceAliases,
180
182
  workspacePatternConfigs,
183
+ rootWorkspace,
181
184
  );
182
185
  }
183
186
  validateWorkspaceDependencyRules({
184
187
  workspaceMap,
188
+ rootWorkspace,
185
189
  });
186
190
  validateWorkspaceAliases(workspaces, workspaceAliases, rootWorkspace.name);
187
191
  logger.debug(