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.
- package/AGENTS.md +537 -0
- package/README.md +51 -13
- package/package.json +1 -1
- package/src/2392.mjs +184 -3
- package/src/5166.mjs +1 -0
- package/src/8529.mjs +10 -0
- package/src/affected/affectedBaseRef.mjs +12 -0
- package/src/affected/externalDependencyChanges.mjs +47 -0
- package/src/affected/fileAffectedWorkspaces.mjs +152 -54
- package/src/affected/gitAffectedFiles.mjs +44 -1
- package/src/affected/gitAffectedWorkspaces.mjs +73 -3
- package/src/affected/index.mjs +2 -0
- package/src/ai/mcp/serverState.mjs +1 -1
- package/src/cli/commands/commandHandlerUtils.mjs +12 -7
- package/src/cli/commands/commands.mjs +4 -1
- package/src/cli/commands/handleSimpleCommands.mjs +2 -2
- package/src/cli/commands/listAffected.mjs +184 -0
- package/src/cli/commands/runScript/handleRunAffected.mjs +99 -0
- package/src/cli/commands/runScript/handleRunScript.mjs +19 -202
- package/src/cli/commands/runScript/index.mjs +1 -0
- package/src/cli/commands/runScript/scriptRunFlow.mjs +213 -0
- package/src/cli/index.d.ts +749 -134
- package/src/config/public.d.ts +66 -2
- package/src/config/rootConfig/rootConfig.mjs +9 -9
- package/src/config/rootConfig/rootConfigSchema.mjs +3 -0
- package/src/config/workspaceConfig/mergeWorkspaceConfig.mjs +33 -19
- package/src/config/workspaceConfig/workspaceConfig.mjs +3 -0
- package/src/config/workspaceConfig/workspaceConfigSchema.mjs +26 -0
- package/src/index.d.ts +307 -5
- package/src/index.mjs +1 -0
- package/src/internal/bun/bunLock.mjs +33 -0
- package/src/internal/generated/aiDocs/docs.mjs +169 -9
- package/src/internal/generated/ajv/validateRootConfig.mjs +1 -1
- package/src/internal/generated/ajv/validateWorkspaceConfig.mjs +1 -1
- package/src/project/implementations/fileSystemProject/affectedWorkspaces.mjs +227 -0
- package/src/project/implementations/{fileSystemProject.mjs → fileSystemProject/fileSystemProject.mjs} +169 -12
- package/src/project/implementations/fileSystemProject/index.mjs +4 -0
- package/src/project/implementations/memoryProject.mjs +1 -0
- package/src/project/implementations/projectBase.mjs +11 -17
- package/src/project/index.mjs +1 -1
- package/src/rslib-runtime.mjs +0 -31
- package/src/workspaces/applyWorkspacePatternConfigs.mjs +16 -2
- package/src/workspaces/dependencyGraph/resolveDependencies.mjs +68 -18
- package/src/workspaces/dependencyGraph/validateDependencyRules.mjs +14 -7
- package/src/workspaces/findWorkspaces.mjs +3 -0
- package/src/workspaces/workspace.mjs +8 -2
- package/src/workspaces/workspacePattern.mjs +134 -46
package/src/2392.mjs
CHANGED
|
@@ -28,7 +28,8 @@ const CLI_COMMANDS_CONFIG = {
|
|
|
28
28
|
options: {
|
|
29
29
|
workspacePatterns: {
|
|
30
30
|
flags: ["-W", "--workspace-patterns <patterns>"],
|
|
31
|
-
description:
|
|
31
|
+
description:
|
|
32
|
+
"Workspace patterns to match, separated by whitespace. Use backslashes to escape spaces if needed.",
|
|
32
33
|
},
|
|
33
34
|
nameOnly: {
|
|
34
35
|
flags: ["-n", "--name-only"],
|
|
@@ -144,6 +145,79 @@ const CLI_COMMANDS_CONFIG = {
|
|
|
144
145
|
"Start the bun-workspaces MCP (Model Context Protocol) server over stdio",
|
|
145
146
|
options: {},
|
|
146
147
|
},
|
|
148
|
+
listAffected: {
|
|
149
|
+
command: "list-affected",
|
|
150
|
+
isGlobal: false,
|
|
151
|
+
aliases: ["ls-affected"],
|
|
152
|
+
description:
|
|
153
|
+
"List workspaces affected by a set of changed files (git or file list)",
|
|
154
|
+
options: {
|
|
155
|
+
base: {
|
|
156
|
+
flags: ["-B", "--base <ref>"],
|
|
157
|
+
description:
|
|
158
|
+
"Git base ref to diff against (default is main if not configured). Cannot be used with --files",
|
|
159
|
+
},
|
|
160
|
+
head: {
|
|
161
|
+
flags: ["-H", "--head <ref>"],
|
|
162
|
+
description:
|
|
163
|
+
"Git head ref to diff against (default: HEAD). Cannot be used with --files",
|
|
164
|
+
},
|
|
165
|
+
files: {
|
|
166
|
+
flags: ["-F", "--files <files>"],
|
|
167
|
+
description:
|
|
168
|
+
"Changed files (paths/dirs/globs, '!' to exclude), separated by spaces. Use backslashes to escape spaces if needed. Bypasses git, so cannot be used with --base or --head.",
|
|
169
|
+
},
|
|
170
|
+
script: {
|
|
171
|
+
flags: ["-S", "--script <script>"],
|
|
172
|
+
description: "Resolve inputs for the named script",
|
|
173
|
+
},
|
|
174
|
+
ignoreUntracked: {
|
|
175
|
+
flags: ["--ignore-untracked"],
|
|
176
|
+
description: "Exclude untracked files",
|
|
177
|
+
},
|
|
178
|
+
ignoreUnstaged: {
|
|
179
|
+
flags: ["--ignore-unstaged"],
|
|
180
|
+
description: "Exclude unstaged files",
|
|
181
|
+
},
|
|
182
|
+
ignoreStaged: {
|
|
183
|
+
flags: ["--ignore-staged"],
|
|
184
|
+
description: "Exclude staged files",
|
|
185
|
+
},
|
|
186
|
+
ignoreUncommitted: {
|
|
187
|
+
flags: ["--ignore-uncommitted"],
|
|
188
|
+
description:
|
|
189
|
+
"Exclude all uncommitted changes (staged, unstaged, untracked)",
|
|
190
|
+
},
|
|
191
|
+
ignoreWorkspaceDeps: {
|
|
192
|
+
flags: ["--ignore-workspace-deps"],
|
|
193
|
+
description:
|
|
194
|
+
"Ignore workspace dependencies derived from package.json files",
|
|
195
|
+
},
|
|
196
|
+
ignoreExternalDeps: {
|
|
197
|
+
flags: ["--ignore-external-deps"],
|
|
198
|
+
description:
|
|
199
|
+
"Ignore changes to external dependencies (e.g. npm packages) versions in bun.lock",
|
|
200
|
+
},
|
|
201
|
+
explain: {
|
|
202
|
+
flags: ["-e", "--explain"],
|
|
203
|
+
description:
|
|
204
|
+
"Include changed-file counts and dependency reasons. With --json, outputs the full result object",
|
|
205
|
+
},
|
|
206
|
+
detailed: {
|
|
207
|
+
flags: ["-D", "--detailed"],
|
|
208
|
+
description:
|
|
209
|
+
"With --explain, render full per-file data and dependency edge chains",
|
|
210
|
+
},
|
|
211
|
+
json: {
|
|
212
|
+
flags: JSON_FLAGS,
|
|
213
|
+
description: "Output as JSON",
|
|
214
|
+
},
|
|
215
|
+
pretty: {
|
|
216
|
+
flags: PRETTY_FLAGS,
|
|
217
|
+
description: "Pretty print JSON",
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
},
|
|
147
221
|
runScript: {
|
|
148
222
|
command: "run-script [script] [workspacePatterns...]",
|
|
149
223
|
isGlobal: false,
|
|
@@ -153,11 +227,118 @@ const CLI_COMMANDS_CONFIG = {
|
|
|
153
227
|
options: {
|
|
154
228
|
script: {
|
|
155
229
|
flags: ["-S", "--script <script>"],
|
|
156
|
-
description: "The script to run.",
|
|
230
|
+
description: "The script to run. (Alternative to positional argument)",
|
|
157
231
|
},
|
|
158
232
|
workspacePatterns: {
|
|
159
233
|
flags: ["-W", "--workspace-patterns <patterns>"],
|
|
160
|
-
description:
|
|
234
|
+
description:
|
|
235
|
+
"Workspace patterns to match, separated by spaces. (Alternative to positional arguments)",
|
|
236
|
+
},
|
|
237
|
+
parallel: {
|
|
238
|
+
flags: ["-P", "--parallel [max]"],
|
|
239
|
+
description:
|
|
240
|
+
'Run the scripts in parallel. Pass "false" for series, or a concurrency limit as a number, percentage ("50%"), "auto", "default", or"unbounded"',
|
|
241
|
+
},
|
|
242
|
+
args: {
|
|
243
|
+
flags: ["-a", "--args <args>"],
|
|
244
|
+
description: "Args to append to the script command",
|
|
245
|
+
},
|
|
246
|
+
outputStyle: {
|
|
247
|
+
flags: ["-o", "--output-style <style>"],
|
|
248
|
+
description: "The output style to use",
|
|
249
|
+
values: [...OUTPUT_STYLE_VALUES],
|
|
250
|
+
},
|
|
251
|
+
groupedLines: {
|
|
252
|
+
flags: ["-L", "--grouped-lines <count>"],
|
|
253
|
+
description: `With grouped output, the max preview lines (number or "auto", default "auto")`,
|
|
254
|
+
},
|
|
255
|
+
noPrefix: {
|
|
256
|
+
flags: ["-N", "--no-prefix"],
|
|
257
|
+
description: "(DEPRECATED) Use --output-style=plain instead",
|
|
258
|
+
deprecated: true,
|
|
259
|
+
},
|
|
260
|
+
inline: {
|
|
261
|
+
flags: ["-i", "--inline"],
|
|
262
|
+
description:
|
|
263
|
+
"Run the script as an inline command from the workspace directory",
|
|
264
|
+
},
|
|
265
|
+
inlineName: {
|
|
266
|
+
flags: ["-I", "--inline-name <name>"],
|
|
267
|
+
description: "An optional name for the script when --inline is passed",
|
|
268
|
+
},
|
|
269
|
+
shell: {
|
|
270
|
+
flags: ["-s", "--shell <shell>"],
|
|
271
|
+
values: [...SCRIPT_SHELL_OPTIONS, "default"],
|
|
272
|
+
description: `When using --inline, the shell to use to run the script`,
|
|
273
|
+
},
|
|
274
|
+
depOrder: {
|
|
275
|
+
flags: ["-d", "--dep-order"],
|
|
276
|
+
description:
|
|
277
|
+
"Scripts for dependent workspaces run only after their dependencies",
|
|
278
|
+
},
|
|
279
|
+
ignoreDepFailure: {
|
|
280
|
+
flags: ["-f", "--ignore-dep-failure"],
|
|
281
|
+
description:
|
|
282
|
+
"In dependency order, continue running scripts even if a dependency fails",
|
|
283
|
+
},
|
|
284
|
+
jsonOutfile: {
|
|
285
|
+
flags: ["-j", "--json-outfile <file>"],
|
|
286
|
+
description: "Output results in a JSON file",
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
runAffected: {
|
|
291
|
+
command: "run-affected [script]",
|
|
292
|
+
isGlobal: false,
|
|
293
|
+
aliases: [],
|
|
294
|
+
description:
|
|
295
|
+
"Run a script across the workspaces affected by a set of changed files (git or file list)",
|
|
296
|
+
options: {
|
|
297
|
+
script: {
|
|
298
|
+
flags: ["-S", "--script <script>"],
|
|
299
|
+
description: "The script to run. (Alternative to positional argument)",
|
|
300
|
+
},
|
|
301
|
+
base: {
|
|
302
|
+
flags: ["-B", "--base <ref>"],
|
|
303
|
+
description:
|
|
304
|
+
"Git base ref to diff against (default is main if not configured). Cannot be used with --files",
|
|
305
|
+
},
|
|
306
|
+
head: {
|
|
307
|
+
flags: ["-H", "--head <ref>"],
|
|
308
|
+
description:
|
|
309
|
+
'Git head ref to diff against (default "HEAD"). Cannot be used with --files',
|
|
310
|
+
},
|
|
311
|
+
files: {
|
|
312
|
+
flags: ["-F", "--files <files>"],
|
|
313
|
+
description:
|
|
314
|
+
"Changed files (paths/dirs/globs, '!' to exclude), separated by whitespace. Use backslashes to escape spaces if needed. Bypasses git, so cannot be used with --base or --head.",
|
|
315
|
+
},
|
|
316
|
+
ignoreUntracked: {
|
|
317
|
+
flags: ["--ignore-untracked"],
|
|
318
|
+
description: "Exclude untracked files",
|
|
319
|
+
},
|
|
320
|
+
ignoreUnstaged: {
|
|
321
|
+
flags: ["--ignore-unstaged"],
|
|
322
|
+
description: "Exclude unstaged files",
|
|
323
|
+
},
|
|
324
|
+
ignoreStaged: {
|
|
325
|
+
flags: ["--ignore-staged"],
|
|
326
|
+
description: "Exclude staged files",
|
|
327
|
+
},
|
|
328
|
+
ignoreUncommitted: {
|
|
329
|
+
flags: ["--ignore-uncommitted"],
|
|
330
|
+
description:
|
|
331
|
+
"Exclude all uncommitted changes (staged, unstaged, untracked)",
|
|
332
|
+
},
|
|
333
|
+
ignoreWorkspaceDeps: {
|
|
334
|
+
flags: ["--ignore-workspace-deps"],
|
|
335
|
+
description:
|
|
336
|
+
"Ignore workspace dependencies derived from package.json files",
|
|
337
|
+
},
|
|
338
|
+
ignoreExternalDeps: {
|
|
339
|
+
flags: ["--ignore-external-deps"],
|
|
340
|
+
description:
|
|
341
|
+
"Ignore changes to external dependencies (e.g. npm packages) versions in bun.lock",
|
|
161
342
|
},
|
|
162
343
|
parallel: {
|
|
163
344
|
flags: ["-P", "--parallel [max]"],
|
package/src/5166.mjs
CHANGED
|
@@ -2,6 +2,7 @@ const USER_ENV_VARS = {
|
|
|
2
2
|
parallelMaxDefault: "BW_PARALLEL_MAX_DEFAULT",
|
|
3
3
|
scriptShellDefault: "BW_SHELL_DEFAULT",
|
|
4
4
|
includeRootWorkspaceDefault: "BW_INCLUDE_ROOT_WORKSPACE_DEFAULT",
|
|
5
|
+
affectedBaseRefDefault: "BW_AFFECTED_BASE_REF_DEFAULT",
|
|
5
6
|
};
|
|
6
7
|
const getUserEnvVarName = (key) => USER_ENV_VARS[key];
|
|
7
8
|
|
package/src/8529.mjs
CHANGED
|
@@ -16,8 +16,18 @@ const CONFIG_LOCATION_PATHS = {
|
|
|
16
16
|
jsonFile: (name) => `${name}.json`,
|
|
17
17
|
packageJson: (_, packageJsonKey) => `package.json["${packageJsonKey}"]`,
|
|
18
18
|
};
|
|
19
|
+
const CONFIG_LOCATION_DESCRIPTIONS =
|
|
20
|
+
/* unused pure expression or super */ null && {
|
|
21
|
+
tsFile: "TypeScript file",
|
|
22
|
+
jsFile: "JavaScript file",
|
|
23
|
+
jsoncFile: "JSONC file",
|
|
24
|
+
jsonFile: "JSON file",
|
|
25
|
+
packageJson: "package.json key",
|
|
26
|
+
};
|
|
19
27
|
const createConfigLocationPath = (locationType, name, packageJsonKey) =>
|
|
20
28
|
CONFIG_LOCATION_PATHS[locationType](name, packageJsonKey);
|
|
29
|
+
const createConfigLocationDescription = (locationType) =>
|
|
30
|
+
CONFIG_LOCATION_DESCRIPTIONS[locationType];
|
|
21
31
|
|
|
22
32
|
export {
|
|
23
33
|
CONFIG_LOCATION_TYPES,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { getUserEnvVar } from "../config/userEnvVars/index.mjs";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_AFFECTED_BASE_REF = "main";
|
|
4
|
+
/**
|
|
5
|
+
* Resolves the default base ref for affected workspace resolution.
|
|
6
|
+
*
|
|
7
|
+
* Precedence: explicit value (typically from root config defaults) >
|
|
8
|
+
* `BW_AFFECTED_BASE_REF_DEFAULT` env var > `"main"`.
|
|
9
|
+
*/ const resolveDefaultAffectedBaseRef = (value) =>
|
|
10
|
+
value || getUserEnvVar("affectedBaseRefDefault") || DEFAULT_AFFECTED_BASE_REF;
|
|
11
|
+
|
|
12
|
+
export { DEFAULT_AFFECTED_BASE_REF, resolveDefaultAffectedBaseRef };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a dep's version for a specific workspace. bun.lock encodes
|
|
3
|
+
* divergent per-workspace resolutions under a `<workspaceName>/<depName>` key
|
|
4
|
+
* when the workspace's range can't dedupe with the hoisted version. Always
|
|
5
|
+
* consult that namespaced key first; fall back to the bare key for the
|
|
6
|
+
* common (hoisted) case.
|
|
7
|
+
*/ const resolveWorkspaceDepVersion = ({ lock, workspaceName, depName }) =>
|
|
8
|
+
lock.get(`${workspaceName}/${depName}`) ?? lock.get(depName) ?? null;
|
|
9
|
+
/**
|
|
10
|
+
* Given each workspace's declared external deps and lockfile version maps
|
|
11
|
+
* at base vs head, emit per-workspace change entries for any external dep
|
|
12
|
+
* whose resolved version differs (including added/removed).
|
|
13
|
+
*
|
|
14
|
+
* Pure function. No I/O.
|
|
15
|
+
*/ const computeExternalDependencyChanges = ({
|
|
16
|
+
workspaces,
|
|
17
|
+
baseLock,
|
|
18
|
+
headLock,
|
|
19
|
+
}) => {
|
|
20
|
+
const result = new Map();
|
|
21
|
+
for (const workspace of workspaces) {
|
|
22
|
+
const changes = [];
|
|
23
|
+
for (const { name, source } of workspace.externalDependencies) {
|
|
24
|
+
const baseVersion = resolveWorkspaceDepVersion({
|
|
25
|
+
lock: baseLock,
|
|
26
|
+
workspaceName: workspace.name,
|
|
27
|
+
depName: name,
|
|
28
|
+
});
|
|
29
|
+
const headVersion = resolveWorkspaceDepVersion({
|
|
30
|
+
lock: headLock,
|
|
31
|
+
workspaceName: workspace.name,
|
|
32
|
+
depName: name,
|
|
33
|
+
});
|
|
34
|
+
if (baseVersion === headVersion) continue;
|
|
35
|
+
changes.push({
|
|
36
|
+
name,
|
|
37
|
+
source,
|
|
38
|
+
baseVersion,
|
|
39
|
+
headVersion,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
if (changes.length) result.set(workspace.name, changes);
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export { computeExternalDependencyChanges };
|
|
@@ -8,10 +8,15 @@ const GLOB_CHARACTER_REGEX = /[*?[{]/;
|
|
|
8
8
|
const toPosixPath = (filePath) => filePath.replaceAll("\\", "/");
|
|
9
9
|
const stripTrailingSlashes = (filePath) => filePath.replace(/\/+$/, "");
|
|
10
10
|
const stripLeadingSlashes = (filePath) => filePath.replace(/^\/+/, "");
|
|
11
|
+
const stripDotSlashSegments = (filePath) => {
|
|
12
|
+
let stripped = filePath;
|
|
13
|
+
while (stripped.startsWith("./")) stripped = stripped.slice(2);
|
|
14
|
+
return stripped === "." ? "" : stripped;
|
|
15
|
+
};
|
|
11
16
|
const normalizeChangedFilePath = ({ rootDirectory, filePath }) => {
|
|
12
17
|
const posixFilePath = toPosixPath(filePath);
|
|
13
18
|
if (!path.isAbsolute(filePath)) {
|
|
14
|
-
return posixFilePath;
|
|
19
|
+
return stripDotSlashSegments(posixFilePath);
|
|
15
20
|
}
|
|
16
21
|
const posixRoot = stripTrailingSlashes(toPosixPath(rootDirectory));
|
|
17
22
|
if (posixFilePath === posixRoot) {
|
|
@@ -142,7 +147,10 @@ const matchChangedFilesForWorkspace = ({
|
|
|
142
147
|
}
|
|
143
148
|
return matchedFiles;
|
|
144
149
|
};
|
|
145
|
-
const resolveInputWorkspaceDependencies = ({
|
|
150
|
+
const resolveInputWorkspaceDependencies = ({
|
|
151
|
+
workspaceInputs,
|
|
152
|
+
rootWorkspace,
|
|
153
|
+
}) => {
|
|
146
154
|
const inputDependenciesByName = new Map();
|
|
147
155
|
const allWorkspaces = workspaceInputs.map(({ workspace }) => workspace);
|
|
148
156
|
for (const { workspace, inputWorkspacePatterns } of workspaceInputs) {
|
|
@@ -153,6 +161,7 @@ const resolveInputWorkspaceDependencies = ({ workspaceInputs }) => {
|
|
|
153
161
|
const matchedNames = matchWorkspacesByPatterns(
|
|
154
162
|
inputWorkspacePatterns,
|
|
155
163
|
allWorkspaces,
|
|
164
|
+
rootWorkspace,
|
|
156
165
|
)
|
|
157
166
|
.map((matchedWorkspace) => matchedWorkspace.name)
|
|
158
167
|
.filter((matchedName) => matchedName !== workspace.name);
|
|
@@ -160,12 +169,36 @@ const resolveInputWorkspaceDependencies = ({ workspaceInputs }) => {
|
|
|
160
169
|
}
|
|
161
170
|
return inputDependenciesByName;
|
|
162
171
|
};
|
|
172
|
+
const collectDirectEdges = ({
|
|
173
|
+
workspace,
|
|
174
|
+
inputDependenciesByName,
|
|
175
|
+
ignoreWorkspaceDependencies,
|
|
176
|
+
}) => {
|
|
177
|
+
const edges = [];
|
|
178
|
+
for (const dependencyName of inputDependenciesByName.get(workspace.name) ??
|
|
179
|
+
[]) {
|
|
180
|
+
edges.push({
|
|
181
|
+
dependencyName,
|
|
182
|
+
edgeSource: "input",
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
if (!ignoreWorkspaceDependencies) {
|
|
186
|
+
for (const dependencyName of workspace.dependencies) {
|
|
187
|
+
edges.push({
|
|
188
|
+
dependencyName,
|
|
189
|
+
edgeSource: "package",
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return edges;
|
|
194
|
+
};
|
|
163
195
|
const computeAffectedWorkspaceSet = ({
|
|
164
196
|
workspaceInputs,
|
|
165
197
|
workspaceByName,
|
|
166
198
|
changedFilesByName,
|
|
199
|
+
externalDepChangesByWorkspace,
|
|
167
200
|
inputDependenciesByName,
|
|
168
|
-
|
|
201
|
+
ignoreWorkspaceDependencies,
|
|
169
202
|
}) => {
|
|
170
203
|
const inputDependentsByName = new Map();
|
|
171
204
|
for (const [workspaceName, dependencyNames] of inputDependenciesByName) {
|
|
@@ -181,7 +214,11 @@ const computeAffectedWorkspaceSet = ({
|
|
|
181
214
|
const affected = new Set();
|
|
182
215
|
const queue = [];
|
|
183
216
|
for (const { workspace } of workspaceInputs) {
|
|
184
|
-
|
|
217
|
+
const hasChangedFiles =
|
|
218
|
+
(changedFilesByName.get(workspace.name)?.length ?? 0) > 0;
|
|
219
|
+
const hasExternalDepChanges =
|
|
220
|
+
(externalDepChangesByWorkspace.get(workspace.name)?.length ?? 0) > 0;
|
|
221
|
+
if (hasChangedFiles || hasExternalDepChanges) {
|
|
185
222
|
affected.add(workspace.name);
|
|
186
223
|
queue.push(workspace.name);
|
|
187
224
|
}
|
|
@@ -191,7 +228,7 @@ const computeAffectedWorkspaceSet = ({
|
|
|
191
228
|
const currentWorkspace = workspaceByName.get(currentName);
|
|
192
229
|
const dependents = [
|
|
193
230
|
...(inputDependentsByName.get(currentName) ?? []),
|
|
194
|
-
...(!
|
|
231
|
+
...(!ignoreWorkspaceDependencies && currentWorkspace
|
|
195
232
|
? currentWorkspace.dependents
|
|
196
233
|
: []),
|
|
197
234
|
];
|
|
@@ -204,66 +241,118 @@ const computeAffectedWorkspaceSet = ({
|
|
|
204
241
|
}
|
|
205
242
|
return affected;
|
|
206
243
|
};
|
|
244
|
+
/**
|
|
245
|
+
* Walk forward from `directDependencyName` through the affected dep graph,
|
|
246
|
+
* appending each next affected dep edge to the chain until we run out of
|
|
247
|
+
* affected dep edges to follow. Stops on:
|
|
248
|
+
* - no further affected dep edges,
|
|
249
|
+
* - revisiting a workspace already in the chain (cycle).
|
|
250
|
+
*
|
|
251
|
+
* Branching is broken deterministically by edge insertion order
|
|
252
|
+
* (input edges before package edges, declaration order within each).
|
|
253
|
+
*/ const extendChainThroughAffectedDeps = ({
|
|
254
|
+
startingWorkspaceName,
|
|
255
|
+
directDependencyName,
|
|
256
|
+
directEdgeSource,
|
|
257
|
+
workspaceByName,
|
|
258
|
+
inputDependenciesByName,
|
|
259
|
+
affectedSet,
|
|
260
|
+
ignoreWorkspaceDependencies,
|
|
261
|
+
}) => {
|
|
262
|
+
const chain = [
|
|
263
|
+
{
|
|
264
|
+
workspaceName: startingWorkspaceName,
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
workspaceName: directDependencyName,
|
|
268
|
+
edgeSource: directEdgeSource,
|
|
269
|
+
},
|
|
270
|
+
];
|
|
271
|
+
const visited = new Set([startingWorkspaceName, directDependencyName]);
|
|
272
|
+
let currentName = directDependencyName;
|
|
273
|
+
while (true) {
|
|
274
|
+
const currentWorkspace = workspaceByName.get(currentName);
|
|
275
|
+
if (!currentWorkspace) break;
|
|
276
|
+
const nextEdge = collectDirectEdges({
|
|
277
|
+
workspace: currentWorkspace,
|
|
278
|
+
inputDependenciesByName,
|
|
279
|
+
ignoreWorkspaceDependencies,
|
|
280
|
+
}).find(
|
|
281
|
+
({ dependencyName }) =>
|
|
282
|
+
!visited.has(dependencyName) &&
|
|
283
|
+
workspaceByName.has(dependencyName) &&
|
|
284
|
+
affectedSet.has(dependencyName),
|
|
285
|
+
);
|
|
286
|
+
if (!nextEdge) break;
|
|
287
|
+
chain.push({
|
|
288
|
+
workspaceName: nextEdge.dependencyName,
|
|
289
|
+
edgeSource: nextEdge.edgeSource,
|
|
290
|
+
});
|
|
291
|
+
visited.add(nextEdge.dependencyName);
|
|
292
|
+
currentName = nextEdge.dependencyName;
|
|
293
|
+
}
|
|
294
|
+
return chain;
|
|
295
|
+
};
|
|
207
296
|
const collectAffectedDependencies = ({
|
|
208
297
|
startingWorkspace,
|
|
209
298
|
workspaceByName,
|
|
210
299
|
inputDependenciesByName,
|
|
211
300
|
affectedSet,
|
|
212
|
-
|
|
301
|
+
ignoreWorkspaceDependencies,
|
|
213
302
|
}) => {
|
|
214
303
|
const results = [];
|
|
215
|
-
const
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const dependencyChain = [
|
|
240
|
-
...chain,
|
|
241
|
-
{
|
|
242
|
-
workspaceName: dependencyName,
|
|
243
|
-
edgeSource,
|
|
244
|
-
},
|
|
245
|
-
];
|
|
246
|
-
if (affectedSet.has(dependencyName)) {
|
|
247
|
-
results.push({
|
|
248
|
-
dependencyName,
|
|
249
|
-
chain: dependencyChain,
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
visit(dependencyName, dependencyChain);
|
|
253
|
-
}
|
|
254
|
-
};
|
|
255
|
-
visit(startingWorkspace.name, [
|
|
256
|
-
{
|
|
257
|
-
workspaceName: startingWorkspace.name,
|
|
258
|
-
},
|
|
259
|
-
]);
|
|
304
|
+
const seen = new Set([startingWorkspace.name]);
|
|
305
|
+
const directEdges = collectDirectEdges({
|
|
306
|
+
workspace: startingWorkspace,
|
|
307
|
+
inputDependenciesByName,
|
|
308
|
+
ignoreWorkspaceDependencies,
|
|
309
|
+
});
|
|
310
|
+
for (const { dependencyName, edgeSource } of directEdges) {
|
|
311
|
+
if (seen.has(dependencyName)) continue;
|
|
312
|
+
if (!workspaceByName.has(dependencyName)) continue;
|
|
313
|
+
seen.add(dependencyName);
|
|
314
|
+
if (!affectedSet.has(dependencyName)) continue;
|
|
315
|
+
results.push({
|
|
316
|
+
dependencyName,
|
|
317
|
+
chain: extendChainThroughAffectedDeps({
|
|
318
|
+
startingWorkspaceName: startingWorkspace.name,
|
|
319
|
+
directDependencyName: dependencyName,
|
|
320
|
+
directEdgeSource: edgeSource,
|
|
321
|
+
workspaceByName,
|
|
322
|
+
inputDependenciesByName,
|
|
323
|
+
affectedSet,
|
|
324
|
+
ignoreWorkspaceDependencies,
|
|
325
|
+
}),
|
|
326
|
+
});
|
|
327
|
+
}
|
|
260
328
|
return results;
|
|
261
329
|
};
|
|
330
|
+
const filterExternalDepChangesByInputs = ({
|
|
331
|
+
changesByWorkspace,
|
|
332
|
+
workspaceInputs,
|
|
333
|
+
}) => {
|
|
334
|
+
const filtered = new Map();
|
|
335
|
+
for (const { workspace, inputExternalDependencyNames } of workspaceInputs) {
|
|
336
|
+
const changes = changesByWorkspace.get(workspace.name);
|
|
337
|
+
if (!changes?.length) continue;
|
|
338
|
+
if (inputExternalDependencyNames === undefined) {
|
|
339
|
+
filtered.set(workspace.name, changes);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (inputExternalDependencyNames.length === 0) continue;
|
|
343
|
+
const allowed = new Set(inputExternalDependencyNames);
|
|
344
|
+
const matched = changes.filter((change) => allowed.has(change.name));
|
|
345
|
+
if (matched.length) filtered.set(workspace.name, matched);
|
|
346
|
+
}
|
|
347
|
+
return filtered;
|
|
348
|
+
};
|
|
262
349
|
const getFileAffectedWorkspaces = async ({
|
|
263
350
|
rootDirectory,
|
|
264
351
|
workspaceInputs,
|
|
265
352
|
changedFilePaths,
|
|
266
|
-
|
|
353
|
+
rootWorkspace,
|
|
354
|
+
externalDepChangesByWorkspace = new Map(),
|
|
355
|
+
ignoreWorkspaceDependencies = false,
|
|
267
356
|
}) => {
|
|
268
357
|
const normalizedChangedFilePaths = changedFilePaths.map((filePath) =>
|
|
269
358
|
normalizeChangedFilePath({
|
|
@@ -285,24 +374,32 @@ const getFileAffectedWorkspaces = async ({
|
|
|
285
374
|
}),
|
|
286
375
|
);
|
|
287
376
|
}
|
|
377
|
+
const filteredExternalDepChanges = filterExternalDepChangesByInputs({
|
|
378
|
+
changesByWorkspace: externalDepChangesByWorkspace,
|
|
379
|
+
workspaceInputs,
|
|
380
|
+
});
|
|
288
381
|
const inputDependenciesByName = resolveInputWorkspaceDependencies({
|
|
289
382
|
workspaceInputs,
|
|
383
|
+
rootWorkspace,
|
|
290
384
|
});
|
|
291
385
|
const affectedSet = computeAffectedWorkspaceSet({
|
|
292
386
|
workspaceInputs,
|
|
293
387
|
workspaceByName,
|
|
294
388
|
changedFilesByName,
|
|
389
|
+
externalDepChangesByWorkspace: filteredExternalDepChanges,
|
|
295
390
|
inputDependenciesByName,
|
|
296
|
-
|
|
391
|
+
ignoreWorkspaceDependencies,
|
|
297
392
|
});
|
|
298
393
|
const affectedWorkspaces = workspaceInputs.map(({ workspace }) => {
|
|
299
394
|
const changedFiles = changedFilesByName.get(workspace.name) ?? [];
|
|
395
|
+
const externalDependencies =
|
|
396
|
+
filteredExternalDepChanges.get(workspace.name) ?? [];
|
|
300
397
|
const dependencies = collectAffectedDependencies({
|
|
301
398
|
startingWorkspace: workspace,
|
|
302
399
|
workspaceByName,
|
|
303
400
|
inputDependenciesByName,
|
|
304
401
|
affectedSet,
|
|
305
|
-
|
|
402
|
+
ignoreWorkspaceDependencies,
|
|
306
403
|
});
|
|
307
404
|
return {
|
|
308
405
|
workspace,
|
|
@@ -310,6 +407,7 @@ const getFileAffectedWorkspaces = async ({
|
|
|
310
407
|
affectedReasons: {
|
|
311
408
|
changedFiles,
|
|
312
409
|
dependencies,
|
|
410
|
+
externalDependencies,
|
|
313
411
|
},
|
|
314
412
|
};
|
|
315
413
|
});
|
|
@@ -52,6 +52,38 @@ const resolveGitRoot = async (rootDirectory) => {
|
|
|
52
52
|
}
|
|
53
53
|
return result.stdout.trim();
|
|
54
54
|
};
|
|
55
|
+
/**
|
|
56
|
+
* Read a project-root-relative file's contents at a specific git ref via
|
|
57
|
+
* `git show <ref>:<repo-relative-path>`. Returns `null` if the file does not
|
|
58
|
+
* exist at that ref (e.g. it was added later). Throws on other git errors.
|
|
59
|
+
*/ const readProjectFileAtGitRef = async ({
|
|
60
|
+
rootDirectory,
|
|
61
|
+
ref,
|
|
62
|
+
projectRelativePath,
|
|
63
|
+
}) => {
|
|
64
|
+
const gitRoot = fs.realpathSync.native(
|
|
65
|
+
path.resolve(await resolveGitRoot(rootDirectory)),
|
|
66
|
+
);
|
|
67
|
+
const absoluteProjectRoot = fs.realpathSync.native(
|
|
68
|
+
path.resolve(rootDirectory),
|
|
69
|
+
);
|
|
70
|
+
const absoluteFile = path.resolve(absoluteProjectRoot, projectRelativePath);
|
|
71
|
+
const repoRelative = path
|
|
72
|
+
.relative(gitRoot, absoluteFile)
|
|
73
|
+
.split(path.sep)
|
|
74
|
+
.join("/");
|
|
75
|
+
const result = await runGit(["show", `${ref}:${repoRelative}`], gitRoot);
|
|
76
|
+
if (result.exitCode === 0) return result.stdout;
|
|
77
|
+
if (
|
|
78
|
+
result.stderr.includes("does not exist") ||
|
|
79
|
+
result.stderr.includes("exists on disk, but not in")
|
|
80
|
+
) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
throw new GIT_AFFECTED_ERRORS.GitCommandFailed(
|
|
84
|
+
`git show ${ref}:${repoRelative} failed (exit ${result.exitCode}): ${result.stderr.trim()}`,
|
|
85
|
+
);
|
|
86
|
+
};
|
|
55
87
|
const toProjectFilePath = ({
|
|
56
88
|
gitRoot,
|
|
57
89
|
absoluteProjectRoot,
|
|
@@ -83,6 +115,10 @@ const getGitAffectedFiles = async (options) => {
|
|
|
83
115
|
const includeStaged = !ignoreUncommitted && !ignoreStaged;
|
|
84
116
|
const includeUnstaged = !ignoreUncommitted && !ignoreUnstaged;
|
|
85
117
|
const includeUntracked = !ignoreUncommitted && !ignoreUntracked;
|
|
118
|
+
const [baseSha, headSha] = await Promise.all([
|
|
119
|
+
runGitOrThrow(["rev-parse", baseRef], gitRoot).then((out) => out.trim()),
|
|
120
|
+
runGitOrThrow(["rev-parse", headRef], gitRoot).then((out) => out.trim()),
|
|
121
|
+
]);
|
|
86
122
|
const collectors = [
|
|
87
123
|
runGitOrThrow(
|
|
88
124
|
["diff", "--name-only", "-z", baseRef, headRef],
|
|
@@ -147,7 +183,14 @@ const getGitAffectedFiles = async (options) => {
|
|
|
147
183
|
.sort((a, b) => a.projectFilePath.localeCompare(b.projectFilePath));
|
|
148
184
|
return {
|
|
149
185
|
files,
|
|
186
|
+
baseSha,
|
|
187
|
+
headSha,
|
|
150
188
|
};
|
|
151
189
|
};
|
|
152
190
|
|
|
153
|
-
export {
|
|
191
|
+
export {
|
|
192
|
+
GIT_AFFECTED_ERRORS,
|
|
193
|
+
GIT_AFFECTED_FILE_REASONS,
|
|
194
|
+
getGitAffectedFiles,
|
|
195
|
+
readProjectFileAtGitRef,
|
|
196
|
+
};
|