bun-workspaces 1.9.0 → 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.
- package/AGENTS.md +537 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/affected/fileAffectedWorkspaces.mjs +7 -1
- package/src/config/rootConfig/rootConfig.mjs +5 -9
- package/src/internal/generated/aiDocs/docs.mjs +19 -8
- package/src/project/implementations/fileSystemProject/affectedWorkspaces.mjs +2 -0
- package/src/project/implementations/projectBase.mjs +11 -17
- package/src/workspaces/applyWorkspacePatternConfigs.mjs +6 -1
- package/src/workspaces/dependencyGraph/validateDependencyRules.mjs +14 -7
- package/src/workspaces/findWorkspaces.mjs +2 -0
- package/src/workspaces/workspacePattern.mjs +134 -46
package/AGENTS.md
ADDED
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
<!-- bun-workspaces (npm package) agent documentation begin -->
|
|
2
|
+
## Project Overview
|
|
3
|
+
|
|
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.
|
|
5
|
+
|
|
6
|
+
Three main domain terms to know:
|
|
7
|
+
|
|
8
|
+
- Project: generally represents a monorepo and is defined by the root `package.json` file
|
|
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
|
+
- 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
|
+
|
|
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
|
+
|
|
14
|
+
## Concepts
|
|
15
|
+
|
|
16
|
+
### Workspace patterns
|
|
17
|
+
|
|
18
|
+
Many features accept a list of workspace patterns to match a subset of workspaces:
|
|
19
|
+
|
|
20
|
+
`[not:][(name|alias|path|tag):][re:]<value>`
|
|
21
|
+
|
|
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.
|
|
23
|
+
|
|
24
|
+
Patterns can include a wildcard to match only by workspace name: `my-workspace-*`.
|
|
25
|
+
|
|
26
|
+
- Alias pattern specifier: `alias:my-alias-*`.
|
|
27
|
+
- Path pattern specifier (supports glob): `path:packages/**/*`.
|
|
28
|
+
- Name pattern specifier: `name:my-workspace-*`.
|
|
29
|
+
- Tag pattern specifier: `tag:my-tag`.
|
|
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.
|
|
36
|
+
|
|
37
|
+
### Workspace Script Metadata
|
|
38
|
+
|
|
39
|
+
Scripts ran via bun-workspaces can access metadata about the workspace, script, and project
|
|
40
|
+
via env vars. This same metadata can also be interpolated into inline scripts and appended args.
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
// in a workspace's script invoked by bun-workspaces using a metadata function
|
|
44
|
+
import { getWorkspaceScriptMetadata } from "bun-workspaces/script";
|
|
45
|
+
|
|
46
|
+
// Use the helper within a script that was invoked via bun-workspaces
|
|
47
|
+
const projectPath = getWorkspaceScriptMetadata("projectPath");
|
|
48
|
+
const projectName = getWorkspaceScriptMetadata("projectName");
|
|
49
|
+
const workspaceName = getWorkspaceScriptMetadata("workspaceName");
|
|
50
|
+
const workspacePath = getWorkspaceScriptMetadata("workspacePath");
|
|
51
|
+
const workspaceRelativePath = getWorkspaceScriptMetadata(
|
|
52
|
+
"workspaceRelativePath",
|
|
53
|
+
);
|
|
54
|
+
const scriptName = getWorkspaceScriptMetadata("scriptName");
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
// In a script, but accessing the same data via plain environment variables (same values as previous example)
|
|
59
|
+
const projectPath = process.env.BW_PROJECT_PATH;
|
|
60
|
+
const workspaceName = process.env.BW_WORKSPACE_NAME;
|
|
61
|
+
const workspacePath = process.env.BW_WORKSPACE_PATH;
|
|
62
|
+
const workspaceRelativePath = process.env.BW_WORKSPACE_RELATIVE_PATH;
|
|
63
|
+
const scriptName = process.env.BW_SCRIPT_NAME;
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# interpolated
|
|
68
|
+
bw run "bun <projectPath>/my-script.ts" --inline \
|
|
69
|
+
--inline-name="my-script-name" \
|
|
70
|
+
--args="<workspaceName> <workspacePath>"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Affected workspaces
|
|
74
|
+
|
|
75
|
+
A workspace is "affected" when something in its set of **inputs** has changed. Inputs default to:
|
|
76
|
+
|
|
77
|
+
- Files in the workspace's directory (only git-trackable files; the default file pattern is `"."`)
|
|
78
|
+
- Workspace dependencies — if a workspace dep is affected for any reason, dependents cascade as affected
|
|
79
|
+
- All non-workspace dependencies declared in its `package.json` (across all four maps: `dependencies`, `devDependencies`, `peerDependencies`, `optionalDependencies`). Version changes are detected by diffing resolved versions in `bun.lock`. For `peerDependencies`/`optionalDependencies`, lockfile presence is the gate — an unresolved optional (e.g., a platform-skipped native binding) emits no change.
|
|
80
|
+
|
|
81
|
+
Inputs are configurable per workspace (`defaultInputs`) and per script (`scripts[name].inputs`):
|
|
82
|
+
|
|
83
|
+
- `files`: file/dir/glob patterns relative to the workspace. Leading `/` makes a pattern relative to the project root. Prefix `!` to exclude. Only git-trackable files match.
|
|
84
|
+
- `workspacePatterns`: workspace patterns whose matched workspaces are treated as inputs (like dependencies, but without needing a real `package.json` dep).
|
|
85
|
+
- `externalDependencies`: an allowlist of package names. Omitted = all external deps participate; `[]` = none participate; non-empty = only listed names participate (intersected with the workspace's actual external deps).
|
|
86
|
+
|
|
87
|
+
There are two diff sources:
|
|
88
|
+
|
|
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.
|
|
90
|
+
- **fileList**: pass changed files explicitly (paths, dirs, or globs) — bypasses git entirely.
|
|
91
|
+
|
|
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
|
+
|
|
94
|
+
### CLI examples:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
alias bw="bunx bun-workspaces"
|
|
98
|
+
|
|
99
|
+
bw list-workspaces # human-readable output
|
|
100
|
+
bw ls --json --pretty # ls is alias for list-workspaces
|
|
101
|
+
bw ls "name:my-workspace-*" "alias:my-alias-*" "path:packages/**/*" # accepts workspace patterns
|
|
102
|
+
|
|
103
|
+
# info includes the name, aliases, path, etc.
|
|
104
|
+
bw workspace-info my-workspace
|
|
105
|
+
bw info my-workspace --json --pretty # info is alias for workspace-info
|
|
106
|
+
|
|
107
|
+
# info includes the script name and workspaces that have it in their package.json "scripts" field
|
|
108
|
+
bw script-info my-script --json --pretty
|
|
109
|
+
|
|
110
|
+
# run the package.json "lint" script for all workspaces that have it
|
|
111
|
+
bw run-script lint
|
|
112
|
+
|
|
113
|
+
# run is alias for run-script
|
|
114
|
+
# run the package.json "lint" script for workspaces using matching specifiers
|
|
115
|
+
bw run lint my-workspace-name "alias:my-alias-pattern-*" "path:my-glob/**/*" # accepts workspace patterns
|
|
116
|
+
|
|
117
|
+
# A workspace's script will wait until any workspaces it depends on have completed
|
|
118
|
+
# Similar to Bun's --filter behavior
|
|
119
|
+
bw run lint --dep-order
|
|
120
|
+
|
|
121
|
+
# Continue running scripts even if a dependency fails
|
|
122
|
+
bw run lint --dep-order --ignore-dep-failure
|
|
123
|
+
|
|
124
|
+
# special root workspace selector (works even if root workspace is not included)
|
|
125
|
+
bw run lint @root
|
|
126
|
+
|
|
127
|
+
# Scripts run in parallel by default
|
|
128
|
+
bw run lint --parallel=false # Run in series
|
|
129
|
+
|
|
130
|
+
# Default can be overridden by config or env var BW_PARALLEL_MAX_DEFAULT
|
|
131
|
+
bw run lint --parallel # default "auto", os.availableParallelism()
|
|
132
|
+
bw run lint --parallel=2 # Run in parallel with a max of 2 concurrent scripts
|
|
133
|
+
bw run lint --parallel=50% # 50% of os.availableParallelism()
|
|
134
|
+
bw run lint --parallel=unbounded # run all in one batch
|
|
135
|
+
|
|
136
|
+
# add args to the script command
|
|
137
|
+
bw run lint --args="--my-arg=value"
|
|
138
|
+
bw run lint --args="--my-arg=<workspaceName>" # use the workspace name in args
|
|
139
|
+
|
|
140
|
+
# run the script as an inline command from the workspace directory
|
|
141
|
+
bw run "bun build" --inline
|
|
142
|
+
bw run "bun build" --inline --inline-name="my-script"
|
|
143
|
+
bw run "bun build" --inline --shell=system # use the system shell
|
|
144
|
+
|
|
145
|
+
# Use the grouped output style (default when on a TTY)
|
|
146
|
+
bw run my-script --output-style=grouped
|
|
147
|
+
|
|
148
|
+
# Set the max preview lines for script output in grouped output style
|
|
149
|
+
bw run my-script --output-style=grouped --grouped-lines=auto
|
|
150
|
+
bw run my-script --output-style=grouped --grouped-lines=10
|
|
151
|
+
|
|
152
|
+
# Use simple script output with workspace prefixes (default when not on a TTY)
|
|
153
|
+
bw run my-script --output-style=prefixed
|
|
154
|
+
|
|
155
|
+
# Use the plain output style (no workspace prefixes)
|
|
156
|
+
bw run my-script --output-style=plain
|
|
157
|
+
|
|
158
|
+
# List affected workspaces (default: git diff HEAD vs the configured base ref, "main" by default)
|
|
159
|
+
bw list-affected
|
|
160
|
+
bw ls-affected # alias
|
|
161
|
+
|
|
162
|
+
# Compare specific git refs
|
|
163
|
+
bw ls-affected --base=my-branch-a --head=my-branch-b
|
|
164
|
+
bw ls-affected -B my-branch-a -H my-branch-b # short forms
|
|
165
|
+
|
|
166
|
+
# Resolve inputs for a specific script (uses scripts[name].inputs when configured)
|
|
167
|
+
bw ls-affected --script=build
|
|
168
|
+
|
|
169
|
+
# Ignore some uncommitted changes (uncommitted included by default)
|
|
170
|
+
bw ls-affected --ignore-uncommitted # all of: staged, unstaged, untracked
|
|
171
|
+
bw ls-affected --ignore-untracked
|
|
172
|
+
bw ls-affected --ignore-unstaged
|
|
173
|
+
bw ls-affected --ignore-staged
|
|
174
|
+
|
|
175
|
+
# Skip workspace dep cascade (only direct file/external-dep changes flag a workspace)
|
|
176
|
+
bw ls-affected --ignore-workspace-deps
|
|
177
|
+
|
|
178
|
+
# Skip lockfile-based external dep version tracking
|
|
179
|
+
bw ls-affected --ignore-external-deps
|
|
180
|
+
|
|
181
|
+
# Bypass git entirely with an explicit list of changed files
|
|
182
|
+
# (paths, dirs, globs; '!' to exclude; whitespace-separated)
|
|
183
|
+
bw ls-affected --files="packages/example/**/*.ts packages/example/my-file.json"
|
|
184
|
+
bw ls-affected -F "packages/a/**/*.ts !packages/a/**/*.test.ts"
|
|
185
|
+
|
|
186
|
+
# Per-workspace summary of why each workspace is affected
|
|
187
|
+
bw ls-affected --explain
|
|
188
|
+
bw ls-affected -e
|
|
189
|
+
|
|
190
|
+
# Full per-file changes and dep cascade chain for each affected workspace
|
|
191
|
+
bw ls-affected --explain --detailed
|
|
192
|
+
bw ls-affected -e -D
|
|
193
|
+
|
|
194
|
+
# JSON output (with --explain produces the full result object)
|
|
195
|
+
bw ls-affected --json --pretty
|
|
196
|
+
bw ls-affected --explain --json --pretty
|
|
197
|
+
|
|
198
|
+
# Run a script across affected workspaces (accepts the same affected options
|
|
199
|
+
# as ls-affected, plus the same script-execution options as run-script:
|
|
200
|
+
# --parallel, --dep-order, --args, --output-style, --inline, etc.)
|
|
201
|
+
bw run-affected build
|
|
202
|
+
bw run-affected build --base=my-branch --ignore-uncommitted --dep-order
|
|
203
|
+
bw run-affected build --files="packages/a/src/**/*.ts" --parallel=2
|
|
204
|
+
bw run-affected "bun build" --inline --inline-name=build # inline command form
|
|
205
|
+
|
|
206
|
+
### Global Options ###
|
|
207
|
+
# Root directory of project:
|
|
208
|
+
bw --cwd=/path/to/project ls
|
|
209
|
+
bw -d /path/to/project ls
|
|
210
|
+
|
|
211
|
+
# Include root workspace as a normal workspace (default false):
|
|
212
|
+
bw --include-root ls
|
|
213
|
+
bw -r ls
|
|
214
|
+
bw --no-include-root ls # override config/env var setting
|
|
215
|
+
|
|
216
|
+
# Log level (debug|info|warn|error|silent, default info)
|
|
217
|
+
bw --log-level=silent ls
|
|
218
|
+
bw -l silent ls
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### API examples:
|
|
222
|
+
|
|
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.
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
import { createFileSystemProject } from "bun-workspaces";
|
|
227
|
+
|
|
228
|
+
const project = createFileSystemProject({
|
|
229
|
+
// the options object itself and its properties are optional
|
|
230
|
+
rootDirectory: "path/to/your/project",
|
|
231
|
+
includeRootWorkspace: false,
|
|
232
|
+
});
|
|
233
|
+
project.workspaces; // array of all workspaces in the project
|
|
234
|
+
project.rootWorkspace; // the root workspace (available even when not included in the workspaces array)
|
|
235
|
+
project.findWorkspaceByName("my-workspace"); // find a workspace by name
|
|
236
|
+
project.findWorkspaceByAlias("my-alias"); // find a workspace by alias
|
|
237
|
+
project.findWorkspaceByNameOrAlias("my-workspace-or-alias"); // find a workspace by name or alias
|
|
238
|
+
project.findWorkspacesByPattern(
|
|
239
|
+
"my-workspace-name",
|
|
240
|
+
"my-workspace-alias",
|
|
241
|
+
"my-name-pattern-*",
|
|
242
|
+
"alias:my-alias-*",
|
|
243
|
+
"path:my-glob/**/*",
|
|
244
|
+
); // find workspaces by pattern like the CLI
|
|
245
|
+
project.runWorkspaceScript({
|
|
246
|
+
workspaceNameOrAlias: "my-workspace",
|
|
247
|
+
script: "lint",
|
|
248
|
+
inline: true,
|
|
249
|
+
// args can be a string or an array of strings
|
|
250
|
+
// if string, the argv will be parsed POSIX-style
|
|
251
|
+
args: "--my-arg=value",
|
|
252
|
+
});
|
|
253
|
+
project.runScriptAcrossWorkspaces({
|
|
254
|
+
script: "lint",
|
|
255
|
+
workspacePatterns: [
|
|
256
|
+
"alias:my-alias-pattern-*",
|
|
257
|
+
"path:my-glob/**/*",
|
|
258
|
+
"workspace-name-a",
|
|
259
|
+
"workspace-alias-b",
|
|
260
|
+
],
|
|
261
|
+
parallel: true, // also could be { max: 2 }, max taking same options as seen in CLI examples above (e.g. "50%", "auto", etc.)
|
|
262
|
+
dependencyOrder: true,
|
|
263
|
+
ignoreDependencyFailure: true,
|
|
264
|
+
// same as for runWorkspaceScript
|
|
265
|
+
args: ["--my", "--appended", "--args"],
|
|
266
|
+
// Optional, callback when script starts, skips, or exits
|
|
267
|
+
onScriptEvent: (event, { workspace, exitResult }) => {
|
|
268
|
+
// event: "start", "skip", "exit"
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Determine affected workspaces — git mode (default)
|
|
273
|
+
project.determineAffectedWorkspaces({
|
|
274
|
+
diffSource: "git",
|
|
275
|
+
// optional: resolve inputs for a specific script (uses scripts[name].inputs)
|
|
276
|
+
script: "build",
|
|
277
|
+
// optional: skip workspace dep cascade
|
|
278
|
+
ignoreWorkspaceDependencies: false,
|
|
279
|
+
// optional: skip lockfile-based external dep version tracking
|
|
280
|
+
ignoreExternalDependencies: false,
|
|
281
|
+
diffOptions: {
|
|
282
|
+
baseRef: "main", // default from config / "main"
|
|
283
|
+
headRef: "HEAD", // default
|
|
284
|
+
ignoreUncommitted: false, // staged + unstaged + untracked
|
|
285
|
+
ignoreUntracked: false,
|
|
286
|
+
ignoreUnstaged: false,
|
|
287
|
+
ignoreStaged: false,
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Determine affected workspaces — fileList mode (bypass git)
|
|
292
|
+
project.determineAffectedWorkspaces({
|
|
293
|
+
diffSource: "fileList",
|
|
294
|
+
// paths, directories, or globs (relative to project root); '!' to exclude
|
|
295
|
+
changedFiles: ["packages/a/**/*.ts", "!packages/a/**/*.test.ts"],
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// Run a script across affected workspaces. Accepts the same affected options
|
|
299
|
+
// as determineAffectedWorkspaces, plus the script-execution options from
|
|
300
|
+
// runScriptAcrossWorkspaces (parallel, dependencyOrder, args, onScriptEvent, etc.).
|
|
301
|
+
project.runAffectedWorkspaceScript({
|
|
302
|
+
script: "build",
|
|
303
|
+
diffSource: "git",
|
|
304
|
+
diffOptions: { baseRef: "main", ignoreUncommitted: true },
|
|
305
|
+
parallel: { max: 2 },
|
|
306
|
+
dependencyOrder: true,
|
|
307
|
+
ignoreDependencyFailure: true,
|
|
308
|
+
});
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## The Workspace object
|
|
312
|
+
|
|
313
|
+
```jsonc
|
|
314
|
+
{
|
|
315
|
+
// The name of the workspace from its package.json
|
|
316
|
+
"name": "my-workspace",
|
|
317
|
+
// Whether the workspace is the root workspace
|
|
318
|
+
"isRoot": false,
|
|
319
|
+
// The relative path to the workspace from the project root
|
|
320
|
+
"path": "my/workspace/path",
|
|
321
|
+
// The glob pattern from the root package.json "workspaces" field
|
|
322
|
+
// that this workspace was matched from
|
|
323
|
+
"matchPattern": "my/workspace/pattern/*",
|
|
324
|
+
// The scripts available in the workspace's package.json
|
|
325
|
+
"scripts": ["my-script"],
|
|
326
|
+
// Aliases defined in workspace configuration (bw.workspace.jsonc/bw.workspace.json)
|
|
327
|
+
"aliases": ["my-alias"],
|
|
328
|
+
// Tags defined in workspace configuration
|
|
329
|
+
"tags": ["my-tag"],
|
|
330
|
+
// Names of other workspaces that this workspace depends on
|
|
331
|
+
"dependencies": ["my-dependency"],
|
|
332
|
+
// Names of other workspaces that depend on this workspace
|
|
333
|
+
"dependents": ["my-dependent"],
|
|
334
|
+
// Non-workspace package deps declared in package.json (across all four maps).
|
|
335
|
+
// `source` is one of "dependencies" | "devDependencies" | "peerDependencies" | "optionalDependencies".
|
|
336
|
+
// `version` is the package.json range, with `catalog:`/`catalog:<name>` resolved when possible.
|
|
337
|
+
// `catalog` is present when declared via a catalog ref.
|
|
338
|
+
"externalDependencies": [
|
|
339
|
+
{ "name": "lodash", "version": "^4.17.0", "source": "dependencies" },
|
|
340
|
+
{ "name": "typescript", "version": "^5.0.0", "source": "devDependencies" },
|
|
341
|
+
{
|
|
342
|
+
"name": "react",
|
|
343
|
+
"version": "^18.0.0",
|
|
344
|
+
"source": "dependencies",
|
|
345
|
+
"catalog": { "name": "" },
|
|
346
|
+
},
|
|
347
|
+
],
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
## Root config
|
|
352
|
+
|
|
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`.
|
|
354
|
+
|
|
355
|
+
Config defaults here take precedence over environment variables. Explicit CLI arguments or API options take precedence over all other settings.
|
|
356
|
+
|
|
357
|
+
```jsonc
|
|
358
|
+
{
|
|
359
|
+
"defaults": {
|
|
360
|
+
"parallelMax": 5, // same options as seen in CLI examples above
|
|
361
|
+
"shell": "system", // "bun" or "system" (default "bun")
|
|
362
|
+
"includeRootWorkspace": true, // treat root package.json as a normal workspace
|
|
363
|
+
"affectedBaseRef": "main", // default git base ref for affected resolution (env: BW_AFFECTED_BASE_REF_DEFAULT)
|
|
364
|
+
},
|
|
365
|
+
"workspacePatternConfigs": [
|
|
366
|
+
// see Workspace Pattern Configs section below
|
|
367
|
+
],
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### mergeRootConfig
|
|
372
|
+
|
|
373
|
+
`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`.
|
|
374
|
+
|
|
375
|
+
```ts
|
|
376
|
+
import { mergeRootConfig } from "bun-workspaces/config";
|
|
377
|
+
|
|
378
|
+
export default mergeRootConfig(
|
|
379
|
+
{ defaults: { parallelMax: 4 } },
|
|
380
|
+
{ defaults: { shell: "system" } },
|
|
381
|
+
(prevConfig) => ({ defaults: { includeRootWorkspace: true } }),
|
|
382
|
+
);
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
## Workspace config
|
|
386
|
+
|
|
387
|
+
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`.
|
|
388
|
+
|
|
389
|
+
Aliases must be unique to each workspace and must not clash with other workspaces' `package.json` names.
|
|
390
|
+
|
|
391
|
+
Tags are strings to group workspaces together; they do not need to be unique.
|
|
392
|
+
|
|
393
|
+
```jsonc
|
|
394
|
+
{
|
|
395
|
+
"alias": "my-alias", // can be array
|
|
396
|
+
"tags": ["my-tag"],
|
|
397
|
+
// Default inputs used to determine if the workspace is affected, applied to
|
|
398
|
+
// all scripts that don't configure their own inputs. See "Inputs" below.
|
|
399
|
+
"defaultInputs": {
|
|
400
|
+
"files": ["src/**/*.ts", "!src/**/*.test.ts"],
|
|
401
|
+
"workspacePatterns": ["tag:shared-lib"],
|
|
402
|
+
"externalDependencies": ["lodash", "react"],
|
|
403
|
+
},
|
|
404
|
+
"scripts": {
|
|
405
|
+
"lint": {
|
|
406
|
+
// set optional sorting order for scripts
|
|
407
|
+
"order": 1,
|
|
408
|
+
},
|
|
409
|
+
"build": {
|
|
410
|
+
// per-script inputs override defaultInputs for this script's affected resolution
|
|
411
|
+
"inputs": {
|
|
412
|
+
"files": ["src/**/*.ts", "/shared-types/**/*.ts"], // leading "/" = relative to the project root
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
"rules": {
|
|
417
|
+
"workspaceDependencies": {
|
|
418
|
+
// allowPatterns: only workspaces matching these patterns are permitted as dependencies
|
|
419
|
+
"allowPatterns": ["my-allow-pattern-*"],
|
|
420
|
+
// denyPatterns: workspaces matching these patterns are forbidden as dependencies.
|
|
421
|
+
// When combined with allowPatterns, deny filters within the allowed subset.
|
|
422
|
+
"denyPatterns": ["my-deny-pattern-*"],
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
}
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
### Inputs
|
|
429
|
+
|
|
430
|
+
The `defaultInputs` field (and the per-script `scripts[name].inputs` field) controls what counts as an input for [affected workspace](#affected-workspaces) resolution. Both have the same shape (`WorkspaceInputsConfig`):
|
|
431
|
+
|
|
432
|
+
- `files` — file paths, directories, or globs relative to the workspace's directory. Leading `/` makes a pattern relative to the project root. Prefix with `!` to exclude. Only git-trackable files are matched. Default when not provided is `["."]` (everything in the workspace dir).
|
|
433
|
+
- `workspacePatterns` — workspace patterns whose matched workspaces are treated as inputs (like dependencies, but without needing a real `package.json` dep edge).
|
|
434
|
+
- `externalDependencies` — allowlist of package names that participate in lockfile-change detection. Omitted = all external deps participate; `[]` = none participate; non-empty list = only listed names participate (intersected with the workspace's actual external deps from `package.json`).
|
|
435
|
+
|
|
436
|
+
Per-script `inputs` fully replaces `defaultInputs` for that script — the two are not merged. If a script has its own `inputs` field, `defaultInputs` is ignored for that script.
|
|
437
|
+
|
|
438
|
+
### Workspace Dependency Rules
|
|
439
|
+
|
|
440
|
+
Using the `rules.workspaceDependencies` field, you can define rules for which workspaces are allowed to be dependencies, using `allowPatterns`, `denyPatterns`, or both.
|
|
441
|
+
|
|
442
|
+
`allowPatterns` defines the permitted subset of dependencies. `denyPatterns` forbids specific dependencies. When both are present, `denyPatterns` further filters within the subset permitted by `allowPatterns`.
|
|
443
|
+
|
|
444
|
+
Workspace Patterns are used to match workspaces.
|
|
445
|
+
|
|
446
|
+
### mergeWorkspaceConfig
|
|
447
|
+
|
|
448
|
+
`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`.
|
|
449
|
+
|
|
450
|
+
```ts
|
|
451
|
+
import { mergeWorkspaceConfig } from "bun-workspaces/config";
|
|
452
|
+
|
|
453
|
+
export default mergeWorkspaceConfig(
|
|
454
|
+
{ alias: "a", tags: ["x"] },
|
|
455
|
+
{ alias: "b", scripts: { build: { order: 1 } } },
|
|
456
|
+
(prevConfig) => ({ tags: ["y"] }),
|
|
457
|
+
);
|
|
458
|
+
// result: { alias: ["a", "b"], tags: ["x", "y"], scripts: { build: { order: 1 } } }
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
## Workspace Pattern Configs
|
|
462
|
+
|
|
463
|
+
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.
|
|
464
|
+
|
|
465
|
+
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.
|
|
466
|
+
|
|
467
|
+
Pattern matching reflects the accumulated state: aliases and tags added by earlier entries are visible to later entries' patterns.
|
|
468
|
+
|
|
469
|
+
```ts
|
|
470
|
+
import { defineRootConfig } from "bun-workspaces/config";
|
|
471
|
+
|
|
472
|
+
export default defineRootConfig({
|
|
473
|
+
workspacePatternConfigs: [
|
|
474
|
+
{
|
|
475
|
+
patterns: ["path:packages/apps/**/*"],
|
|
476
|
+
config: { tags: ["app"] },
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
// "tag:app" matches because the entry above added it
|
|
480
|
+
patterns: ["tag:app"],
|
|
481
|
+
config: {
|
|
482
|
+
rules: { workspaceDependencies: { allowPatterns: ["tag:lib"] } },
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
patterns: ["tag:app"],
|
|
487
|
+
// Factory form: JS/TS only — receives static workspace data and accumulated config
|
|
488
|
+
config: (workspace, prevConfig) => ({
|
|
489
|
+
alias: workspace.name.replace(/^@my-scope\//, ""),
|
|
490
|
+
}),
|
|
491
|
+
},
|
|
492
|
+
],
|
|
493
|
+
});
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
### Factory function context (`RawWorkspace`)
|
|
497
|
+
|
|
498
|
+
The factory `(workspace: RawWorkspace, prevConfig: ResolvedWorkspaceConfig) => WorkspaceConfig` receives:
|
|
499
|
+
|
|
500
|
+
- `workspace.name` — package name from package.json
|
|
501
|
+
- `workspace.isRoot` — whether this is the root workspace
|
|
502
|
+
- `workspace.path` — relative path from project root
|
|
503
|
+
- `workspace.matchPattern` — glob from root package.json `workspaces` field that matched
|
|
504
|
+
- `workspace.scripts` — sorted list of script names from package.json
|
|
505
|
+
- `workspace.dependencies` — names of workspace dependencies
|
|
506
|
+
- `workspace.dependents` — names of workspaces that depend on this one
|
|
507
|
+
|
|
508
|
+
`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`, `defaultInputs?: WorkspaceInputsConfig`.
|
|
509
|
+
|
|
510
|
+
## TypeScript/JSON Config Files
|
|
511
|
+
|
|
512
|
+
### TypeScript
|
|
513
|
+
|
|
514
|
+
`bw.workspace.ts`
|
|
515
|
+
|
|
516
|
+
```ts
|
|
517
|
+
import { defineWorkspaceConfig } from "bun-workspaces/config";
|
|
518
|
+
|
|
519
|
+
export default defineWorkspaceConfig({
|
|
520
|
+
alias: "my-alias",
|
|
521
|
+
tags: ["my-tag"],
|
|
522
|
+
});
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
`bw.root.ts`
|
|
526
|
+
|
|
527
|
+
```ts
|
|
528
|
+
import { defineRootConfig } from "bun-workspaces/config";
|
|
529
|
+
|
|
530
|
+
export default defineRootConfig({
|
|
531
|
+
defaults: {
|
|
532
|
+
parallelMax: 5,
|
|
533
|
+
},
|
|
534
|
+
});
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
<!-- bun-workspaces (npm package) agent documentation end -->
|
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ A [monorepo](http://sonarsource.com/resources/library/monorepo/) tool that enhan
|
|
|
18
18
|
- Run one-off [**Bun Shell**](https://bun.com/docs/runtime/shell) commands in your workspaces 🐚
|
|
19
19
|
- Use with Bun as your package manager for **Node** projects 🎁
|
|
20
20
|
- Determine **affected workspaces** based on changed files 🕸️
|
|
21
|
-
-
|
|
21
|
+
- AI: Provides an [AGENTS.md](https://bunworkspaces.com/ai/agents) file and an [MCP server](https://bunworkspaces.com/ai/mcp)! 🛠️
|
|
22
22
|
|
|
23
23
|
To get started, all you need is a repo using Bun's workspaces feature for nested JavaScript/TypeScript packages. This adds enhanced features on top of plain workspaces.
|
|
24
24
|
|
package/package.json
CHANGED
|
@@ -147,7 +147,10 @@ const matchChangedFilesForWorkspace = ({
|
|
|
147
147
|
}
|
|
148
148
|
return matchedFiles;
|
|
149
149
|
};
|
|
150
|
-
const resolveInputWorkspaceDependencies = ({
|
|
150
|
+
const resolveInputWorkspaceDependencies = ({
|
|
151
|
+
workspaceInputs,
|
|
152
|
+
rootWorkspace,
|
|
153
|
+
}) => {
|
|
151
154
|
const inputDependenciesByName = new Map();
|
|
152
155
|
const allWorkspaces = workspaceInputs.map(({ workspace }) => workspace);
|
|
153
156
|
for (const { workspace, inputWorkspacePatterns } of workspaceInputs) {
|
|
@@ -158,6 +161,7 @@ const resolveInputWorkspaceDependencies = ({ workspaceInputs }) => {
|
|
|
158
161
|
const matchedNames = matchWorkspacesByPatterns(
|
|
159
162
|
inputWorkspacePatterns,
|
|
160
163
|
allWorkspaces,
|
|
164
|
+
rootWorkspace,
|
|
161
165
|
)
|
|
162
166
|
.map((matchedWorkspace) => matchedWorkspace.name)
|
|
163
167
|
.filter((matchedName) => matchedName !== workspace.name);
|
|
@@ -346,6 +350,7 @@ const getFileAffectedWorkspaces = async ({
|
|
|
346
350
|
rootDirectory,
|
|
347
351
|
workspaceInputs,
|
|
348
352
|
changedFilePaths,
|
|
353
|
+
rootWorkspace,
|
|
349
354
|
externalDepChangesByWorkspace = new Map(),
|
|
350
355
|
ignoreWorkspaceDependencies = false,
|
|
351
356
|
}) => {
|
|
@@ -375,6 +380,7 @@ const getFileAffectedWorkspaces = async ({
|
|
|
375
380
|
});
|
|
376
381
|
const inputDependenciesByName = resolveInputWorkspaceDependencies({
|
|
377
382
|
workspaceInputs,
|
|
383
|
+
rootWorkspace,
|
|
378
384
|
});
|
|
379
385
|
const affectedSet = computeAffectedWorkspaceSet({
|
|
380
386
|
workspaceInputs,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolveDefaultAffectedBaseRef } from "../../affected/affectedBaseRef.mjs";
|
|
2
|
-
import
|
|
2
|
+
import { validate } from "../../internal/generated/ajv/validateRootConfig.mjs";
|
|
3
3
|
import {
|
|
4
4
|
determineParallelMax,
|
|
5
5
|
resolveScriptShell,
|
|
@@ -9,16 +9,16 @@ import { executeValidator } from "../util/validateConfig.mjs";
|
|
|
9
9
|
import { validateWorkspaceConfig } from "../workspaceConfig/workspaceConfig.mjs";
|
|
10
10
|
import { ROOT_CONFIG_ERRORS } from "./errors.mjs";
|
|
11
11
|
|
|
12
|
-
const
|
|
12
|
+
const validateRootConfig = (config) =>
|
|
13
13
|
executeValidator(
|
|
14
|
-
|
|
14
|
+
validate,
|
|
15
15
|
"RootConfig",
|
|
16
16
|
config,
|
|
17
17
|
ROOT_CONFIG_ERRORS.InvalidRootConfig,
|
|
18
18
|
);
|
|
19
19
|
const createDefaultRootConfig = () => resolveRootConfig({});
|
|
20
20
|
const resolveRootConfig = (config) => {
|
|
21
|
-
|
|
21
|
+
validateRootConfig(config);
|
|
22
22
|
for (const entry of config.workspacePatternConfigs ?? []) {
|
|
23
23
|
if (typeof entry.config !== "function") {
|
|
24
24
|
validateWorkspaceConfig(entry.config);
|
|
@@ -42,8 +42,4 @@ const resolveRootConfig = (config) => {
|
|
|
42
42
|
};
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
-
export {
|
|
46
|
-
createDefaultRootConfig,
|
|
47
|
-
resolveRootConfig,
|
|
48
|
-
rootConfig_validateRootConfig as validateRootConfig,
|
|
49
|
-
};
|
|
45
|
+
export { createDefaultRootConfig, resolveRootConfig, validateRootConfig };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// This file is generated by scripts/
|
|
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
|
});
|
|
@@ -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
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
144
|
+
return rootMatch ? [rootMatch, ...rest] : rest;
|
|
151
145
|
}
|
|
152
146
|
createScriptCommand(options) {
|
|
153
147
|
validateJSTypes(
|
|
@@ -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(
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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})`,
|
|
@@ -178,10 +178,12 @@ const findWorkspaces = ({
|
|
|
178
178
|
workspaceMap,
|
|
179
179
|
workspaceAliases,
|
|
180
180
|
workspacePatternConfigs,
|
|
181
|
+
rootWorkspace,
|
|
181
182
|
);
|
|
182
183
|
}
|
|
183
184
|
validateWorkspaceDependencyRules({
|
|
184
185
|
workspaceMap,
|
|
186
|
+
rootWorkspace,
|
|
185
187
|
});
|
|
186
188
|
validateWorkspaceAliases(workspaces, workspaceAliases, rootWorkspace.name);
|
|
187
189
|
logger.debug(
|
|
@@ -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
|
|
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
|
-
|
|
60
|
+
afterNegation.startsWith(target + WORKSPACE_PATTERN_SEPARATOR),
|
|
25
61
|
);
|
|
26
62
|
if (!target) {
|
|
27
63
|
return {
|
|
28
64
|
target: "default",
|
|
29
|
-
value:
|
|
65
|
+
value: afterNegation,
|
|
30
66
|
isNegated,
|
|
67
|
+
isRegex: false,
|
|
68
|
+
isRootSelector: false,
|
|
31
69
|
};
|
|
32
70
|
}
|
|
33
|
-
const
|
|
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
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
};
|