bun-workspaces 1.8.1 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +51 -13
  2. package/package.json +1 -1
  3. package/src/2392.mjs +184 -3
  4. package/src/5166.mjs +1 -0
  5. package/src/8529.mjs +10 -0
  6. package/src/affected/affectedBaseRef.mjs +12 -0
  7. package/src/affected/externalDependencyChanges.mjs +47 -0
  8. package/src/affected/fileAffectedWorkspaces.mjs +413 -0
  9. package/src/affected/gitAffectedFiles.mjs +196 -0
  10. package/src/affected/gitAffectedWorkspaces.mjs +108 -0
  11. package/src/affected/index.mjs +7 -0
  12. package/src/ai/mcp/serverState.mjs +1 -1
  13. package/src/cli/commands/commandHandlerUtils.mjs +12 -7
  14. package/src/cli/commands/commands.mjs +4 -1
  15. package/src/cli/commands/handleSimpleCommands.mjs +2 -2
  16. package/src/cli/commands/listAffected.mjs +184 -0
  17. package/src/cli/commands/runScript/handleRunAffected.mjs +99 -0
  18. package/src/cli/commands/runScript/handleRunScript.mjs +19 -202
  19. package/src/cli/commands/runScript/index.mjs +1 -0
  20. package/src/cli/commands/runScript/output/renderGroupedOutput.mjs +22 -7
  21. package/src/cli/commands/runScript/output/tuiTerminal.mjs +45 -0
  22. package/src/cli/commands/runScript/scriptRunFlow.mjs +213 -0
  23. package/src/cli/index.d.ts +749 -134
  24. package/src/config/public.d.ts +66 -2
  25. package/src/config/rootConfig/rootConfig.mjs +4 -0
  26. package/src/config/rootConfig/rootConfigSchema.mjs +3 -0
  27. package/src/config/workspaceConfig/mergeWorkspaceConfig.mjs +33 -19
  28. package/src/config/workspaceConfig/workspaceConfig.mjs +3 -0
  29. package/src/config/workspaceConfig/workspaceConfigSchema.mjs +26 -0
  30. package/src/index.d.ts +307 -5
  31. package/src/index.mjs +1 -0
  32. package/src/internal/bun/bunLock.mjs +33 -0
  33. package/src/internal/generated/aiDocs/docs.mjs +152 -3
  34. package/src/internal/generated/ajv/validateRootConfig.mjs +1 -1
  35. package/src/internal/generated/ajv/validateWorkspaceConfig.mjs +1 -1
  36. package/src/project/implementations/fileSystemProject/affectedWorkspaces.mjs +225 -0
  37. package/src/project/implementations/{fileSystemProject.mjs → fileSystemProject/fileSystemProject.mjs} +169 -12
  38. package/src/project/implementations/fileSystemProject/index.mjs +4 -0
  39. package/src/project/implementations/memoryProject.mjs +1 -0
  40. package/src/project/index.mjs +1 -1
  41. package/src/rslib-runtime.mjs +0 -31
  42. package/src/workspaces/applyWorkspacePatternConfigs.mjs +10 -1
  43. package/src/workspaces/dependencyGraph/resolveDependencies.mjs +68 -18
  44. package/src/workspaces/findWorkspaces.mjs +1 -0
  45. package/src/workspaces/index.mjs +1 -0
  46. package/src/workspaces/workspace.mjs +8 -2
  47. package/src/workspaces/workspacePattern.mjs +13 -3
@@ -0,0 +1,413 @@
1
+ import path from "path";
2
+ import bun from "bun";
3
+ import { logger } from "../internal/logger/index.mjs";
4
+ import { matchWorkspacesByPatterns } from "../workspaces/index.mjs";
5
+
6
+ const FILE_PATTERN_NEGATION_PREFIX = "!";
7
+ const GLOB_CHARACTER_REGEX = /[*?[{]/;
8
+ const toPosixPath = (filePath) => filePath.replaceAll("\\", "/");
9
+ const stripTrailingSlashes = (filePath) => filePath.replace(/\/+$/, "");
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
+ };
16
+ const normalizeChangedFilePath = ({ rootDirectory, filePath }) => {
17
+ const posixFilePath = toPosixPath(filePath);
18
+ if (!path.isAbsolute(filePath)) {
19
+ return stripDotSlashSegments(posixFilePath);
20
+ }
21
+ const posixRoot = stripTrailingSlashes(toPosixPath(rootDirectory));
22
+ if (posixFilePath === posixRoot) {
23
+ return "";
24
+ }
25
+ if (posixRoot && posixFilePath.startsWith(`${posixRoot}/`)) {
26
+ return posixFilePath.slice(posixRoot.length + 1);
27
+ }
28
+ return posixFilePath;
29
+ };
30
+ const PROJECT_RELATIVE_PREFIX = "/";
31
+ const PARENT_SEGMENT = "..";
32
+ const resolveInputPattern = ({ workspacePath, inputPattern }) => {
33
+ const posixPattern = toPosixPath(inputPattern);
34
+ let rawJoined;
35
+ if (posixPattern.startsWith(PROJECT_RELATIVE_PREFIX)) {
36
+ rawJoined = stripLeadingSlashes(posixPattern);
37
+ } else {
38
+ const normalizedWorkspacePath = stripTrailingSlashes(
39
+ toPosixPath(workspacePath),
40
+ );
41
+ const stripped = stripTrailingSlashes(posixPattern);
42
+ if (!normalizedWorkspacePath || normalizedWorkspacePath === ".") {
43
+ rawJoined = stripped;
44
+ } else if (!stripped || stripped === ".") {
45
+ rawJoined = normalizedWorkspacePath;
46
+ } else {
47
+ rawJoined = `${normalizedWorkspacePath}/${stripped}`;
48
+ }
49
+ }
50
+ if (!rawJoined) return "";
51
+ const normalized = path.posix.normalize(rawJoined);
52
+ if (normalized === ".") return "";
53
+ return stripTrailingSlashes(normalized);
54
+ };
55
+ const isPatternOutsideProject = (resolvedPattern) =>
56
+ resolvedPattern === PARENT_SEGMENT ||
57
+ resolvedPattern.startsWith(`${PARENT_SEGMENT}/`);
58
+ const matchesResolvedPattern = ({ filePath, resolvedPattern }) => {
59
+ if (!resolvedPattern) {
60
+ return true;
61
+ }
62
+ if (GLOB_CHARACTER_REGEX.test(resolvedPattern)) {
63
+ return new bun.Glob(resolvedPattern).match(filePath);
64
+ }
65
+ return (
66
+ filePath === resolvedPattern || filePath.startsWith(`${resolvedPattern}/`)
67
+ );
68
+ };
69
+ const splitFilePatterns = (patterns) => {
70
+ const includes = [];
71
+ const excludes = [];
72
+ for (const pattern of patterns) {
73
+ if (pattern.startsWith(FILE_PATTERN_NEGATION_PREFIX)) {
74
+ excludes.push(pattern.slice(FILE_PATTERN_NEGATION_PREFIX.length));
75
+ } else {
76
+ includes.push(pattern);
77
+ }
78
+ }
79
+ return {
80
+ includes,
81
+ excludes,
82
+ };
83
+ };
84
+ const resolveFilePatterns = ({ workspace, patterns, isExclude }) => {
85
+ const resolved = [];
86
+ for (const inputPattern of patterns) {
87
+ const resolvedPattern = resolveInputPattern({
88
+ workspacePath: workspace.path,
89
+ inputPattern,
90
+ });
91
+ if (isPatternOutsideProject(resolvedPattern)) {
92
+ const displayPattern = isExclude
93
+ ? `${FILE_PATTERN_NEGATION_PREFIX}${inputPattern}`
94
+ : inputPattern;
95
+ logger.warn(
96
+ `Input pattern ${JSON.stringify(displayPattern)} for workspace "${workspace.name}" resolves outside the project root and will be ignored.`,
97
+ );
98
+ continue;
99
+ }
100
+ resolved.push({
101
+ inputPattern,
102
+ resolvedPattern,
103
+ });
104
+ }
105
+ return resolved;
106
+ };
107
+ const matchChangedFilesForWorkspace = ({
108
+ workspace,
109
+ inputFilePatterns,
110
+ changedFilePaths,
111
+ }) => {
112
+ const { includes, excludes } = splitFilePatterns(inputFilePatterns);
113
+ const resolvedIncludes = resolveFilePatterns({
114
+ workspace,
115
+ patterns: includes,
116
+ isExclude: false,
117
+ });
118
+ const resolvedExcludes = resolveFilePatterns({
119
+ workspace,
120
+ patterns: excludes,
121
+ isExclude: true,
122
+ });
123
+ const matchedFiles = [];
124
+ const matchedFilePaths = new Set();
125
+ for (const filePath of changedFilePaths) {
126
+ if (matchedFilePaths.has(filePath)) continue;
127
+ const matchingInclude = resolvedIncludes.find(({ resolvedPattern }) =>
128
+ matchesResolvedPattern({
129
+ filePath,
130
+ resolvedPattern,
131
+ }),
132
+ );
133
+ if (!matchingInclude) continue;
134
+ const isExcluded = resolvedExcludes.some(({ resolvedPattern }) =>
135
+ matchesResolvedPattern({
136
+ filePath,
137
+ resolvedPattern,
138
+ }),
139
+ );
140
+ if (isExcluded) continue;
141
+ matchedFiles.push({
142
+ filePath,
143
+ fileMetadata: undefined,
144
+ inputPattern: matchingInclude.inputPattern,
145
+ });
146
+ matchedFilePaths.add(filePath);
147
+ }
148
+ return matchedFiles;
149
+ };
150
+ const resolveInputWorkspaceDependencies = ({ workspaceInputs }) => {
151
+ const inputDependenciesByName = new Map();
152
+ const allWorkspaces = workspaceInputs.map(({ workspace }) => workspace);
153
+ for (const { workspace, inputWorkspacePatterns } of workspaceInputs) {
154
+ if (inputWorkspacePatterns.length === 0) {
155
+ inputDependenciesByName.set(workspace.name, []);
156
+ continue;
157
+ }
158
+ const matchedNames = matchWorkspacesByPatterns(
159
+ inputWorkspacePatterns,
160
+ allWorkspaces,
161
+ )
162
+ .map((matchedWorkspace) => matchedWorkspace.name)
163
+ .filter((matchedName) => matchedName !== workspace.name);
164
+ inputDependenciesByName.set(workspace.name, matchedNames);
165
+ }
166
+ return inputDependenciesByName;
167
+ };
168
+ const collectDirectEdges = ({
169
+ workspace,
170
+ inputDependenciesByName,
171
+ ignoreWorkspaceDependencies,
172
+ }) => {
173
+ const edges = [];
174
+ for (const dependencyName of inputDependenciesByName.get(workspace.name) ??
175
+ []) {
176
+ edges.push({
177
+ dependencyName,
178
+ edgeSource: "input",
179
+ });
180
+ }
181
+ if (!ignoreWorkspaceDependencies) {
182
+ for (const dependencyName of workspace.dependencies) {
183
+ edges.push({
184
+ dependencyName,
185
+ edgeSource: "package",
186
+ });
187
+ }
188
+ }
189
+ return edges;
190
+ };
191
+ const computeAffectedWorkspaceSet = ({
192
+ workspaceInputs,
193
+ workspaceByName,
194
+ changedFilesByName,
195
+ externalDepChangesByWorkspace,
196
+ inputDependenciesByName,
197
+ ignoreWorkspaceDependencies,
198
+ }) => {
199
+ const inputDependentsByName = new Map();
200
+ for (const [workspaceName, dependencyNames] of inputDependenciesByName) {
201
+ for (const dependencyName of dependencyNames) {
202
+ const existing = inputDependentsByName.get(dependencyName);
203
+ if (existing) {
204
+ existing.push(workspaceName);
205
+ } else {
206
+ inputDependentsByName.set(dependencyName, [workspaceName]);
207
+ }
208
+ }
209
+ }
210
+ const affected = new Set();
211
+ const queue = [];
212
+ for (const { workspace } of workspaceInputs) {
213
+ const hasChangedFiles =
214
+ (changedFilesByName.get(workspace.name)?.length ?? 0) > 0;
215
+ const hasExternalDepChanges =
216
+ (externalDepChangesByWorkspace.get(workspace.name)?.length ?? 0) > 0;
217
+ if (hasChangedFiles || hasExternalDepChanges) {
218
+ affected.add(workspace.name);
219
+ queue.push(workspace.name);
220
+ }
221
+ }
222
+ while (queue.length > 0) {
223
+ const currentName = queue.shift();
224
+ const currentWorkspace = workspaceByName.get(currentName);
225
+ const dependents = [
226
+ ...(inputDependentsByName.get(currentName) ?? []),
227
+ ...(!ignoreWorkspaceDependencies && currentWorkspace
228
+ ? currentWorkspace.dependents
229
+ : []),
230
+ ];
231
+ for (const dependentName of dependents) {
232
+ if (!workspaceByName.has(dependentName)) continue;
233
+ if (affected.has(dependentName)) continue;
234
+ affected.add(dependentName);
235
+ queue.push(dependentName);
236
+ }
237
+ }
238
+ return affected;
239
+ };
240
+ /**
241
+ * Walk forward from `directDependencyName` through the affected dep graph,
242
+ * appending each next affected dep edge to the chain until we run out of
243
+ * affected dep edges to follow. Stops on:
244
+ * - no further affected dep edges,
245
+ * - revisiting a workspace already in the chain (cycle).
246
+ *
247
+ * Branching is broken deterministically by edge insertion order
248
+ * (input edges before package edges, declaration order within each).
249
+ */ const extendChainThroughAffectedDeps = ({
250
+ startingWorkspaceName,
251
+ directDependencyName,
252
+ directEdgeSource,
253
+ workspaceByName,
254
+ inputDependenciesByName,
255
+ affectedSet,
256
+ ignoreWorkspaceDependencies,
257
+ }) => {
258
+ const chain = [
259
+ {
260
+ workspaceName: startingWorkspaceName,
261
+ },
262
+ {
263
+ workspaceName: directDependencyName,
264
+ edgeSource: directEdgeSource,
265
+ },
266
+ ];
267
+ const visited = new Set([startingWorkspaceName, directDependencyName]);
268
+ let currentName = directDependencyName;
269
+ while (true) {
270
+ const currentWorkspace = workspaceByName.get(currentName);
271
+ if (!currentWorkspace) break;
272
+ const nextEdge = collectDirectEdges({
273
+ workspace: currentWorkspace,
274
+ inputDependenciesByName,
275
+ ignoreWorkspaceDependencies,
276
+ }).find(
277
+ ({ dependencyName }) =>
278
+ !visited.has(dependencyName) &&
279
+ workspaceByName.has(dependencyName) &&
280
+ affectedSet.has(dependencyName),
281
+ );
282
+ if (!nextEdge) break;
283
+ chain.push({
284
+ workspaceName: nextEdge.dependencyName,
285
+ edgeSource: nextEdge.edgeSource,
286
+ });
287
+ visited.add(nextEdge.dependencyName);
288
+ currentName = nextEdge.dependencyName;
289
+ }
290
+ return chain;
291
+ };
292
+ const collectAffectedDependencies = ({
293
+ startingWorkspace,
294
+ workspaceByName,
295
+ inputDependenciesByName,
296
+ affectedSet,
297
+ ignoreWorkspaceDependencies,
298
+ }) => {
299
+ const results = [];
300
+ const seen = new Set([startingWorkspace.name]);
301
+ const directEdges = collectDirectEdges({
302
+ workspace: startingWorkspace,
303
+ inputDependenciesByName,
304
+ ignoreWorkspaceDependencies,
305
+ });
306
+ for (const { dependencyName, edgeSource } of directEdges) {
307
+ if (seen.has(dependencyName)) continue;
308
+ if (!workspaceByName.has(dependencyName)) continue;
309
+ seen.add(dependencyName);
310
+ if (!affectedSet.has(dependencyName)) continue;
311
+ results.push({
312
+ dependencyName,
313
+ chain: extendChainThroughAffectedDeps({
314
+ startingWorkspaceName: startingWorkspace.name,
315
+ directDependencyName: dependencyName,
316
+ directEdgeSource: edgeSource,
317
+ workspaceByName,
318
+ inputDependenciesByName,
319
+ affectedSet,
320
+ ignoreWorkspaceDependencies,
321
+ }),
322
+ });
323
+ }
324
+ return results;
325
+ };
326
+ const filterExternalDepChangesByInputs = ({
327
+ changesByWorkspace,
328
+ workspaceInputs,
329
+ }) => {
330
+ const filtered = new Map();
331
+ for (const { workspace, inputExternalDependencyNames } of workspaceInputs) {
332
+ const changes = changesByWorkspace.get(workspace.name);
333
+ if (!changes?.length) continue;
334
+ if (inputExternalDependencyNames === undefined) {
335
+ filtered.set(workspace.name, changes);
336
+ continue;
337
+ }
338
+ if (inputExternalDependencyNames.length === 0) continue;
339
+ const allowed = new Set(inputExternalDependencyNames);
340
+ const matched = changes.filter((change) => allowed.has(change.name));
341
+ if (matched.length) filtered.set(workspace.name, matched);
342
+ }
343
+ return filtered;
344
+ };
345
+ const getFileAffectedWorkspaces = async ({
346
+ rootDirectory,
347
+ workspaceInputs,
348
+ changedFilePaths,
349
+ externalDepChangesByWorkspace = new Map(),
350
+ ignoreWorkspaceDependencies = false,
351
+ }) => {
352
+ const normalizedChangedFilePaths = changedFilePaths.map((filePath) =>
353
+ normalizeChangedFilePath({
354
+ rootDirectory,
355
+ filePath,
356
+ }),
357
+ );
358
+ const workspaceByName = new Map(
359
+ workspaceInputs.map(({ workspace }) => [workspace.name, workspace]),
360
+ );
361
+ const changedFilesByName = new Map();
362
+ for (const { workspace, inputFilePatterns } of workspaceInputs) {
363
+ changedFilesByName.set(
364
+ workspace.name,
365
+ matchChangedFilesForWorkspace({
366
+ workspace,
367
+ inputFilePatterns,
368
+ changedFilePaths: normalizedChangedFilePaths,
369
+ }),
370
+ );
371
+ }
372
+ const filteredExternalDepChanges = filterExternalDepChangesByInputs({
373
+ changesByWorkspace: externalDepChangesByWorkspace,
374
+ workspaceInputs,
375
+ });
376
+ const inputDependenciesByName = resolveInputWorkspaceDependencies({
377
+ workspaceInputs,
378
+ });
379
+ const affectedSet = computeAffectedWorkspaceSet({
380
+ workspaceInputs,
381
+ workspaceByName,
382
+ changedFilesByName,
383
+ externalDepChangesByWorkspace: filteredExternalDepChanges,
384
+ inputDependenciesByName,
385
+ ignoreWorkspaceDependencies,
386
+ });
387
+ const affectedWorkspaces = workspaceInputs.map(({ workspace }) => {
388
+ const changedFiles = changedFilesByName.get(workspace.name) ?? [];
389
+ const externalDependencies =
390
+ filteredExternalDepChanges.get(workspace.name) ?? [];
391
+ const dependencies = collectAffectedDependencies({
392
+ startingWorkspace: workspace,
393
+ workspaceByName,
394
+ inputDependenciesByName,
395
+ affectedSet,
396
+ ignoreWorkspaceDependencies,
397
+ });
398
+ return {
399
+ workspace,
400
+ isAffected: affectedSet.has(workspace.name),
401
+ affectedReasons: {
402
+ changedFiles,
403
+ dependencies,
404
+ externalDependencies,
405
+ },
406
+ };
407
+ });
408
+ return {
409
+ affectedWorkspaces,
410
+ };
411
+ };
412
+
413
+ export { getFileAffectedWorkspaces };
@@ -0,0 +1,196 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { defineErrors } from "../internal/core/index.mjs";
4
+ import { createSubprocess } from "../runScript/subprocesses.mjs";
5
+
6
+ const GIT_AFFECTED_ERRORS = defineErrors("NoGitRepository", "GitCommandFailed");
7
+ const GIT_AFFECTED_FILE_REASONS = ["diff", "staged", "unstaged", "untracked"];
8
+ const runGit = async (args, cwd) => {
9
+ const proc = createSubprocess(["git", ...args], {
10
+ cwd,
11
+ stdout: "pipe",
12
+ stderr: "pipe",
13
+ });
14
+ const [stdout, stderr, exitCode] = await Promise.all([
15
+ new Response(proc.stdout).text(),
16
+ new Response(proc.stderr).text(),
17
+ proc.exited,
18
+ ]);
19
+ return {
20
+ stdout,
21
+ stderr,
22
+ exitCode,
23
+ };
24
+ };
25
+ const runGitOrThrow = async (args, cwd) => {
26
+ const { stdout, stderr, exitCode } = await runGit(args, cwd);
27
+ if (exitCode !== 0) {
28
+ throw new GIT_AFFECTED_ERRORS.GitCommandFailed(
29
+ `git ${args.join(" ")} failed (exit ${exitCode}): ${stderr.trim()}`,
30
+ );
31
+ }
32
+ return stdout;
33
+ };
34
+ /**
35
+ * Git's `-z` flag emits paths separated by NUL bytes with no quoting or
36
+ * escaping, which is the only safe way to parse output containing paths
37
+ * with spaces, newlines, or non-ASCII characters under `core.quotePath`.
38
+ */ const parseNullSeparated = (output) => output.split("\0").filter(Boolean);
39
+ const resolveGitRoot = async (rootDirectory) => {
40
+ let result;
41
+ try {
42
+ result = await runGit(["rev-parse", "--show-toplevel"], rootDirectory);
43
+ } catch (error) {
44
+ throw new GIT_AFFECTED_ERRORS.NoGitRepository(
45
+ `Not a git repository: ${rootDirectory}${error instanceof Error ? ` (${error.message})` : ""}`,
46
+ );
47
+ }
48
+ if (result.exitCode !== 0 || !result.stdout.trim()) {
49
+ throw new GIT_AFFECTED_ERRORS.NoGitRepository(
50
+ `Not a git repository: ${rootDirectory}`,
51
+ );
52
+ }
53
+ return result.stdout.trim();
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
+ };
87
+ const toProjectFilePath = ({
88
+ gitRoot,
89
+ absoluteProjectRoot,
90
+ gitRelativePath,
91
+ }) => {
92
+ const absolute = path.resolve(gitRoot, gitRelativePath);
93
+ const relative = path.relative(absoluteProjectRoot, absolute);
94
+ if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
95
+ return null;
96
+ }
97
+ return relative.split(path.sep).join("/");
98
+ };
99
+ const getGitAffectedFiles = async (options) => {
100
+ const {
101
+ rootDirectory,
102
+ baseRef,
103
+ headRef,
104
+ ignoreUntracked,
105
+ ignoreStaged,
106
+ ignoreUnstaged,
107
+ ignoreUncommitted,
108
+ } = options;
109
+ const gitRoot = fs.realpathSync.native(
110
+ path.resolve(await resolveGitRoot(rootDirectory)),
111
+ );
112
+ const absoluteProjectRoot = fs.realpathSync.native(
113
+ path.resolve(rootDirectory),
114
+ );
115
+ const includeStaged = !ignoreUncommitted && !ignoreStaged;
116
+ const includeUnstaged = !ignoreUncommitted && !ignoreUnstaged;
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
+ ]);
122
+ const collectors = [
123
+ runGitOrThrow(
124
+ ["diff", "--name-only", "-z", baseRef, headRef],
125
+ gitRoot,
126
+ ).then((out) => ({
127
+ reason: "diff",
128
+ paths: parseNullSeparated(out),
129
+ })),
130
+ ];
131
+ if (includeStaged) {
132
+ collectors.push(
133
+ runGitOrThrow(["diff", "--cached", "--name-only", "-z"], gitRoot).then(
134
+ (out) => ({
135
+ reason: "staged",
136
+ paths: parseNullSeparated(out),
137
+ }),
138
+ ),
139
+ );
140
+ }
141
+ if (includeUnstaged) {
142
+ collectors.push(
143
+ runGitOrThrow(["diff", "--name-only", "-z"], gitRoot).then((out) => ({
144
+ reason: "unstaged",
145
+ paths: parseNullSeparated(out),
146
+ })),
147
+ );
148
+ }
149
+ if (includeUntracked) {
150
+ collectors.push(
151
+ runGitOrThrow(
152
+ ["ls-files", "--others", "--exclude-standard", "-z"],
153
+ gitRoot,
154
+ ).then((out) => ({
155
+ reason: "untracked",
156
+ paths: parseNullSeparated(out),
157
+ })),
158
+ );
159
+ }
160
+ const buckets = await Promise.all(collectors);
161
+ const reasonsByPath = new Map();
162
+ for (const { reason, paths } of buckets) {
163
+ for (const gitRelativePath of paths) {
164
+ const projectFilePath = toProjectFilePath({
165
+ gitRoot,
166
+ absoluteProjectRoot,
167
+ gitRelativePath,
168
+ });
169
+ if (!projectFilePath) continue;
170
+ let set = reasonsByPath.get(projectFilePath);
171
+ if (!set) {
172
+ set = new Set();
173
+ reasonsByPath.set(projectFilePath, set);
174
+ }
175
+ set.add(reason);
176
+ }
177
+ }
178
+ const files = Array.from(reasonsByPath.entries())
179
+ .map(([projectFilePath, reasonSet]) => ({
180
+ projectFilePath,
181
+ reasons: GIT_AFFECTED_FILE_REASONS.filter((r) => reasonSet.has(r)),
182
+ }))
183
+ .sort((a, b) => a.projectFilePath.localeCompare(b.projectFilePath));
184
+ return {
185
+ files,
186
+ baseSha,
187
+ headSha,
188
+ };
189
+ };
190
+
191
+ export {
192
+ GIT_AFFECTED_ERRORS,
193
+ GIT_AFFECTED_FILE_REASONS,
194
+ getGitAffectedFiles,
195
+ readProjectFileAtGitRef,
196
+ };
@@ -0,0 +1,108 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { parseBunLockPackageVersions } from "../internal/bun/bunLock.mjs";
4
+ import { BunWorkspacesError } from "../internal/core/index.mjs";
5
+ import { logger } from "../internal/logger/index.mjs";
6
+ import { computeExternalDependencyChanges } from "./externalDependencyChanges.mjs";
7
+ import { getFileAffectedWorkspaces } from "./fileAffectedWorkspaces.mjs";
8
+ import {
9
+ getGitAffectedFiles,
10
+ readProjectFileAtGitRef,
11
+ } from "./gitAffectedFiles.mjs";
12
+
13
+ const BUN_LOCK_PROJECT_RELATIVE_PATH = "bun.lock";
14
+ const readCurrentBunLock = (rootDirectory) => {
15
+ const lockPath = path.join(rootDirectory, BUN_LOCK_PROJECT_RELATIVE_PATH);
16
+ try {
17
+ return fs.readFileSync(lockPath, "utf8");
18
+ } catch {
19
+ return null;
20
+ }
21
+ };
22
+ const loadVersionsAt = async (rootDirectory, ref) => {
23
+ const contents = await readProjectFileAtGitRef({
24
+ rootDirectory,
25
+ ref,
26
+ projectRelativePath: BUN_LOCK_PROJECT_RELATIVE_PATH,
27
+ });
28
+ if (contents === null) return new Map();
29
+ const parsed = parseBunLockPackageVersions(contents);
30
+ if (parsed instanceof BunWorkspacesError) {
31
+ logger.warn(
32
+ `Could not parse bun.lock at ref "${ref}": ${parsed.message}. Treating as empty.`,
33
+ );
34
+ return new Map();
35
+ }
36
+ return parsed;
37
+ };
38
+ const loadCurrentVersions = (rootDirectory) => {
39
+ const contents = readCurrentBunLock(rootDirectory);
40
+ if (contents === null) return new Map();
41
+ const parsed = parseBunLockPackageVersions(contents);
42
+ if (parsed instanceof BunWorkspacesError) {
43
+ logger.warn(
44
+ `Could not parse current bun.lock: ${parsed.message}. Treating as empty.`,
45
+ );
46
+ return new Map();
47
+ }
48
+ return parsed;
49
+ };
50
+ const getGitAffectedWorkspaces = async ({
51
+ rootDirectory,
52
+ workspacesOptions,
53
+ gitOptions,
54
+ }) => {
55
+ const {
56
+ files: gitFiles,
57
+ baseSha,
58
+ headSha,
59
+ } = await getGitAffectedFiles({
60
+ rootDirectory,
61
+ ...gitOptions,
62
+ });
63
+ const gitFileByPath = new Map(
64
+ gitFiles.map((file) => [file.projectFilePath, file]),
65
+ );
66
+ const projectWorkspaces = workspacesOptions.workspaces ?? [];
67
+ const externalDepChangesByWorkspace =
68
+ workspacesOptions.ignoreExternalDependencies || !projectWorkspaces.length
69
+ ? new Map()
70
+ : computeExternalDependencyChanges({
71
+ workspaces: projectWorkspaces,
72
+ baseLock: await loadVersionsAt(rootDirectory, gitOptions.baseRef),
73
+ headLock:
74
+ gitOptions.headRef === "HEAD"
75
+ ? loadCurrentVersions(rootDirectory)
76
+ : await loadVersionsAt(rootDirectory, gitOptions.headRef),
77
+ });
78
+ const {
79
+ workspaces: _omit,
80
+ ignoreExternalDependencies: _omit2,
81
+ ...fileOpts
82
+ } = workspacesOptions;
83
+ const { affectedWorkspaces } = await getFileAffectedWorkspaces({
84
+ rootDirectory,
85
+ ...fileOpts,
86
+ changedFilePaths: gitFiles.map((file) => file.projectFilePath),
87
+ externalDepChangesByWorkspace,
88
+ });
89
+ const annotatedWorkspaces = affectedWorkspaces.map((result) => ({
90
+ ...result,
91
+ affectedReasons: {
92
+ ...result.affectedReasons,
93
+ changedFiles: result.affectedReasons.changedFiles.map((changedFile) => ({
94
+ ...changedFile,
95
+ fileMetadata: {
96
+ git: gitFileByPath.get(changedFile.filePath),
97
+ },
98
+ })),
99
+ },
100
+ }));
101
+ return {
102
+ affectedWorkspaces: annotatedWorkspaces,
103
+ baseSha,
104
+ headSha,
105
+ };
106
+ };
107
+
108
+ export { getGitAffectedWorkspaces };