@reviewflow/sdk 0.0.0-stage → 6.22.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/LICENSE +15 -0
- package/README.md +86 -2
- package/build/files-node.mjs +70 -0
- package/build/git-node.mjs +1792 -0
- package/build/index-browser.es.js +3328 -0
- package/build/index-node.mjs +3328 -0
- package/package.json +135 -3
- package/src/api/pullRequest.ts +617 -0
- package/src/config/insertCheckoutEntry.ts +54 -0
- package/src/diff/diffPaths.ts +78 -0
- package/src/diff/impacts.ts +47 -0
- package/src/diff/readingOrder.ts +428 -0
- package/src/diff/themes.ts +105 -0
- package/src/diff/types.ts +141 -0
- package/src/files/editableFile.ts +163 -0
- package/src/files.ts +13 -0
- package/src/git/groupDiffPaths.ts +134 -0
- package/src/git/manifests.ts +96 -0
- package/src/git/moduleBindings.ts +571 -0
- package/src/git/pullRequestDiff.ts +159 -0
- package/src/git/readBranchDiff.ts +359 -0
- package/src/git/readImpacts.ts +797 -0
- package/src/git/readingOrder.ts +190 -0
- package/src/git/run.ts +213 -0
- package/src/git.ts +38 -0
- package/src/github/applySuggestions.ts +399 -0
- package/src/github/editPullRequest.ts +790 -0
- package/src/github/fetchCompareDiff.ts +21 -0
- package/src/github/imageProxies.ts +43 -0
- package/src/github/lineDiff.ts +145 -0
- package/src/github/prQuery.ts +298 -0
- package/src/github/pullRequestDetail.ts +1347 -0
- package/src/github/pullRequestHead.ts +65 -0
- package/src/github/pullRequestRevisions.ts +124 -0
- package/src/github/transport.ts +171 -0
- package/src/index.ts +35 -0
- package/src/reviewflow/board.ts +333 -0
- package/src/reviewflow/changedParts.ts +80 -0
- package/src/reviewflow/checksFromReviewflow.ts +75 -0
- package/src/reviewflow/client.ts +589 -0
- package/src/reviewflow/follow.ts +345 -0
- package/src/reviewflow/status.ts +17 -0
- package/src/reviewflow/stream.ts +199 -0
- package/src/types/entities.ts +8 -0
- package/src/types/ids.ts +1 -0
- package/src/types/reviewflow.ts +32 -0
- package/src/utils/pullRequestEdit.ts +108 -0
- package/src/utils/pullRequestPart.ts +12 -0
- package/src/utils/pullRequestReaction.ts +19 -0
- package/src/utils/pullRequestSuggestion.ts +148 -0
|
@@ -0,0 +1,1792 @@
|
|
|
1
|
+
import { execFile, spawn } from 'node:child_process';
|
|
2
|
+
import { env } from 'node:process';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import { lstat } from 'node:fs/promises';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
import { parseSync } from 'oxc-parser';
|
|
8
|
+
import { builtinModules } from 'node:module';
|
|
9
|
+
import { setImmediate } from 'node:timers/promises';
|
|
10
|
+
|
|
11
|
+
const execFileAsync = promisify(execFile);
|
|
12
|
+
const timeoutMs = 1e4;
|
|
13
|
+
const networkTimeoutMs = 6e4;
|
|
14
|
+
const defaultMaxBuffer = 4 * 1024 * 1024;
|
|
15
|
+
const batchMaxBuffer = 256 * 1024 * 1024;
|
|
16
|
+
const networkEnv = () => ({
|
|
17
|
+
...env,
|
|
18
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
19
|
+
GIT_SSH_COMMAND: env.GIT_SSH_COMMAND ?? "ssh -o BatchMode=yes"
|
|
20
|
+
});
|
|
21
|
+
const gitFailedCode = "ERR_GIT_FAILED";
|
|
22
|
+
const reasonLine = (stderr) => {
|
|
23
|
+
const lines = stderr.split("\n").map((line) => line.trim()).filter((line) => line !== "");
|
|
24
|
+
return lines.find((line) => line.startsWith("! ")) ?? lines.find((line) => /^(?:fatal|error):/.test(line)) ?? lines.find((line) => !line.startsWith("hint:")) ?? "";
|
|
25
|
+
};
|
|
26
|
+
const failureReason = (error, timeout) => {
|
|
27
|
+
if (typeof error !== "object" || error === null) return "";
|
|
28
|
+
if ("killed" in error && error.killed === true) {
|
|
29
|
+
return `killed after ${String(timeout / 1e3)}s without finishing`;
|
|
30
|
+
}
|
|
31
|
+
return "stderr" in error ? reasonLine(String(error.stderr)) : "";
|
|
32
|
+
};
|
|
33
|
+
const isExitOneAnswer = (error) => typeof error === "object" && error !== null && "code" in error && error.code === 1 && "stdout" in error && typeof error.stdout === "string" && error.stdout !== "";
|
|
34
|
+
const git = async (cwd, args, options = {}) => {
|
|
35
|
+
const timeout = options.network ? networkTimeoutMs : timeoutMs;
|
|
36
|
+
try {
|
|
37
|
+
const { stdout } = await execFileAsync("git", args, {
|
|
38
|
+
cwd,
|
|
39
|
+
timeout,
|
|
40
|
+
maxBuffer: options.maxBuffer ?? defaultMaxBuffer,
|
|
41
|
+
env: options.network ? networkEnv() : env,
|
|
42
|
+
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
43
|
+
});
|
|
44
|
+
return stdout;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (options.exitOneIsAnswer && isExitOneAnswer(error)) return error.stdout;
|
|
47
|
+
const reason = failureReason(error, timeout);
|
|
48
|
+
throw Object.assign(
|
|
49
|
+
new Error(
|
|
50
|
+
`git ${args.join(" ")} failed in ${cwd}${reason ? `: ${reason}` : ""}`,
|
|
51
|
+
{ cause: error }
|
|
52
|
+
),
|
|
53
|
+
{ code: gitFailedCode }
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const unansweredHeader = / (?:missing|ambiguous)$/;
|
|
58
|
+
const parseBatchOutput = (output, paths) => {
|
|
59
|
+
const contents = /* @__PURE__ */ new Map();
|
|
60
|
+
let offset = 0;
|
|
61
|
+
for (const path of paths) {
|
|
62
|
+
const newline = output.indexOf("\n", offset);
|
|
63
|
+
if (newline === -1) break;
|
|
64
|
+
const header = output.toString("utf8", offset, newline);
|
|
65
|
+
offset = newline + 1;
|
|
66
|
+
if (unansweredHeader.test(header)) continue;
|
|
67
|
+
const [, type, sizeText] = header.split(" ");
|
|
68
|
+
const size = Number(sizeText);
|
|
69
|
+
if (!Number.isSafeInteger(size) || size < 0) break;
|
|
70
|
+
if (type === "blob") {
|
|
71
|
+
contents.set(path, output.toString("utf8", offset, offset + size));
|
|
72
|
+
}
|
|
73
|
+
offset += size + 1;
|
|
74
|
+
}
|
|
75
|
+
return contents;
|
|
76
|
+
};
|
|
77
|
+
const catFiles = async (cwd, sha, paths) => {
|
|
78
|
+
const asked = paths.filter((path) => !path.includes("\n"));
|
|
79
|
+
if (asked.length === 0) return /* @__PURE__ */ new Map();
|
|
80
|
+
const args = ["cat-file", "--batch"];
|
|
81
|
+
return new Promise((resolve, reject) => {
|
|
82
|
+
const child = spawn("git", args, { cwd, env, timeout: timeoutMs });
|
|
83
|
+
const chunks = [];
|
|
84
|
+
let size = 0;
|
|
85
|
+
let stderr = "";
|
|
86
|
+
child.stdout.on("data", (chunk) => {
|
|
87
|
+
size += chunk.length;
|
|
88
|
+
if (size > batchMaxBuffer) {
|
|
89
|
+
child.kill();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
chunks.push(chunk);
|
|
93
|
+
});
|
|
94
|
+
child.stderr.on("data", (chunk) => {
|
|
95
|
+
stderr += chunk.toString("utf8");
|
|
96
|
+
});
|
|
97
|
+
child.on("error", reject);
|
|
98
|
+
child.stdin.on("error", () => {
|
|
99
|
+
});
|
|
100
|
+
child.on("close", (code, signal) => {
|
|
101
|
+
if (code === 0) {
|
|
102
|
+
resolve(parseBatchOutput(Buffer.concat(chunks), asked));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const reason = signal === null ? reasonLine(stderr) : `killed (${signal}) after reading ${String(size)} bytes`;
|
|
106
|
+
reject(
|
|
107
|
+
Object.assign(
|
|
108
|
+
new Error(
|
|
109
|
+
`git ${args.join(" ")} failed in ${cwd}${reason ? `: ${reason}` : ""}`
|
|
110
|
+
),
|
|
111
|
+
{ code: gitFailedCode }
|
|
112
|
+
)
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
child.stdin.end(asked.map((path) => `${sha}:${path}
|
|
116
|
+
`).join(""));
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
const gitOrNull = async (cwd, args, options = {}) => {
|
|
120
|
+
try {
|
|
121
|
+
return await git(cwd, args, options);
|
|
122
|
+
} catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const defaultDiffLimits = {
|
|
128
|
+
maxLinesPerFile: 3e3,
|
|
129
|
+
maxTotalLines: 3e4,
|
|
130
|
+
maxFiles: 500,
|
|
131
|
+
maxUntrackedFiles: 100,
|
|
132
|
+
maxUntrackedBytes: 512 * 1024
|
|
133
|
+
};
|
|
134
|
+
const maxPatchBytes = 64 * 1024 * 1024;
|
|
135
|
+
const neutralConfig = ["-c", "core.quotePath=false"];
|
|
136
|
+
const diffFlags = [
|
|
137
|
+
"--no-color",
|
|
138
|
+
"--no-ext-diff",
|
|
139
|
+
"--no-textconv",
|
|
140
|
+
"--src-prefix=a/",
|
|
141
|
+
"--dst-prefix=b/"
|
|
142
|
+
];
|
|
143
|
+
const parseNumstat = (output) => {
|
|
144
|
+
const records = output.split("\0");
|
|
145
|
+
const entries = [];
|
|
146
|
+
for (let index = 0; index < records.length; index++) {
|
|
147
|
+
const record = records[index] ?? "";
|
|
148
|
+
if (record === "") continue;
|
|
149
|
+
const [added = "", deleted = "", ...rest] = record.split(" ");
|
|
150
|
+
const lines = added === "-" || deleted === "-" ? null : Number(added) + Number(deleted);
|
|
151
|
+
const path = rest.join(" ");
|
|
152
|
+
if (path !== "") {
|
|
153
|
+
entries.push({ path, paths: [path], lines });
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const from = records[index + 1] ?? "";
|
|
157
|
+
const to = records[index + 2] ?? "";
|
|
158
|
+
index += 2;
|
|
159
|
+
entries.push({ path: to, paths: [from, to], lines });
|
|
160
|
+
}
|
|
161
|
+
return entries;
|
|
162
|
+
};
|
|
163
|
+
const omissionReason = (lines, budget, limits) => {
|
|
164
|
+
if (lines === null) return "binary";
|
|
165
|
+
if (lines > limits.maxLinesPerFile) return "tooLarge";
|
|
166
|
+
if (budget.files + 1 > limits.maxFiles || budget.lines + lines > limits.maxTotalLines) {
|
|
167
|
+
return "budget";
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
};
|
|
171
|
+
const pathspecs = (included, excluded) => {
|
|
172
|
+
if (excluded.length === 0) return [];
|
|
173
|
+
if (included.length < excluded.length) {
|
|
174
|
+
return ["--", ...included.map((path) => `:(literal)${path}`)];
|
|
175
|
+
}
|
|
176
|
+
return ["--", ...excluded.map((path) => `:(exclude,literal)${path}`)];
|
|
177
|
+
};
|
|
178
|
+
const readNumstat = async (cwd, range, whitespaceFlags) => parseNumstat(
|
|
179
|
+
await git(cwd, [
|
|
180
|
+
...neutralConfig,
|
|
181
|
+
"diff",
|
|
182
|
+
"--no-relative",
|
|
183
|
+
"-M",
|
|
184
|
+
...whitespaceFlags,
|
|
185
|
+
"--numstat",
|
|
186
|
+
"-z",
|
|
187
|
+
...range
|
|
188
|
+
])
|
|
189
|
+
);
|
|
190
|
+
const whitespaceOnlyFiles = async (cwd, range, shown) => {
|
|
191
|
+
const shownPaths = new Set(shown.map(({ path }) => path));
|
|
192
|
+
const all = await readNumstat(cwd, range, []);
|
|
193
|
+
return all.filter(({ path }) => !shownPaths.has(path)).map(({ path }) => ({ path, reason: "whitespace" }));
|
|
194
|
+
};
|
|
195
|
+
const readTrackedDiff = async ({
|
|
196
|
+
cwd,
|
|
197
|
+
range,
|
|
198
|
+
whitespace,
|
|
199
|
+
limits,
|
|
200
|
+
budget
|
|
201
|
+
}) => {
|
|
202
|
+
const whitespaceFlags = whitespace === "hide" ? ["-w"] : [];
|
|
203
|
+
const entries = await readNumstat(cwd, range, whitespaceFlags);
|
|
204
|
+
const omitted = whitespace === "hide" ? await whitespaceOnlyFiles(cwd, range, entries) : [];
|
|
205
|
+
const excluded = [];
|
|
206
|
+
const included = [];
|
|
207
|
+
const paths = [];
|
|
208
|
+
for (const entry of entries) {
|
|
209
|
+
const reason = omissionReason(entry.lines, budget, limits);
|
|
210
|
+
if (reason === null) {
|
|
211
|
+
budget.files += 1;
|
|
212
|
+
budget.lines += entry.lines ?? 0;
|
|
213
|
+
paths.push(entry.path);
|
|
214
|
+
included.push(...entry.paths);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
omitted.push({ path: entry.path, reason });
|
|
218
|
+
excluded.push(...entry.paths);
|
|
219
|
+
}
|
|
220
|
+
if (paths.length === 0) return { patch: "", paths, omitted };
|
|
221
|
+
const patch = await git(
|
|
222
|
+
cwd,
|
|
223
|
+
[
|
|
224
|
+
...neutralConfig,
|
|
225
|
+
"diff",
|
|
226
|
+
...diffFlags,
|
|
227
|
+
"--no-relative",
|
|
228
|
+
"-M",
|
|
229
|
+
...whitespaceFlags,
|
|
230
|
+
...range,
|
|
231
|
+
...pathspecs(included, excluded)
|
|
232
|
+
],
|
|
233
|
+
{ maxBuffer: maxPatchBytes }
|
|
234
|
+
);
|
|
235
|
+
return { patch, paths, omitted };
|
|
236
|
+
};
|
|
237
|
+
const countAddedLines = (patch) => {
|
|
238
|
+
const lines = patch.split("\n");
|
|
239
|
+
const firstHunk = lines.findIndex((line) => line.startsWith("@@"));
|
|
240
|
+
if (firstHunk === -1) return 0;
|
|
241
|
+
return lines.slice(firstHunk).filter((line) => line.startsWith("+")).length;
|
|
242
|
+
};
|
|
243
|
+
const isBinaryPatch = (patch) => /^Binary files /m.test(patch);
|
|
244
|
+
const readUntrackedDiff = async (cwd, limits, budget) => {
|
|
245
|
+
const listing = await git(cwd, [
|
|
246
|
+
"ls-files",
|
|
247
|
+
"--others",
|
|
248
|
+
"--exclude-standard",
|
|
249
|
+
"-z"
|
|
250
|
+
]);
|
|
251
|
+
const listed = listing.split("\0").filter((path) => path !== "");
|
|
252
|
+
const patches = [];
|
|
253
|
+
const paths = [];
|
|
254
|
+
const omitted = [];
|
|
255
|
+
for (const [index, path] of listed.entries()) {
|
|
256
|
+
if (index >= limits.maxUntrackedFiles) {
|
|
257
|
+
omitted.push({ path, reason: "budget" });
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
const entry = await lstat(join(cwd, path)).catch(() => null);
|
|
261
|
+
if (entry === null || entry.isDirectory()) continue;
|
|
262
|
+
if (entry.size > limits.maxUntrackedBytes) {
|
|
263
|
+
omitted.push({ path, reason: "tooLarge" });
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const patch = await git(
|
|
267
|
+
cwd,
|
|
268
|
+
[
|
|
269
|
+
...neutralConfig,
|
|
270
|
+
"diff",
|
|
271
|
+
...diffFlags,
|
|
272
|
+
"--no-index",
|
|
273
|
+
"--",
|
|
274
|
+
"/dev/null",
|
|
275
|
+
path
|
|
276
|
+
],
|
|
277
|
+
{ exitOneIsAnswer: true, maxBuffer: maxPatchBytes }
|
|
278
|
+
);
|
|
279
|
+
const lines = isBinaryPatch(patch) ? null : countAddedLines(patch);
|
|
280
|
+
const reason = omissionReason(lines, budget, limits);
|
|
281
|
+
if (reason !== null) {
|
|
282
|
+
omitted.push({ path, reason });
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
budget.files += 1;
|
|
286
|
+
budget.lines += lines ?? 0;
|
|
287
|
+
patches.push(patch);
|
|
288
|
+
paths.push(path);
|
|
289
|
+
}
|
|
290
|
+
return { patches, paths, omitted };
|
|
291
|
+
};
|
|
292
|
+
const readBranchDiff = async ({
|
|
293
|
+
cwd,
|
|
294
|
+
mergeBase,
|
|
295
|
+
scope = "committed",
|
|
296
|
+
head = "HEAD",
|
|
297
|
+
whitespace = "show",
|
|
298
|
+
limits = defaultDiffLimits
|
|
299
|
+
}) => {
|
|
300
|
+
if (mergeBase.startsWith("-") || head.startsWith("-")) {
|
|
301
|
+
throw new Error(
|
|
302
|
+
`not a revision: ${mergeBase.startsWith("-") ? mergeBase : head}`
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
const budget = { files: 0, lines: 0 };
|
|
306
|
+
const tracked = await readTrackedDiff({
|
|
307
|
+
cwd,
|
|
308
|
+
range: scope === "committed" ? [mergeBase, head] : [mergeBase],
|
|
309
|
+
whitespace,
|
|
310
|
+
limits,
|
|
311
|
+
budget
|
|
312
|
+
});
|
|
313
|
+
if (scope === "committed") {
|
|
314
|
+
return {
|
|
315
|
+
patch: tracked.patch,
|
|
316
|
+
paths: tracked.paths,
|
|
317
|
+
omitted: tracked.omitted,
|
|
318
|
+
untrackedCount: 0
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
const untracked = await readUntrackedDiff(cwd, limits, budget);
|
|
322
|
+
return {
|
|
323
|
+
patch: [tracked.patch, ...untracked.patches].join(""),
|
|
324
|
+
paths: [...tracked.paths, ...untracked.paths],
|
|
325
|
+
omitted: [...tracked.omitted, ...untracked.omitted],
|
|
326
|
+
untrackedCount: untracked.patches.length
|
|
327
|
+
};
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const manifestNames = [
|
|
331
|
+
// javascript
|
|
332
|
+
"package.json",
|
|
333
|
+
// python
|
|
334
|
+
"pyproject.toml",
|
|
335
|
+
"setup.py",
|
|
336
|
+
// ruby
|
|
337
|
+
"Gemfile",
|
|
338
|
+
// rust
|
|
339
|
+
"Cargo.toml",
|
|
340
|
+
// go
|
|
341
|
+
"go.mod",
|
|
342
|
+
// php
|
|
343
|
+
"composer.json",
|
|
344
|
+
// jvm
|
|
345
|
+
"pom.xml",
|
|
346
|
+
"build.gradle",
|
|
347
|
+
"build.gradle.kts",
|
|
348
|
+
// elixir
|
|
349
|
+
"mix.exs",
|
|
350
|
+
// dart
|
|
351
|
+
"pubspec.yaml",
|
|
352
|
+
// swift
|
|
353
|
+
"Package.swift"
|
|
354
|
+
];
|
|
355
|
+
const manifests = new Set(manifestNames);
|
|
356
|
+
const isManifestPath = (path) => manifests.has(path.slice(path.lastIndexOf("/") + 1));
|
|
357
|
+
const packageOf = (path, isPackage) => {
|
|
358
|
+
const directories = path.split("/").slice(0, -1);
|
|
359
|
+
for (let depth = directories.length; depth >= 1; depth--) {
|
|
360
|
+
const directory = directories.slice(0, depth).join("/");
|
|
361
|
+
if (isPackage(directory)) return directory;
|
|
362
|
+
}
|
|
363
|
+
return directories[0] ?? "";
|
|
364
|
+
};
|
|
365
|
+
const noPackages = () => false;
|
|
366
|
+
const listTree = async (checkout, sha) => {
|
|
367
|
+
const listing = await git(
|
|
368
|
+
checkout,
|
|
369
|
+
["ls-tree", "-r", "--name-only", "-z", sha],
|
|
370
|
+
{
|
|
371
|
+
maxBuffer: 256 * 1024 * 1024
|
|
372
|
+
}
|
|
373
|
+
);
|
|
374
|
+
return listing.split("\0").filter((path) => path !== "");
|
|
375
|
+
};
|
|
376
|
+
const packagesInListing = (paths) => {
|
|
377
|
+
const directories = /* @__PURE__ */ new Set();
|
|
378
|
+
for (const path of paths) {
|
|
379
|
+
const slash = path.lastIndexOf("/");
|
|
380
|
+
if (slash === -1) continue;
|
|
381
|
+
if (isManifestPath(path)) directories.add(path.slice(0, slash));
|
|
382
|
+
}
|
|
383
|
+
return (directory) => directories.has(directory);
|
|
384
|
+
};
|
|
385
|
+
const packagesAtTree = async (checkout, sha) => packagesInListing(await listTree(checkout, sha));
|
|
386
|
+
const packagesOnDisk = (worktree) => {
|
|
387
|
+
const known = /* @__PURE__ */ new Map();
|
|
388
|
+
return (directory) => {
|
|
389
|
+
const cached = known.get(directory);
|
|
390
|
+
if (cached !== void 0) return cached;
|
|
391
|
+
const found = manifestNames.some(
|
|
392
|
+
(manifest) => existsSync(join(worktree, directory, manifest))
|
|
393
|
+
);
|
|
394
|
+
known.set(directory, found);
|
|
395
|
+
return found;
|
|
396
|
+
};
|
|
397
|
+
};
|
|
398
|
+
const groupDiffPaths = (isPackage, paths) => {
|
|
399
|
+
const groups = /* @__PURE__ */ new Map();
|
|
400
|
+
for (const path of paths) {
|
|
401
|
+
const name = packageOf(path, isPackage);
|
|
402
|
+
const group = groups.get(name);
|
|
403
|
+
if (group === void 0) groups.set(name, [path]);
|
|
404
|
+
else group.push(path);
|
|
405
|
+
}
|
|
406
|
+
return [...groups].toSorted(([left], [right]) => Number(left !== "") - Number(right !== "")).map(([name, groupPaths]) => ({ name, paths: groupPaths }));
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const cEscapes = {
|
|
410
|
+
a: 7,
|
|
411
|
+
b: 8,
|
|
412
|
+
f: 12,
|
|
413
|
+
n: 10,
|
|
414
|
+
r: 13,
|
|
415
|
+
t: 9,
|
|
416
|
+
v: 11,
|
|
417
|
+
'"': 34,
|
|
418
|
+
"\\": 92
|
|
419
|
+
};
|
|
420
|
+
const unquoteGitPath = (quoted) => {
|
|
421
|
+
const bytes = [];
|
|
422
|
+
const encoder = new TextEncoder();
|
|
423
|
+
for (let index = 1; index < quoted.length - 1; index++) {
|
|
424
|
+
const char = quoted[index];
|
|
425
|
+
if (char !== "\\") {
|
|
426
|
+
bytes.push(...encoder.encode(char));
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
const next = quoted[index + 1] ?? "";
|
|
430
|
+
const octal = /^[0-7]{3}/.exec(quoted.slice(index + 1));
|
|
431
|
+
if (octal) {
|
|
432
|
+
bytes.push(Number.parseInt(octal[0], 8));
|
|
433
|
+
index += 3;
|
|
434
|
+
} else {
|
|
435
|
+
bytes.push(cEscapes[next] ?? next.codePointAt(0) ?? 0);
|
|
436
|
+
index += 1;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return new TextDecoder().decode(new Uint8Array(bytes));
|
|
440
|
+
};
|
|
441
|
+
const unquoted = (name) => name.length > 1 && name.startsWith('"') && name.endsWith('"') ? unquoteGitPath(name) : name;
|
|
442
|
+
const headerName = (raw, prefix) => {
|
|
443
|
+
const name = unquoted(raw.endsWith(" ") ? raw.slice(0, -1) : raw);
|
|
444
|
+
return name.startsWith(prefix) ? name.slice(prefix.length) : null;
|
|
445
|
+
};
|
|
446
|
+
const pathOfBlock = (block) => {
|
|
447
|
+
const added = /^\+\+\+ (.*)$/m.exec(block)?.[1];
|
|
448
|
+
const addedName = added === void 0 ? null : headerName(added, "b/");
|
|
449
|
+
if (addedName !== null) return addedName;
|
|
450
|
+
const removed = /^--- (.*)$/m.exec(block)?.[1];
|
|
451
|
+
const removedName = removed === void 0 ? null : headerName(removed, "a/");
|
|
452
|
+
if (removedName !== null) return removedName;
|
|
453
|
+
const renamed = /^rename to (.*)$/m.exec(block)?.[1];
|
|
454
|
+
if (renamed !== void 0) return unquoted(renamed);
|
|
455
|
+
const header = block.split("\n", 1)[0] ?? "";
|
|
456
|
+
const quotedSeparator = header.lastIndexOf(' "b/');
|
|
457
|
+
if (quotedSeparator !== -1) {
|
|
458
|
+
return headerName(header.slice(quotedSeparator + 1), "b/");
|
|
459
|
+
}
|
|
460
|
+
const separator = header.lastIndexOf(" b/");
|
|
461
|
+
return separator === -1 ? null : header.slice(separator + 3);
|
|
462
|
+
};
|
|
463
|
+
const diffPaths = (patch) => patch.split(/^diff --git /m).slice(1).map(pathOfBlock).filter((path) => path !== null);
|
|
464
|
+
|
|
465
|
+
const fetchCompareDiff = (transport, { ownerRepo, base, head }) => {
|
|
466
|
+
const [owner, name, ...rest] = ownerRepo.split("/");
|
|
467
|
+
if (owner === void 0 || name === void 0 || rest.length > 0) {
|
|
468
|
+
throw new Error(`${ownerRepo} is not owner/repo`);
|
|
469
|
+
}
|
|
470
|
+
return transport.text({
|
|
471
|
+
path: `repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/compare/${base}...${head}`,
|
|
472
|
+
accept: "application/vnd.github.diff"
|
|
473
|
+
});
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
const isSha = (value) => /^[\da-f]{40}$/i.test(value);
|
|
477
|
+
const hasCommit = async (checkout, sha) => await gitOrNull(checkout, ["cat-file", "-e", `${sha}^{commit}`]) !== null;
|
|
478
|
+
const ensureCommits = async (checkout, shas, onStep, signal) => {
|
|
479
|
+
const missing = [];
|
|
480
|
+
for (const sha of shas) {
|
|
481
|
+
if (!await hasCommit(checkout, sha)) missing.push(sha);
|
|
482
|
+
}
|
|
483
|
+
if (missing.length === 0) return;
|
|
484
|
+
onStep?.("fetching");
|
|
485
|
+
await git(
|
|
486
|
+
checkout,
|
|
487
|
+
["fetch", "--no-write-fetch-head", "--no-tags", "origin", ...missing],
|
|
488
|
+
{ network: true, ...signal === void 0 ? {} : { signal } }
|
|
489
|
+
);
|
|
490
|
+
};
|
|
491
|
+
const readFromGit = async (checkout, { head, base, whitespace = "show", onStep, signal }) => {
|
|
492
|
+
await ensureCommits(checkout, [head, base], onStep, signal);
|
|
493
|
+
onStep?.("diffing");
|
|
494
|
+
const mergeBaseOutput = await git(checkout, ["merge-base", base, head]);
|
|
495
|
+
const mergeBase = mergeBaseOutput.trim();
|
|
496
|
+
const { patch, paths, omitted } = await readBranchDiff({
|
|
497
|
+
cwd: checkout,
|
|
498
|
+
mergeBase,
|
|
499
|
+
scope: "committed",
|
|
500
|
+
head,
|
|
501
|
+
whitespace
|
|
502
|
+
});
|
|
503
|
+
return {
|
|
504
|
+
source: "git",
|
|
505
|
+
fallbackReason: null,
|
|
506
|
+
whitespace,
|
|
507
|
+
patch,
|
|
508
|
+
sides: { old: mergeBase, new: head },
|
|
509
|
+
omitted,
|
|
510
|
+
groups: groupDiffPaths(await packagesAtTree(checkout, head), paths)
|
|
511
|
+
};
|
|
512
|
+
};
|
|
513
|
+
const readFromGitHub = async ({ checkout, transport, ref, head, base }, fallbackReason) => {
|
|
514
|
+
const patch = await fetchCompareDiff(transport, {
|
|
515
|
+
ownerRepo: ref.ownerRepo,
|
|
516
|
+
base,
|
|
517
|
+
head
|
|
518
|
+
});
|
|
519
|
+
let isPackage = noPackages;
|
|
520
|
+
if (checkout !== null) {
|
|
521
|
+
isPackage = await packagesAtTree(checkout, head).catch(() => noPackages);
|
|
522
|
+
}
|
|
523
|
+
return {
|
|
524
|
+
source: "github",
|
|
525
|
+
fallbackReason,
|
|
526
|
+
whitespace: "show",
|
|
527
|
+
patch,
|
|
528
|
+
sides: null,
|
|
529
|
+
omitted: [],
|
|
530
|
+
groups: groupDiffPaths(isPackage, diffPaths(patch))
|
|
531
|
+
};
|
|
532
|
+
};
|
|
533
|
+
const readPullRequestDiff = async (request) => {
|
|
534
|
+
if (!isSha(request.head) || !isSha(request.base)) {
|
|
535
|
+
throw new Error("head and base must be commit shas");
|
|
536
|
+
}
|
|
537
|
+
if (request.checkout === null) return readFromGitHub(request, "no checkout");
|
|
538
|
+
try {
|
|
539
|
+
return await readFromGit(request.checkout, request);
|
|
540
|
+
} catch (error) {
|
|
541
|
+
if (request.signal?.aborted) throw error;
|
|
542
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
543
|
+
return readFromGitHub(request, reason);
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
const readFileAtSha = async (checkout, sha, path) => {
|
|
547
|
+
if (!isSha(sha)) throw new Error("sha must be a commit sha");
|
|
548
|
+
if (path === "") throw new Error("path is required");
|
|
549
|
+
return git(checkout, ["cat-file", "blob", `${sha}:${path}`], {
|
|
550
|
+
maxBuffer: 64 * 1024 * 1024
|
|
551
|
+
});
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
const typescriptExtensions = [".ts", ".tsx", ".mts", ".cts"];
|
|
555
|
+
const extensionOf = (path) => {
|
|
556
|
+
const slash = path.lastIndexOf("/");
|
|
557
|
+
const dot = path.lastIndexOf(".");
|
|
558
|
+
return dot > slash ? path.slice(dot) : "";
|
|
559
|
+
};
|
|
560
|
+
const isTypescriptPath = (path) => typescriptExtensions.includes(extensionOf(path));
|
|
561
|
+
const attachableSuffix = /\.(?:test|spec|stories)(?=\.[cm]?tsx?$)/;
|
|
562
|
+
const testDirectories = /* @__PURE__ */ new Set([
|
|
563
|
+
"__fixtures__",
|
|
564
|
+
"__mocks__",
|
|
565
|
+
"__tests__",
|
|
566
|
+
"fixtures",
|
|
567
|
+
"test",
|
|
568
|
+
"tests"
|
|
569
|
+
]);
|
|
570
|
+
const isAttachablePath = (path) => isTypescriptPath(path) && (attachableSuffix.test(path) || path.split("/").slice(0, -1).some((directory) => testDirectories.has(directory)));
|
|
571
|
+
const javascriptToTypescript = {
|
|
572
|
+
".js": ".ts",
|
|
573
|
+
".jsx": ".tsx",
|
|
574
|
+
".mjs": ".mts",
|
|
575
|
+
".cjs": ".cts"
|
|
576
|
+
};
|
|
577
|
+
const directoryOf = (path) => {
|
|
578
|
+
const slash = path.lastIndexOf("/");
|
|
579
|
+
return slash === -1 ? "" : path.slice(0, slash);
|
|
580
|
+
};
|
|
581
|
+
const normalize = (path) => {
|
|
582
|
+
const segments = [];
|
|
583
|
+
for (const segment of path.split("/")) {
|
|
584
|
+
if (segment === "" || segment === ".") continue;
|
|
585
|
+
if (segment === "..") {
|
|
586
|
+
if (segments.length === 0) return null;
|
|
587
|
+
segments.pop();
|
|
588
|
+
} else {
|
|
589
|
+
segments.push(segment);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return segments.join("/");
|
|
593
|
+
};
|
|
594
|
+
const candidatesOf = (target) => {
|
|
595
|
+
const extension = extensionOf(target);
|
|
596
|
+
const rewritten = javascriptToTypescript[extension];
|
|
597
|
+
const declaration = rewritten === void 0 ? `${target}.d.ts` : `${target.slice(0, -extension.length)}.d.ts`;
|
|
598
|
+
return [
|
|
599
|
+
target,
|
|
600
|
+
...rewritten === void 0 ? [] : [target.slice(0, -extension.length) + rewritten],
|
|
601
|
+
...typescriptExtensions.map((typescript) => target + typescript),
|
|
602
|
+
declaration,
|
|
603
|
+
`${target}/index.ts`,
|
|
604
|
+
`${target}/index.tsx`
|
|
605
|
+
];
|
|
606
|
+
};
|
|
607
|
+
const resolveTarget = (target, exists) => target === null ? null : candidatesOf(target).find(
|
|
608
|
+
(candidate) => isTypescriptPath(candidate) && exists(candidate)
|
|
609
|
+
) ?? null;
|
|
610
|
+
const resolveRelative = (from, specifier, exists) => {
|
|
611
|
+
if (!specifier.startsWith(".")) return null;
|
|
612
|
+
return resolveTarget(normalize(`${directoryOf(from)}/${specifier}`), exists);
|
|
613
|
+
};
|
|
614
|
+
const conditionOrder = ["types", "import", "default"];
|
|
615
|
+
const targetOf = (value) => {
|
|
616
|
+
if (typeof value === "string") return value;
|
|
617
|
+
if (typeof value !== "object" || value === null) return null;
|
|
618
|
+
const conditions = value;
|
|
619
|
+
for (const condition of conditionOrder) {
|
|
620
|
+
const target = targetOf(conditions[condition]);
|
|
621
|
+
if (target !== null) return target;
|
|
622
|
+
}
|
|
623
|
+
return null;
|
|
624
|
+
};
|
|
625
|
+
const expand = (pattern, specifier) => {
|
|
626
|
+
const star = pattern.indexOf("*");
|
|
627
|
+
if (star === -1) return pattern === specifier ? "" : null;
|
|
628
|
+
const prefix = pattern.slice(0, star);
|
|
629
|
+
const suffix = pattern.slice(star + 1);
|
|
630
|
+
if (!specifier.startsWith(prefix) || !specifier.endsWith(suffix)) return null;
|
|
631
|
+
return specifier.slice(prefix.length, specifier.length - suffix.length);
|
|
632
|
+
};
|
|
633
|
+
const specificity = (pattern) => pattern.includes("*") ? pattern.indexOf("*") : Number.MAX_SAFE_INTEGER;
|
|
634
|
+
const resolveSubpathImport = (specifier, { directory, imports }, exists) => {
|
|
635
|
+
if (!specifier.startsWith("#")) return null;
|
|
636
|
+
if (typeof imports !== "object" || imports === null) return null;
|
|
637
|
+
const patterns = Object.entries(imports).toSorted(
|
|
638
|
+
([left], [right]) => specificity(right) - specificity(left)
|
|
639
|
+
);
|
|
640
|
+
for (const [pattern, value] of patterns) {
|
|
641
|
+
const captured = expand(pattern, specifier);
|
|
642
|
+
const target = targetOf(value);
|
|
643
|
+
if (captured === null || target?.startsWith("./") !== true) continue;
|
|
644
|
+
return resolveTarget(
|
|
645
|
+
normalize(`${directory}/${target.replace("*", captured)}`),
|
|
646
|
+
exists
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
return null;
|
|
650
|
+
};
|
|
651
|
+
const joinPath = (directory, name) => directory === "" ? name : `${directory}/${name}`;
|
|
652
|
+
const coveredDirectoriesOf = (path) => {
|
|
653
|
+
const directory = directoryOf(path);
|
|
654
|
+
return directory.split("/").at(-1) === "__tests__" ? [directory, directoryOf(directory)] : [directory];
|
|
655
|
+
};
|
|
656
|
+
const attachableKinds = /* @__PURE__ */ new Set(["spec", "stories", "test"]);
|
|
657
|
+
const subjectsOf = (path) => {
|
|
658
|
+
const parts = path.slice(directoryOf(path).length).replace(/^\//, "").replace(/\.[cm]?tsx?$/, "").split(".");
|
|
659
|
+
const kind = parts.findIndex((part) => attachableKinds.has(part));
|
|
660
|
+
const stem = kind === -1 ? parts : parts.slice(0, kind);
|
|
661
|
+
const names = stem.map(
|
|
662
|
+
(_, dropped) => stem.slice(0, stem.length - dropped).join(".")
|
|
663
|
+
);
|
|
664
|
+
return coveredDirectoriesOf(path).flatMap(
|
|
665
|
+
(directory) => names.flatMap(
|
|
666
|
+
(name) => typescriptExtensions.map(
|
|
667
|
+
(extension) => joinPath(directory, name + extension)
|
|
668
|
+
)
|
|
669
|
+
)
|
|
670
|
+
);
|
|
671
|
+
};
|
|
672
|
+
const reachedFrom = (path, { code, changed, imports }) => {
|
|
673
|
+
const reached = [];
|
|
674
|
+
const visited = /* @__PURE__ */ new Set([path]);
|
|
675
|
+
const visit = (from) => {
|
|
676
|
+
for (const target of imports.get(from) ?? []) {
|
|
677
|
+
if (visited.has(target)) continue;
|
|
678
|
+
visited.add(target);
|
|
679
|
+
if (code.has(target)) reached.push(target);
|
|
680
|
+
else if (!changed.has(target)) visit(target);
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
visit(path);
|
|
684
|
+
return reached;
|
|
685
|
+
};
|
|
686
|
+
const countReachable = (entry, edges) => {
|
|
687
|
+
const seen = /* @__PURE__ */ new Set([entry]);
|
|
688
|
+
const stack = [entry];
|
|
689
|
+
while (stack.length > 0) {
|
|
690
|
+
for (const target of edges.get(stack.pop()) ?? []) {
|
|
691
|
+
if (seen.has(target)) continue;
|
|
692
|
+
seen.add(target);
|
|
693
|
+
stack.push(target);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return seen.size - 1;
|
|
697
|
+
};
|
|
698
|
+
const attachTo = (attachable, code, reached) => {
|
|
699
|
+
const covered = coveredDirectoriesOf(attachable);
|
|
700
|
+
return subjectsOf(attachable).find((subject) => code.has(subject)) ?? reached.find((target) => covered.includes(directoryOf(target))) ?? reached[0] ?? null;
|
|
701
|
+
};
|
|
702
|
+
const orderGroup = ({ name, paths }, changed, imports) => {
|
|
703
|
+
const code = new Set(
|
|
704
|
+
paths.filter((path) => isTypescriptPath(path) && !isAttachablePath(path))
|
|
705
|
+
);
|
|
706
|
+
const attachables = paths.filter(isAttachablePath);
|
|
707
|
+
const others = paths.filter((path) => !isTypescriptPath(path));
|
|
708
|
+
const reach = { code, changed, imports };
|
|
709
|
+
const edges = new Map(
|
|
710
|
+
[...code].map((path) => [path, reachedFrom(path, reach)])
|
|
711
|
+
);
|
|
712
|
+
const imported = new Set([...edges.values()].flat());
|
|
713
|
+
const attached = /* @__PURE__ */ new Map();
|
|
714
|
+
const standalone = [];
|
|
715
|
+
for (const attachable of attachables) {
|
|
716
|
+
const subject = attachTo(attachable, code, reachedFrom(attachable, reach));
|
|
717
|
+
if (subject === null) standalone.push(attachable);
|
|
718
|
+
else attached.set(subject, [...attached.get(subject) ?? [], attachable]);
|
|
719
|
+
}
|
|
720
|
+
const reachable = new Map(
|
|
721
|
+
[...code].map((path) => [path, countReachable(path, edges)])
|
|
722
|
+
);
|
|
723
|
+
const byPath = (left, right) => left.localeCompare(right);
|
|
724
|
+
const roots = [...code].filter((path) => !imported.has(path)).toSorted(
|
|
725
|
+
(left, right) => reachable.get(right) - reachable.get(left) || byPath(left, right)
|
|
726
|
+
);
|
|
727
|
+
const ordered = [];
|
|
728
|
+
const visited = /* @__PURE__ */ new Set();
|
|
729
|
+
const visit = (path) => {
|
|
730
|
+
if (visited.has(path)) return;
|
|
731
|
+
visited.add(path);
|
|
732
|
+
ordered.push(path);
|
|
733
|
+
for (const target of edges.get(path) ?? []) visit(target);
|
|
734
|
+
};
|
|
735
|
+
for (const root of roots) visit(root);
|
|
736
|
+
for (const path of [...code].toSorted(byPath)) visit(path);
|
|
737
|
+
const entries = new Set(
|
|
738
|
+
roots.filter((root) => (edges.get(root) ?? []).length > 0)
|
|
739
|
+
);
|
|
740
|
+
const rootPaths = new Set(roots);
|
|
741
|
+
const codeFile = (path) => ({
|
|
742
|
+
path,
|
|
743
|
+
entry: entries.has(path),
|
|
744
|
+
root: rootPaths.has(path),
|
|
745
|
+
attached: (attached.get(path) ?? []).toSorted(byPath)
|
|
746
|
+
});
|
|
747
|
+
const plainFile = (path) => ({
|
|
748
|
+
path,
|
|
749
|
+
entry: false,
|
|
750
|
+
root: false,
|
|
751
|
+
attached: []
|
|
752
|
+
});
|
|
753
|
+
return {
|
|
754
|
+
name,
|
|
755
|
+
files: [
|
|
756
|
+
...ordered.map(codeFile),
|
|
757
|
+
...others.map(plainFile),
|
|
758
|
+
...standalone.map(plainFile)
|
|
759
|
+
]
|
|
760
|
+
};
|
|
761
|
+
};
|
|
762
|
+
const transitiveDependencies = (name, dependsOn) => {
|
|
763
|
+
const found = /* @__PURE__ */ new Set();
|
|
764
|
+
const stack = [name];
|
|
765
|
+
while (stack.length > 0) {
|
|
766
|
+
for (const dependency of dependsOn.get(stack.pop()) ?? []) {
|
|
767
|
+
if (found.has(dependency) || dependency === name) continue;
|
|
768
|
+
found.add(dependency);
|
|
769
|
+
stack.push(dependency);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
return found;
|
|
773
|
+
};
|
|
774
|
+
const orderPackages = (names, dependsOn) => {
|
|
775
|
+
const remaining = new Set(names.filter((name) => name !== ""));
|
|
776
|
+
const dependencies = new Map(
|
|
777
|
+
[...remaining].map((name) => [
|
|
778
|
+
name,
|
|
779
|
+
transitiveDependencies(name, dependsOn)
|
|
780
|
+
])
|
|
781
|
+
);
|
|
782
|
+
const isDependedOnByRemaining = (name) => [...remaining].some(
|
|
783
|
+
(other) => other !== name && dependencies.get(other).has(name)
|
|
784
|
+
);
|
|
785
|
+
const ordered = names.includes("") ? [""] : [];
|
|
786
|
+
while (remaining.size > 0) {
|
|
787
|
+
const sorted = [...remaining].toSorted(
|
|
788
|
+
(left, right) => left.localeCompare(right)
|
|
789
|
+
);
|
|
790
|
+
const next = sorted.find((name) => !isDependedOnByRemaining(name)) ?? sorted[0];
|
|
791
|
+
ordered.push(next);
|
|
792
|
+
remaining.delete(next);
|
|
793
|
+
}
|
|
794
|
+
return ordered;
|
|
795
|
+
};
|
|
796
|
+
const orderForReading = ({
|
|
797
|
+
groups,
|
|
798
|
+
imports,
|
|
799
|
+
dependsOn
|
|
800
|
+
}) => {
|
|
801
|
+
const byName = new Map(groups.map((group) => [group.name, group]));
|
|
802
|
+
const changed = new Set(groups.flatMap(({ paths }) => paths));
|
|
803
|
+
return orderPackages(
|
|
804
|
+
groups.map(({ name }) => name),
|
|
805
|
+
dependsOn
|
|
806
|
+
).map((name) => orderGroup(byName.get(name), changed, imports));
|
|
807
|
+
};
|
|
808
|
+
|
|
809
|
+
const manifestDirectoryOf = (path, manifests) => {
|
|
810
|
+
const directories = path.split("/").slice(0, -1);
|
|
811
|
+
for (let depth = directories.length; depth >= 0; depth--) {
|
|
812
|
+
const directory = directories.slice(0, depth).join("/");
|
|
813
|
+
if (manifests.has(directory)) return directory;
|
|
814
|
+
}
|
|
815
|
+
return null;
|
|
816
|
+
};
|
|
817
|
+
const resolveLocalSpecifier = ({
|
|
818
|
+
from,
|
|
819
|
+
specifier,
|
|
820
|
+
exists,
|
|
821
|
+
manifests
|
|
822
|
+
}) => {
|
|
823
|
+
if (!specifier.startsWith("#")) {
|
|
824
|
+
return resolveRelative(from, specifier, exists);
|
|
825
|
+
}
|
|
826
|
+
const directory = manifestDirectoryOf(from, manifests);
|
|
827
|
+
if (directory === null) return null;
|
|
828
|
+
return resolveSubpathImport(
|
|
829
|
+
specifier,
|
|
830
|
+
{ directory, imports: manifests.get(directory)?.imports },
|
|
831
|
+
exists
|
|
832
|
+
);
|
|
833
|
+
};
|
|
834
|
+
const dependencyFields = [
|
|
835
|
+
"dependencies",
|
|
836
|
+
"devDependencies",
|
|
837
|
+
"peerDependencies"
|
|
838
|
+
];
|
|
839
|
+
const dependencyNamesOf = (manifest) => dependencyFields.flatMap((field) => {
|
|
840
|
+
const dependencies = manifest[field];
|
|
841
|
+
return typeof dependencies === "object" && dependencies !== null ? Object.keys(dependencies) : [];
|
|
842
|
+
});
|
|
843
|
+
const parseManifest = (source) => {
|
|
844
|
+
try {
|
|
845
|
+
const parsed = JSON.parse(source);
|
|
846
|
+
return typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
847
|
+
} catch {
|
|
848
|
+
return null;
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
const directoryOfManifest = (path) => path.includes("/") ? path.slice(0, path.lastIndexOf("/")) : "";
|
|
852
|
+
const readManifests = async (checkout, head, tree) => {
|
|
853
|
+
const manifestPaths = tree.filter(
|
|
854
|
+
(path) => (path === "package.json" || path.endsWith("/package.json")) && !path.split("/").includes("node_modules")
|
|
855
|
+
);
|
|
856
|
+
const contents = await catFiles(checkout, head, manifestPaths);
|
|
857
|
+
const manifests = /* @__PURE__ */ new Map();
|
|
858
|
+
for (const [path, source] of contents) {
|
|
859
|
+
const manifest = parseManifest(source);
|
|
860
|
+
if (manifest !== null) manifests.set(directoryOfManifest(path), manifest);
|
|
861
|
+
}
|
|
862
|
+
return manifests;
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
const maxUnchangedHops = 2;
|
|
866
|
+
const stringLiteral$1 = /^(["'`])(.*)\1$/s;
|
|
867
|
+
const importSpecifiers = (path, source) => {
|
|
868
|
+
const { staticImports, staticExports, dynamicImports } = parseSync(
|
|
869
|
+
path,
|
|
870
|
+
source
|
|
871
|
+
).module;
|
|
872
|
+
const found = [
|
|
873
|
+
...staticImports.map(({ start, moduleRequest }) => ({
|
|
874
|
+
start,
|
|
875
|
+
specifier: moduleRequest.value
|
|
876
|
+
})),
|
|
877
|
+
...staticExports.flatMap(
|
|
878
|
+
({ start, entries }) => entries.flatMap(
|
|
879
|
+
({ moduleRequest }) => moduleRequest === null ? [] : [{ start, specifier: moduleRequest.value }]
|
|
880
|
+
)
|
|
881
|
+
),
|
|
882
|
+
...dynamicImports.flatMap(({ start, moduleRequest }) => {
|
|
883
|
+
const literal = stringLiteral$1.exec(
|
|
884
|
+
source.slice(moduleRequest.start, moduleRequest.end)
|
|
885
|
+
);
|
|
886
|
+
const specifier = literal?.[2];
|
|
887
|
+
return specifier === void 0 || specifier.includes("${") ? [] : [{ start, specifier }];
|
|
888
|
+
})
|
|
889
|
+
];
|
|
890
|
+
return [
|
|
891
|
+
...new Set(
|
|
892
|
+
found.toSorted((left, right) => left.start - right.start).map(({ specifier }) => specifier)
|
|
893
|
+
)
|
|
894
|
+
];
|
|
895
|
+
};
|
|
896
|
+
const readImports = async ({
|
|
897
|
+
checkout,
|
|
898
|
+
head,
|
|
899
|
+
changed,
|
|
900
|
+
exists,
|
|
901
|
+
isPackage,
|
|
902
|
+
manifests
|
|
903
|
+
}) => {
|
|
904
|
+
const changedSet = new Set(changed);
|
|
905
|
+
const imports = /* @__PURE__ */ new Map();
|
|
906
|
+
let toRead = changed.filter((path) => isTypescriptPath(path) && exists(path));
|
|
907
|
+
for (let hop = 0; hop <= maxUnchangedHops && toRead.length > 0; hop++) {
|
|
908
|
+
const contents = await catFiles(checkout, head, toRead);
|
|
909
|
+
const next = /* @__PURE__ */ new Set();
|
|
910
|
+
for (const path of toRead) {
|
|
911
|
+
const source = contents.get(path);
|
|
912
|
+
const resolved = (source === void 0 ? [] : importSpecifiers(path, source)).map(
|
|
913
|
+
(specifier) => resolveLocalSpecifier({ from: path, specifier, exists, manifests })
|
|
914
|
+
).filter((target) => target !== null);
|
|
915
|
+
imports.set(path, resolved);
|
|
916
|
+
const ownPackage = packageOf(path, isPackage);
|
|
917
|
+
for (const target of resolved) {
|
|
918
|
+
if (changedSet.has(target) || imports.has(target)) continue;
|
|
919
|
+
if (packageOf(target, isPackage) === ownPackage) next.add(target);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
toRead = [...next];
|
|
923
|
+
}
|
|
924
|
+
return imports;
|
|
925
|
+
};
|
|
926
|
+
const packageDependencies = (manifests) => {
|
|
927
|
+
const directoryOfName = /* @__PURE__ */ new Map();
|
|
928
|
+
for (const [directory, { name }] of manifests) {
|
|
929
|
+
if (typeof name === "string") directoryOfName.set(name, directory);
|
|
930
|
+
}
|
|
931
|
+
return new Map(
|
|
932
|
+
[...manifests].map(([directory, manifest]) => [
|
|
933
|
+
directory,
|
|
934
|
+
new Set(
|
|
935
|
+
dependencyNamesOf(manifest).flatMap((name) => {
|
|
936
|
+
const dependency = directoryOfName.get(name);
|
|
937
|
+
return dependency === void 0 || dependency === directory ? [] : [dependency];
|
|
938
|
+
})
|
|
939
|
+
)
|
|
940
|
+
])
|
|
941
|
+
);
|
|
942
|
+
};
|
|
943
|
+
const readReadingOrder = async ({
|
|
944
|
+
checkout,
|
|
945
|
+
head,
|
|
946
|
+
base
|
|
947
|
+
}) => {
|
|
948
|
+
if (!isSha(head) || !isSha(base)) {
|
|
949
|
+
throw new Error("head and base must be commit shas");
|
|
950
|
+
}
|
|
951
|
+
await ensureCommits(checkout, [head, base], void 0);
|
|
952
|
+
const mergeBaseOutput = await git(checkout, ["merge-base", base, head]);
|
|
953
|
+
const mergeBase = mergeBaseOutput.trim();
|
|
954
|
+
const numstat = await readNumstat(checkout, [mergeBase, head], []);
|
|
955
|
+
const changed = numstat.map(({ path }) => path);
|
|
956
|
+
const tree = await listTree(checkout, head);
|
|
957
|
+
const inTree = new Set(tree);
|
|
958
|
+
const exists = (path) => inTree.has(path);
|
|
959
|
+
const isPackage = packagesInListing(tree);
|
|
960
|
+
const manifests = await readManifests(checkout, head, tree);
|
|
961
|
+
const imports = await readImports({
|
|
962
|
+
checkout,
|
|
963
|
+
head,
|
|
964
|
+
changed,
|
|
965
|
+
exists,
|
|
966
|
+
isPackage,
|
|
967
|
+
manifests
|
|
968
|
+
});
|
|
969
|
+
return {
|
|
970
|
+
groups: orderForReading({
|
|
971
|
+
groups: groupDiffPaths(isPackage, changed),
|
|
972
|
+
imports,
|
|
973
|
+
dependsOn: packageDependencies(manifests)
|
|
974
|
+
})
|
|
975
|
+
};
|
|
976
|
+
};
|
|
977
|
+
|
|
978
|
+
const stringLiteral = /^(["'`])(.*)\1$/s;
|
|
979
|
+
const lineStartsOf = (source) => {
|
|
980
|
+
const starts = [0];
|
|
981
|
+
for (let index = source.indexOf("\n"); index !== -1; ) {
|
|
982
|
+
starts.push(index + 1);
|
|
983
|
+
index = source.indexOf("\n", index + 1);
|
|
984
|
+
}
|
|
985
|
+
return starts;
|
|
986
|
+
};
|
|
987
|
+
const lineOf = (starts, offset) => {
|
|
988
|
+
let low = 0;
|
|
989
|
+
let high = starts.length - 1;
|
|
990
|
+
while (low < high) {
|
|
991
|
+
const middle = Math.ceil((low + high) / 2);
|
|
992
|
+
if (starts[middle] <= offset) low = middle;
|
|
993
|
+
else high = middle - 1;
|
|
994
|
+
}
|
|
995
|
+
return low + 1;
|
|
996
|
+
};
|
|
997
|
+
const rangeOf = (starts, { start, end }) => ({
|
|
998
|
+
start: lineOf(starts, start),
|
|
999
|
+
end: lineOf(starts, Math.max(start, end - 1))
|
|
1000
|
+
});
|
|
1001
|
+
const overlaps = (left, right) => left.start <= right.end && right.start <= left.end;
|
|
1002
|
+
const kindOf = ({ kind }) => String(kind);
|
|
1003
|
+
const nameOf = (entry) => kindOf(entry) === "Default" ? "default" : entry.name;
|
|
1004
|
+
const parse = (path, source) => ({
|
|
1005
|
+
result: parseSync(path, source),
|
|
1006
|
+
starts: lineStartsOf(source)
|
|
1007
|
+
});
|
|
1008
|
+
const importedNames = ({ result }) => {
|
|
1009
|
+
const names = /* @__PURE__ */ new Map();
|
|
1010
|
+
for (const { moduleRequest, entries } of result.module.staticImports) {
|
|
1011
|
+
for (const { importName, localName } of entries) {
|
|
1012
|
+
names.set(localName.value, {
|
|
1013
|
+
specifier: moduleRequest.value,
|
|
1014
|
+
imported: kindOf(importName) === "NamespaceObject" ? "*" : nameOf(importName) ?? "*"
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
return names;
|
|
1019
|
+
};
|
|
1020
|
+
const matchImport = ({ result }, { importName, moduleRequest }) => {
|
|
1021
|
+
const name = nameOf(importName) ?? "*";
|
|
1022
|
+
const statement = result.module.staticImports.find(
|
|
1023
|
+
({ moduleRequest: request }) => request.start === moduleRequest?.start
|
|
1024
|
+
);
|
|
1025
|
+
const entries = statement?.entries ?? [];
|
|
1026
|
+
const asDefault = entries.find(
|
|
1027
|
+
(entry) => kindOf(entry.importName) === "Default" && entry.localName.value === name
|
|
1028
|
+
);
|
|
1029
|
+
if (asDefault !== void 0) {
|
|
1030
|
+
return { name: "default", local: asDefault.localName.value };
|
|
1031
|
+
}
|
|
1032
|
+
const named = entries.find((entry) => entry.importName.name === name);
|
|
1033
|
+
return { name, local: named?.localName.value ?? null };
|
|
1034
|
+
};
|
|
1035
|
+
const isNode = (value) => typeof value === "object" && value !== null && typeof value.type === "string";
|
|
1036
|
+
const isExportList = (node) => node.type === "ExportNamedDeclaration" && node.declaration === null;
|
|
1037
|
+
const labelKeys = {
|
|
1038
|
+
MemberExpression: "property",
|
|
1039
|
+
Property: "key",
|
|
1040
|
+
PropertyDefinition: "key",
|
|
1041
|
+
MethodDefinition: "key",
|
|
1042
|
+
TSPropertySignature: "key",
|
|
1043
|
+
TSMethodSignature: "key",
|
|
1044
|
+
TSQualifiedName: "right",
|
|
1045
|
+
JSXMemberExpression: "property",
|
|
1046
|
+
JSXAttribute: "name"
|
|
1047
|
+
};
|
|
1048
|
+
const collectNames = (root, names) => {
|
|
1049
|
+
const stack = [root];
|
|
1050
|
+
while (stack.length > 0) {
|
|
1051
|
+
const value = stack.pop();
|
|
1052
|
+
if (Array.isArray(value)) {
|
|
1053
|
+
stack.push(...value);
|
|
1054
|
+
continue;
|
|
1055
|
+
}
|
|
1056
|
+
if (!isNode(value)) continue;
|
|
1057
|
+
if (value.type === "Identifier" || value.type === "JSXIdentifier") {
|
|
1058
|
+
if (typeof value.name === "string") names.add(value.name);
|
|
1059
|
+
stack.push(value.typeAnnotation);
|
|
1060
|
+
continue;
|
|
1061
|
+
}
|
|
1062
|
+
const labelKey = labelKeys[value.type];
|
|
1063
|
+
for (const [key, child] of Object.entries(value)) {
|
|
1064
|
+
if (key === labelKey && value.computed !== true) continue;
|
|
1065
|
+
if (typeof child === "object" && child !== null) stack.push(child);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
};
|
|
1069
|
+
const namesUsedLocally = ({ result }) => {
|
|
1070
|
+
const names = /* @__PURE__ */ new Set();
|
|
1071
|
+
for (const node of result.program.body) {
|
|
1072
|
+
const isListing = node.type === "ImportDeclaration" || node.type === "ExportAllDeclaration" || isExportList(node);
|
|
1073
|
+
if (!isListing) collectNames(node, names);
|
|
1074
|
+
}
|
|
1075
|
+
return names;
|
|
1076
|
+
};
|
|
1077
|
+
const readBindings = (parsed) => {
|
|
1078
|
+
const { result, starts } = parsed;
|
|
1079
|
+
const imported = importedNames(parsed);
|
|
1080
|
+
const bindings = [];
|
|
1081
|
+
const reExports = [];
|
|
1082
|
+
const passedOn = /* @__PURE__ */ new Set();
|
|
1083
|
+
for (const { start, end, entries } of result.module.staticExports) {
|
|
1084
|
+
for (const entry of entries) {
|
|
1085
|
+
const exported = nameOf(entry.exportName);
|
|
1086
|
+
const inStatement = entry.exportName.start === null || entry.exportName.start >= start && entry.exportName.start < end;
|
|
1087
|
+
const isListed = !inStatement || entries.length > 1 && entry.end < end;
|
|
1088
|
+
const lines = rangeOf(
|
|
1089
|
+
starts,
|
|
1090
|
+
isListed ? { start: entry.start, end: entry.end } : { start, end }
|
|
1091
|
+
);
|
|
1092
|
+
const importKind = kindOf(entry.importName);
|
|
1093
|
+
if (entry.moduleRequest !== null && importKind !== "None") {
|
|
1094
|
+
const { value: specifier } = entry.moduleRequest;
|
|
1095
|
+
if (importKind === "AllButDefault") {
|
|
1096
|
+
reExports.push({ specifier, local: "*", exported: "*", lines });
|
|
1097
|
+
} else if (exported !== null) {
|
|
1098
|
+
const matched = matchImport(parsed, entry);
|
|
1099
|
+
if (matched.local !== null && !inStatement) {
|
|
1100
|
+
passedOn.add(matched.local);
|
|
1101
|
+
}
|
|
1102
|
+
reExports.push({
|
|
1103
|
+
specifier,
|
|
1104
|
+
local: importKind === "All" ? "*" : matched.name,
|
|
1105
|
+
exported,
|
|
1106
|
+
lines
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1111
|
+
if (exported === null) continue;
|
|
1112
|
+
const local = entry.localName.name;
|
|
1113
|
+
const viaImport = local === null ? void 0 : imported.get(local);
|
|
1114
|
+
if (viaImport === void 0) {
|
|
1115
|
+
bindings.push({ name: exported, local, lines });
|
|
1116
|
+
} else {
|
|
1117
|
+
passedOn.add(local);
|
|
1118
|
+
reExports.push({
|
|
1119
|
+
specifier: viaImport.specifier,
|
|
1120
|
+
local: viaImport.imported,
|
|
1121
|
+
exported,
|
|
1122
|
+
lines
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
return { bindings, reExports, passedOn };
|
|
1128
|
+
};
|
|
1129
|
+
const exportedBindings = (path, source) => {
|
|
1130
|
+
const { bindings, reExports } = readBindings(parse(path, source));
|
|
1131
|
+
return { bindings, reExports };
|
|
1132
|
+
};
|
|
1133
|
+
const starSpecifiers = (reExports) => reExports.filter(({ local, exported }) => local === "*" && exported === "*").map(({ specifier }) => specifier);
|
|
1134
|
+
const exportedNames = ({
|
|
1135
|
+
bindings,
|
|
1136
|
+
reExports
|
|
1137
|
+
}) => [
|
|
1138
|
+
.../* @__PURE__ */ new Set([
|
|
1139
|
+
...bindings.map(({ name }) => name),
|
|
1140
|
+
...reExports.flatMap(
|
|
1141
|
+
({ exported }) => exported === "*" ? [] : [exported]
|
|
1142
|
+
)
|
|
1143
|
+
])
|
|
1144
|
+
];
|
|
1145
|
+
const importsOf = (parsed, source, passedOn) => {
|
|
1146
|
+
const { result } = parsed;
|
|
1147
|
+
const { staticImports, dynamicImports } = result.module;
|
|
1148
|
+
const usedLocally = namesUsedLocally(parsed);
|
|
1149
|
+
const found = [
|
|
1150
|
+
...staticImports.flatMap(({ start, moduleRequest, entries }) => {
|
|
1151
|
+
const used = entries.filter(
|
|
1152
|
+
({ localName }) => !passedOn.has(localName.value) || usedLocally.has(localName.value)
|
|
1153
|
+
);
|
|
1154
|
+
if (entries.length > 0 && used.length === 0) return [];
|
|
1155
|
+
const names = entries.length === 0 ? ["*"] : used.map(
|
|
1156
|
+
({ importName }) => kindOf(importName) === "NamespaceObject" ? "*" : nameOf(importName) ?? "*"
|
|
1157
|
+
);
|
|
1158
|
+
return [{ start, specifier: moduleRequest.value, names }];
|
|
1159
|
+
}),
|
|
1160
|
+
...dynamicImports.flatMap(({ start, moduleRequest }) => {
|
|
1161
|
+
const literal = stringLiteral.exec(
|
|
1162
|
+
source.slice(moduleRequest.start, moduleRequest.end)
|
|
1163
|
+
);
|
|
1164
|
+
const specifier = literal?.[2];
|
|
1165
|
+
return specifier === void 0 || specifier.includes("${") ? [] : [{ start, specifier, names: ["*"] }];
|
|
1166
|
+
})
|
|
1167
|
+
];
|
|
1168
|
+
const bySpecifier = /* @__PURE__ */ new Map();
|
|
1169
|
+
for (const { specifier, names } of found.toSorted(
|
|
1170
|
+
(left, right) => left.start - right.start
|
|
1171
|
+
)) {
|
|
1172
|
+
const merged = bySpecifier.get(specifier) ?? /* @__PURE__ */ new Set();
|
|
1173
|
+
for (const name of names) merged.add(name);
|
|
1174
|
+
bySpecifier.set(specifier, merged);
|
|
1175
|
+
}
|
|
1176
|
+
return [...bySpecifier].map(([specifier, names]) => ({
|
|
1177
|
+
specifier,
|
|
1178
|
+
names: [...names]
|
|
1179
|
+
}));
|
|
1180
|
+
};
|
|
1181
|
+
const moduleBindings = (path, source) => {
|
|
1182
|
+
const parsed = parse(path, source);
|
|
1183
|
+
const { bindings, reExports, passedOn } = readBindings(parsed);
|
|
1184
|
+
return { bindings, reExports, imports: importsOf(parsed, source, passedOn) };
|
|
1185
|
+
};
|
|
1186
|
+
const patternNames = (pattern, names) => {
|
|
1187
|
+
if (!isNode(pattern)) return;
|
|
1188
|
+
switch (pattern.type) {
|
|
1189
|
+
case "Identifier":
|
|
1190
|
+
names.push(String(pattern.name));
|
|
1191
|
+
break;
|
|
1192
|
+
case "ObjectPattern":
|
|
1193
|
+
for (const property of pattern.properties) {
|
|
1194
|
+
if (isNode(property)) {
|
|
1195
|
+
patternNames(property.value ?? property.argument, names);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
break;
|
|
1199
|
+
case "ArrayPattern":
|
|
1200
|
+
for (const element of pattern.elements) {
|
|
1201
|
+
patternNames(element, names);
|
|
1202
|
+
}
|
|
1203
|
+
break;
|
|
1204
|
+
case "AssignmentPattern":
|
|
1205
|
+
patternNames(pattern.left, names);
|
|
1206
|
+
break;
|
|
1207
|
+
case "RestElement":
|
|
1208
|
+
patternNames(pattern.argument, names);
|
|
1209
|
+
break;
|
|
1210
|
+
}
|
|
1211
|
+
};
|
|
1212
|
+
const declaredNamesOf = (statement) => {
|
|
1213
|
+
const names = [];
|
|
1214
|
+
if (statement.type === "VariableDeclaration") {
|
|
1215
|
+
for (const declarator of statement.declarations) {
|
|
1216
|
+
if (isNode(declarator)) patternNames(declarator.id, names);
|
|
1217
|
+
}
|
|
1218
|
+
} else if (statement.type === "FunctionDeclaration" || statement.type === "ClassDeclaration" || statement.type.startsWith("TS")) {
|
|
1219
|
+
patternNames(statement.id, names);
|
|
1220
|
+
}
|
|
1221
|
+
return names;
|
|
1222
|
+
};
|
|
1223
|
+
const statementsOf = ({ result, starts }) => result.program.body.map((node) => {
|
|
1224
|
+
const mentioned = /* @__PURE__ */ new Set();
|
|
1225
|
+
collectNames(node, mentioned);
|
|
1226
|
+
return {
|
|
1227
|
+
lines: rangeOf(starts, node),
|
|
1228
|
+
isList: isExportList(node),
|
|
1229
|
+
declared: declaredNamesOf(node),
|
|
1230
|
+
mentioned
|
|
1231
|
+
};
|
|
1232
|
+
});
|
|
1233
|
+
const touchedExports = ({
|
|
1234
|
+
path,
|
|
1235
|
+
source,
|
|
1236
|
+
touchedLines,
|
|
1237
|
+
baseExports,
|
|
1238
|
+
baseStars = []
|
|
1239
|
+
}) => {
|
|
1240
|
+
const parsed = parse(path, source);
|
|
1241
|
+
const { bindings, reExports } = readBindings(parsed);
|
|
1242
|
+
const statements = statementsOf(parsed);
|
|
1243
|
+
const isTouched = (lines) => touchedLines.some((touched2) => overlaps(touched2, lines));
|
|
1244
|
+
const exportedLocals = new Set(
|
|
1245
|
+
bindings.flatMap(({ local }) => local === null ? [] : [local])
|
|
1246
|
+
);
|
|
1247
|
+
const helpers = statements.filter(
|
|
1248
|
+
({ declared }) => declared.length > 0 && !declared.some((name) => exportedLocals.has(name))
|
|
1249
|
+
);
|
|
1250
|
+
const touchedHelperNames = new Set(
|
|
1251
|
+
helpers.filter(({ lines }) => isTouched(lines)).flatMap(({ declared }) => declared)
|
|
1252
|
+
);
|
|
1253
|
+
for (let grown = true; grown; ) {
|
|
1254
|
+
grown = false;
|
|
1255
|
+
for (const { declared, mentioned } of helpers) {
|
|
1256
|
+
if (declared.every((name) => touchedHelperNames.has(name))) continue;
|
|
1257
|
+
if (![...touchedHelperNames].some((name) => mentioned.has(name))) {
|
|
1258
|
+
continue;
|
|
1259
|
+
}
|
|
1260
|
+
for (const name of declared) touchedHelperNames.add(name);
|
|
1261
|
+
grown = true;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
const isBindingTouched = ({ local, lines }) => {
|
|
1265
|
+
const declaring = [
|
|
1266
|
+
...statements.filter(
|
|
1267
|
+
(statement) => !statement.isList && overlaps(statement.lines, lines)
|
|
1268
|
+
),
|
|
1269
|
+
...local === null ? [] : statements.filter(({ declared }) => declared.includes(local))
|
|
1270
|
+
];
|
|
1271
|
+
return isTouched(lines) || declaring.some(
|
|
1272
|
+
({ lines: declaringLines, mentioned }) => isTouched(declaringLines) || [...touchedHelperNames].some((name) => mentioned.has(name))
|
|
1273
|
+
);
|
|
1274
|
+
};
|
|
1275
|
+
const touched = /* @__PURE__ */ new Set([
|
|
1276
|
+
...bindings.filter(isBindingTouched).map(({ name }) => name),
|
|
1277
|
+
...reExports.filter(({ exported, lines }) => exported !== "*" && isTouched(lines)).map(({ exported }) => exported)
|
|
1278
|
+
]);
|
|
1279
|
+
const now = new Set(exportedNames({ bindings, reExports }));
|
|
1280
|
+
const stars = starSpecifiers(reExports);
|
|
1281
|
+
return {
|
|
1282
|
+
touched: [...touched],
|
|
1283
|
+
removed: baseExports.filter((name) => !now.has(name)),
|
|
1284
|
+
touchedStars: starSpecifiers(
|
|
1285
|
+
reExports.filter(({ lines }) => isTouched(lines))
|
|
1286
|
+
),
|
|
1287
|
+
removedStars: baseStars.filter((specifier) => !stars.includes(specifier))
|
|
1288
|
+
};
|
|
1289
|
+
};
|
|
1290
|
+
|
|
1291
|
+
const maxTypescriptFiles = 2e4;
|
|
1292
|
+
const maxReadMs = 15e3;
|
|
1293
|
+
const batchSize = 250;
|
|
1294
|
+
const diffChunkSize = 200;
|
|
1295
|
+
const maxTreeBuffer = 256 * 1024 * 1024;
|
|
1296
|
+
const defaultCacheEntries = 2 * maxTypescriptFiles;
|
|
1297
|
+
const createBindingsCache = (maxEntries = defaultCacheEntries) => {
|
|
1298
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1299
|
+
return {
|
|
1300
|
+
get: (oid) => {
|
|
1301
|
+
const summary = entries.get(oid);
|
|
1302
|
+
if (summary === void 0) return void 0;
|
|
1303
|
+
entries.delete(oid);
|
|
1304
|
+
entries.set(oid, summary);
|
|
1305
|
+
return summary;
|
|
1306
|
+
},
|
|
1307
|
+
set: (oid, summary) => {
|
|
1308
|
+
entries.delete(oid);
|
|
1309
|
+
entries.set(oid, summary);
|
|
1310
|
+
for (const oldest of entries.keys()) {
|
|
1311
|
+
if (entries.size <= maxEntries) break;
|
|
1312
|
+
entries.delete(oldest);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
};
|
|
1316
|
+
};
|
|
1317
|
+
const listBlobs = async (checkout, sha) => {
|
|
1318
|
+
const listing = await git(checkout, ["ls-tree", "-r", "-z", sha], {
|
|
1319
|
+
maxBuffer: maxTreeBuffer
|
|
1320
|
+
});
|
|
1321
|
+
return listing.split("\0").flatMap((record) => {
|
|
1322
|
+
const tab = record.indexOf(" ");
|
|
1323
|
+
if (tab === -1) return [];
|
|
1324
|
+
const [, type, oid] = record.slice(0, tab).split(" ");
|
|
1325
|
+
return type === "blob" && oid !== void 0 ? [{ path: record.slice(tab + 1), oid }] : [];
|
|
1326
|
+
});
|
|
1327
|
+
};
|
|
1328
|
+
const isInNodeModules = (path) => path.split("/").includes("node_modules");
|
|
1329
|
+
const emptySummary = { exports: [], imports: [], reExports: [] };
|
|
1330
|
+
const summarize = (path, source) => {
|
|
1331
|
+
const { bindings, reExports, imports } = moduleBindings(path, source);
|
|
1332
|
+
return {
|
|
1333
|
+
exports: exportedNames({ bindings, reExports }),
|
|
1334
|
+
imports,
|
|
1335
|
+
reExports
|
|
1336
|
+
};
|
|
1337
|
+
};
|
|
1338
|
+
const chunksOf = (items, size) => Array.from(
|
|
1339
|
+
{ length: Math.ceil(items.length / size) },
|
|
1340
|
+
(_, index) => items.slice(index * size, (index + 1) * size)
|
|
1341
|
+
);
|
|
1342
|
+
const readSummaries = async ({
|
|
1343
|
+
checkout,
|
|
1344
|
+
head,
|
|
1345
|
+
blobs,
|
|
1346
|
+
cache,
|
|
1347
|
+
budgetMs
|
|
1348
|
+
}) => {
|
|
1349
|
+
const summaries = /* @__PURE__ */ new Map();
|
|
1350
|
+
const missing = /* @__PURE__ */ new Map();
|
|
1351
|
+
for (const { oid, path } of blobs) {
|
|
1352
|
+
const cached = cache.get(oid);
|
|
1353
|
+
if (cached === void 0) missing.set(oid, path);
|
|
1354
|
+
else summaries.set(oid, cached);
|
|
1355
|
+
}
|
|
1356
|
+
const deadline = Date.now() + budgetMs;
|
|
1357
|
+
for (const batch of chunksOf([...missing], batchSize)) {
|
|
1358
|
+
if (Date.now() > deadline) return null;
|
|
1359
|
+
const contents = await catFiles(
|
|
1360
|
+
checkout,
|
|
1361
|
+
head,
|
|
1362
|
+
batch.map(([, path]) => path)
|
|
1363
|
+
);
|
|
1364
|
+
for (const [oid, path] of batch) {
|
|
1365
|
+
const source = contents.get(path);
|
|
1366
|
+
const summary = source === void 0 ? emptySummary : summarize(path, source);
|
|
1367
|
+
summaries.set(oid, summary);
|
|
1368
|
+
cache.set(oid, summary);
|
|
1369
|
+
}
|
|
1370
|
+
await setImmediate();
|
|
1371
|
+
}
|
|
1372
|
+
return summaries;
|
|
1373
|
+
};
|
|
1374
|
+
const hunkHeader = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/;
|
|
1375
|
+
const parseTouchedLines = (diff) => {
|
|
1376
|
+
const touched = /* @__PURE__ */ new Map();
|
|
1377
|
+
let current = null;
|
|
1378
|
+
for (const line of diff.split("\n")) {
|
|
1379
|
+
if (line.startsWith("diff --git ")) {
|
|
1380
|
+
current = null;
|
|
1381
|
+
} else if (line.startsWith("+++ ")) {
|
|
1382
|
+
const path = line.slice(4).split(" ")[0];
|
|
1383
|
+
if (path.startsWith("b/")) {
|
|
1384
|
+
current = touched.get(path.slice(2)) ?? [];
|
|
1385
|
+
touched.set(path.slice(2), current);
|
|
1386
|
+
}
|
|
1387
|
+
} else if (line.startsWith("@@")) {
|
|
1388
|
+
const match = hunkHeader.exec(line);
|
|
1389
|
+
if (current === null || match === null) continue;
|
|
1390
|
+
const start = Number(match[1]);
|
|
1391
|
+
const count = match[2] === void 0 ? 1 : Number(match[2]);
|
|
1392
|
+
current.push(
|
|
1393
|
+
count === 0 ? { start: start + 1, end: start + 1 } : { start, end: start + count - 1 }
|
|
1394
|
+
);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
return touched;
|
|
1398
|
+
};
|
|
1399
|
+
const readTouchedLines = async ({
|
|
1400
|
+
checkout,
|
|
1401
|
+
mergeBase,
|
|
1402
|
+
head,
|
|
1403
|
+
entries
|
|
1404
|
+
}) => {
|
|
1405
|
+
const touched = /* @__PURE__ */ new Map();
|
|
1406
|
+
for (const chunk of chunksOf(entries, diffChunkSize)) {
|
|
1407
|
+
const diff = await git(
|
|
1408
|
+
checkout,
|
|
1409
|
+
[
|
|
1410
|
+
"-c",
|
|
1411
|
+
"core.quotePath=false",
|
|
1412
|
+
"diff",
|
|
1413
|
+
"-U0",
|
|
1414
|
+
"-M",
|
|
1415
|
+
"--no-color",
|
|
1416
|
+
"--no-ext-diff",
|
|
1417
|
+
"--no-textconv",
|
|
1418
|
+
"--no-relative",
|
|
1419
|
+
"--src-prefix=a/",
|
|
1420
|
+
"--dst-prefix=b/",
|
|
1421
|
+
mergeBase,
|
|
1422
|
+
head,
|
|
1423
|
+
"--",
|
|
1424
|
+
...chunk.flatMap(
|
|
1425
|
+
({ paths }) => paths.map((path) => `:(literal)${path}`)
|
|
1426
|
+
)
|
|
1427
|
+
],
|
|
1428
|
+
{ maxBuffer: maxTreeBuffer }
|
|
1429
|
+
);
|
|
1430
|
+
for (const [path, ranges] of parseTouchedLines(diff)) {
|
|
1431
|
+
touched.set(path, ranges);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
return touched;
|
|
1435
|
+
};
|
|
1436
|
+
const splitPackageSpecifier = (specifier) => {
|
|
1437
|
+
const segments = specifier.split("/");
|
|
1438
|
+
const nameLength = specifier.startsWith("@") ? 2 : 1;
|
|
1439
|
+
const rest = segments.slice(nameLength).join("/");
|
|
1440
|
+
return {
|
|
1441
|
+
name: segments.slice(0, nameLength).join("/"),
|
|
1442
|
+
subpath: rest === "" ? "." : `./${rest}`
|
|
1443
|
+
};
|
|
1444
|
+
};
|
|
1445
|
+
const builtins = new Set(builtinModules);
|
|
1446
|
+
const urlScheme = /^[a-z][a-z0-9+.-]*:/;
|
|
1447
|
+
const typesPackageOf = (name) => `@types/${name.startsWith("@") ? name.slice(1).replace("/", "__") : name}`;
|
|
1448
|
+
const isConventionalAlias = (specifier) => specifier.startsWith("@/") || specifier.startsWith("~");
|
|
1449
|
+
const isBuiltinOrUrl = (specifier, name) => urlScheme.test(specifier) || specifier.startsWith("/") || builtins.has(specifier) || builtins.has(name);
|
|
1450
|
+
const external = { kind: "external" };
|
|
1451
|
+
const createResolver = ({
|
|
1452
|
+
exists,
|
|
1453
|
+
manifests
|
|
1454
|
+
}) => {
|
|
1455
|
+
const directoryOfName = /* @__PURE__ */ new Map();
|
|
1456
|
+
for (const [directory, { name }] of manifests) {
|
|
1457
|
+
if (typeof name === "string") directoryOfName.set(name, directory);
|
|
1458
|
+
}
|
|
1459
|
+
const dependenciesByDirectory = /* @__PURE__ */ new Map();
|
|
1460
|
+
const dependenciesOf = (directory) => {
|
|
1461
|
+
const known = dependenciesByDirectory.get(directory);
|
|
1462
|
+
if (known !== void 0) return known;
|
|
1463
|
+
const manifest = manifests.get(directory);
|
|
1464
|
+
const names = new Set(
|
|
1465
|
+
manifest === void 0 ? [] : dependencyNamesOf(manifest)
|
|
1466
|
+
);
|
|
1467
|
+
dependenciesByDirectory.set(directory, names);
|
|
1468
|
+
return names;
|
|
1469
|
+
};
|
|
1470
|
+
const isDependencyOf = (from, name) => {
|
|
1471
|
+
const nearest = manifestDirectoryOf(from, manifests);
|
|
1472
|
+
const wanted = [name, typesPackageOf(name)];
|
|
1473
|
+
return [nearest, ""].some(
|
|
1474
|
+
(directory) => directory !== null && wanted.some((candidate) => dependenciesOf(directory).has(candidate))
|
|
1475
|
+
);
|
|
1476
|
+
};
|
|
1477
|
+
const resolveTarget = (directory, target) => typeof target === "string" && target.startsWith("./") ? resolveRelative(`${directory}/package.json`, target, exists) : null;
|
|
1478
|
+
const resolveInPackage = (directory, subpath) => {
|
|
1479
|
+
const manifest = manifests.get(directory);
|
|
1480
|
+
const { exports } = manifest;
|
|
1481
|
+
if (exports === void 0) {
|
|
1482
|
+
return subpath === "." ? resolveTarget(directory, targetOf(manifest.types) ?? manifest.main) : resolveRelative(`${directory}/package.json`, subpath, exists);
|
|
1483
|
+
}
|
|
1484
|
+
const isSubpathMap = typeof exports === "object" && exports !== null && Object.keys(exports).some((key) => key.startsWith("."));
|
|
1485
|
+
if (!isSubpathMap) {
|
|
1486
|
+
return subpath === "." ? resolveTarget(directory, targetOf(exports)) : null;
|
|
1487
|
+
}
|
|
1488
|
+
const patterns = Object.entries(exports).filter(([pattern]) => pattern.startsWith(".")).toSorted(([left], [right]) => specificity(right) - specificity(left));
|
|
1489
|
+
for (const [pattern, value] of patterns) {
|
|
1490
|
+
const captured = expand(pattern, subpath);
|
|
1491
|
+
if (captured === null) continue;
|
|
1492
|
+
const target = targetOf(value);
|
|
1493
|
+
return resolveTarget(
|
|
1494
|
+
directory,
|
|
1495
|
+
typeof target === "string" ? target.replace("*", captured) : null
|
|
1496
|
+
);
|
|
1497
|
+
}
|
|
1498
|
+
return null;
|
|
1499
|
+
};
|
|
1500
|
+
return (from, specifier) => {
|
|
1501
|
+
if (specifier.startsWith(".") || specifier.startsWith("#")) {
|
|
1502
|
+
const path = resolveLocalSpecifier({
|
|
1503
|
+
from,
|
|
1504
|
+
specifier,
|
|
1505
|
+
exists,
|
|
1506
|
+
manifests
|
|
1507
|
+
});
|
|
1508
|
+
return path === null ? external : { kind: "file", path };
|
|
1509
|
+
}
|
|
1510
|
+
if (isConventionalAlias(specifier)) return { kind: "alias" };
|
|
1511
|
+
const { name, subpath } = splitPackageSpecifier(specifier);
|
|
1512
|
+
const directory = directoryOfName.get(name);
|
|
1513
|
+
if (directory !== void 0) {
|
|
1514
|
+
const path = resolveInPackage(directory, subpath);
|
|
1515
|
+
return path === null ? { kind: "workspaceMiss", directory } : { kind: "file", path };
|
|
1516
|
+
}
|
|
1517
|
+
if (isBuiltinOrUrl(specifier, name) || isDependencyOf(from, name)) {
|
|
1518
|
+
return external;
|
|
1519
|
+
}
|
|
1520
|
+
return { kind: "alias" };
|
|
1521
|
+
};
|
|
1522
|
+
};
|
|
1523
|
+
const maxStarHops = 4;
|
|
1524
|
+
const readSummariesAt = async ({
|
|
1525
|
+
checkout,
|
|
1526
|
+
ref,
|
|
1527
|
+
roots,
|
|
1528
|
+
resolveFile
|
|
1529
|
+
}) => {
|
|
1530
|
+
const found = /* @__PURE__ */ new Map();
|
|
1531
|
+
let toRead = [...new Set(roots)];
|
|
1532
|
+
for (let hop = 0; hop < maxStarHops && toRead.length > 0; hop++) {
|
|
1533
|
+
const contents = await catFiles(checkout, ref, toRead);
|
|
1534
|
+
const next = /* @__PURE__ */ new Set();
|
|
1535
|
+
for (const path of toRead) {
|
|
1536
|
+
const source = contents.get(path);
|
|
1537
|
+
if (source === void 0) continue;
|
|
1538
|
+
const summary = summarize(path, source);
|
|
1539
|
+
found.set(path, summary);
|
|
1540
|
+
for (const specifier of starSpecifiers(summary.reExports)) {
|
|
1541
|
+
const target = resolveFile(path, specifier);
|
|
1542
|
+
if (target !== null && !found.has(target)) next.add(target);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
toRead = [...next];
|
|
1546
|
+
}
|
|
1547
|
+
return found;
|
|
1548
|
+
};
|
|
1549
|
+
const starNames = ({
|
|
1550
|
+
from,
|
|
1551
|
+
specifier,
|
|
1552
|
+
summaryOf,
|
|
1553
|
+
resolveFile
|
|
1554
|
+
}) => {
|
|
1555
|
+
const names = /* @__PURE__ */ new Set();
|
|
1556
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1557
|
+
const visit = (importer, request) => {
|
|
1558
|
+
const target = resolveFile(importer, request);
|
|
1559
|
+
if (target === null) return false;
|
|
1560
|
+
if (visited.has(target)) return true;
|
|
1561
|
+
visited.add(target);
|
|
1562
|
+
const summary = summaryOf(target);
|
|
1563
|
+
if (summary === void 0) return false;
|
|
1564
|
+
for (const name of summary.exports) {
|
|
1565
|
+
if (name !== "default") names.add(name);
|
|
1566
|
+
}
|
|
1567
|
+
return starSpecifiers(summary.reExports).every(
|
|
1568
|
+
(next) => visit(target, next)
|
|
1569
|
+
);
|
|
1570
|
+
};
|
|
1571
|
+
return visit(from, specifier) ? [...names] : null;
|
|
1572
|
+
};
|
|
1573
|
+
const buildGraph = ({
|
|
1574
|
+
files,
|
|
1575
|
+
resolve,
|
|
1576
|
+
packageOfFile,
|
|
1577
|
+
changedPackages
|
|
1578
|
+
}) => {
|
|
1579
|
+
const importers = /* @__PURE__ */ new Map();
|
|
1580
|
+
const reExporters = /* @__PURE__ */ new Map();
|
|
1581
|
+
const unresolvedIn = /* @__PURE__ */ new Set();
|
|
1582
|
+
const push = (map, key, item) => {
|
|
1583
|
+
const items = map.get(key);
|
|
1584
|
+
if (items === void 0) map.set(key, [item]);
|
|
1585
|
+
else items.push(item);
|
|
1586
|
+
};
|
|
1587
|
+
for (const [path, { imports, reExports }] of files) {
|
|
1588
|
+
const reach = (specifier) => {
|
|
1589
|
+
const resolution = resolve(path, specifier);
|
|
1590
|
+
const importerPackage = packageOfFile(path);
|
|
1591
|
+
if (resolution.kind === "alias" && changedPackages.has(importerPackage) || resolution.kind === "workspaceMiss" && changedPackages.has(
|
|
1592
|
+
resolution.directory === "" ? "." : resolution.directory
|
|
1593
|
+
)) {
|
|
1594
|
+
unresolvedIn.add(importerPackage);
|
|
1595
|
+
}
|
|
1596
|
+
return resolution.kind === "file" ? resolution.path : null;
|
|
1597
|
+
};
|
|
1598
|
+
for (const { specifier, names } of imports) {
|
|
1599
|
+
const target = reach(specifier);
|
|
1600
|
+
if (target !== null) push(importers, target, { from: path, names });
|
|
1601
|
+
}
|
|
1602
|
+
for (const { specifier, local, exported } of reExports) {
|
|
1603
|
+
const target = reach(specifier);
|
|
1604
|
+
if (target !== null) {
|
|
1605
|
+
push(reExporters, target, { from: path, local, exported });
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
return { importers, reExporters, unresolvedIn };
|
|
1610
|
+
};
|
|
1611
|
+
const importersOf = ({ importers, reExporters }, file, name) => {
|
|
1612
|
+
const found = /* @__PURE__ */ new Set();
|
|
1613
|
+
const visited = /* @__PURE__ */ new Set([`${file}\0${name}`]);
|
|
1614
|
+
const stack = [[file, name]];
|
|
1615
|
+
while (stack.length > 0) {
|
|
1616
|
+
const [current, currentName] = stack.pop();
|
|
1617
|
+
for (const { from, names } of importers.get(current) ?? []) {
|
|
1618
|
+
if (names.includes(currentName) || names.includes("*")) found.add(from);
|
|
1619
|
+
}
|
|
1620
|
+
for (const { from, local, exported } of reExporters.get(current) ?? []) {
|
|
1621
|
+
const passesAll = local === "*" && exported === "*";
|
|
1622
|
+
if (local !== "*" && local !== currentName) continue;
|
|
1623
|
+
if (passesAll && currentName === "default") continue;
|
|
1624
|
+
const next = [
|
|
1625
|
+
from,
|
|
1626
|
+
exported === "*" ? currentName : exported
|
|
1627
|
+
];
|
|
1628
|
+
const key = `${next[0]}\0${next[1]}`;
|
|
1629
|
+
if (visited.has(key)) continue;
|
|
1630
|
+
visited.add(key);
|
|
1631
|
+
stack.push(next);
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
found.delete(file);
|
|
1635
|
+
return found;
|
|
1636
|
+
};
|
|
1637
|
+
const readImpacts = async ({
|
|
1638
|
+
checkout,
|
|
1639
|
+
head,
|
|
1640
|
+
base,
|
|
1641
|
+
cache,
|
|
1642
|
+
maxFiles = maxTypescriptFiles,
|
|
1643
|
+
maxReadMs: budgetMs = maxReadMs
|
|
1644
|
+
}) => {
|
|
1645
|
+
if (!isSha(head) || !isSha(base)) {
|
|
1646
|
+
throw new Error("head and base must be commit shas");
|
|
1647
|
+
}
|
|
1648
|
+
await ensureCommits(checkout, [head, base], void 0);
|
|
1649
|
+
const mergeBaseOutput = await git(checkout, ["merge-base", base, head]);
|
|
1650
|
+
const mergeBase = mergeBaseOutput.trim();
|
|
1651
|
+
const numstat = await readNumstat(checkout, [mergeBase, head], []);
|
|
1652
|
+
const blobs = await listBlobs(checkout, head);
|
|
1653
|
+
const treePaths = blobs.map(({ path }) => path);
|
|
1654
|
+
const inTree = new Set(treePaths);
|
|
1655
|
+
const typescriptBlobs = blobs.filter(
|
|
1656
|
+
({ path }) => isTypescriptPath(path) && !isInNodeModules(path)
|
|
1657
|
+
);
|
|
1658
|
+
if (typescriptBlobs.length > maxFiles) {
|
|
1659
|
+
return { tooLarge: true, reason: "files" };
|
|
1660
|
+
}
|
|
1661
|
+
const summaries = await readSummaries({
|
|
1662
|
+
checkout,
|
|
1663
|
+
head,
|
|
1664
|
+
blobs: typescriptBlobs,
|
|
1665
|
+
cache,
|
|
1666
|
+
budgetMs
|
|
1667
|
+
});
|
|
1668
|
+
if (summaries === null) return { tooLarge: true, reason: "time" };
|
|
1669
|
+
const manifests = await readManifests(checkout, head, treePaths);
|
|
1670
|
+
const files = new Map(
|
|
1671
|
+
typescriptBlobs.map(({ path, oid }) => [path, summaries.get(oid)])
|
|
1672
|
+
);
|
|
1673
|
+
const packageOfFile = (path) => manifestDirectoryOf(path, manifests) || ".";
|
|
1674
|
+
const analyzed = numstat.filter(({ path }) => files.has(path));
|
|
1675
|
+
const resolve = createResolver({
|
|
1676
|
+
exists: (path) => inTree.has(path),
|
|
1677
|
+
manifests
|
|
1678
|
+
});
|
|
1679
|
+
const resolveFile = (from, specifier) => {
|
|
1680
|
+
const resolution = resolve(from, specifier);
|
|
1681
|
+
return resolution.kind === "file" ? resolution.path : null;
|
|
1682
|
+
};
|
|
1683
|
+
const graph = buildGraph({
|
|
1684
|
+
files,
|
|
1685
|
+
resolve,
|
|
1686
|
+
packageOfFile,
|
|
1687
|
+
changedPackages: new Set(analyzed.map(({ path }) => packageOfFile(path)))
|
|
1688
|
+
});
|
|
1689
|
+
const changedSet = new Set(numstat.map(({ path }) => path));
|
|
1690
|
+
const notAnalyzed = numstat.filter(({ path }) => !files.has(path)).map(({ path }) => path);
|
|
1691
|
+
const touchedLines = await readTouchedLines({
|
|
1692
|
+
checkout,
|
|
1693
|
+
mergeBase,
|
|
1694
|
+
head,
|
|
1695
|
+
entries: analyzed
|
|
1696
|
+
});
|
|
1697
|
+
const sources = await catFiles(
|
|
1698
|
+
checkout,
|
|
1699
|
+
head,
|
|
1700
|
+
analyzed.map(({ path }) => path)
|
|
1701
|
+
);
|
|
1702
|
+
const baseSources = await catFiles(
|
|
1703
|
+
checkout,
|
|
1704
|
+
mergeBase,
|
|
1705
|
+
analyzed.map(({ paths }) => paths[0])
|
|
1706
|
+
);
|
|
1707
|
+
const impacts = [];
|
|
1708
|
+
for (const { path, paths } of analyzed) {
|
|
1709
|
+
const source = sources.get(path);
|
|
1710
|
+
if (source === void 0) {
|
|
1711
|
+
notAnalyzed.push(path);
|
|
1712
|
+
continue;
|
|
1713
|
+
}
|
|
1714
|
+
const baseSource = baseSources.get(paths[0]);
|
|
1715
|
+
const baseBindings = baseSource === void 0 ? void 0 : exportedBindings(path, baseSource);
|
|
1716
|
+
const { touched, removed, touchedStars, removedStars } = touchedExports({
|
|
1717
|
+
path,
|
|
1718
|
+
source,
|
|
1719
|
+
touchedLines: touchedLines.get(path) ?? [],
|
|
1720
|
+
baseExports: baseBindings === void 0 ? [] : exportedNames(baseBindings),
|
|
1721
|
+
baseStars: baseBindings === void 0 ? [] : starSpecifiers(baseBindings.reExports)
|
|
1722
|
+
});
|
|
1723
|
+
let starsUnknown = false;
|
|
1724
|
+
const known = (names) => {
|
|
1725
|
+
if (names === null) starsUnknown = true;
|
|
1726
|
+
return names ?? [];
|
|
1727
|
+
};
|
|
1728
|
+
const starNamesAtHead = (specifier) => known(
|
|
1729
|
+
starNames({
|
|
1730
|
+
from: path,
|
|
1731
|
+
specifier,
|
|
1732
|
+
summaryOf: (target) => files.get(target),
|
|
1733
|
+
resolveFile
|
|
1734
|
+
})
|
|
1735
|
+
);
|
|
1736
|
+
const baseSummaries = removedStars.length === 0 ? /* @__PURE__ */ new Map() : await readSummariesAt({
|
|
1737
|
+
checkout,
|
|
1738
|
+
ref: mergeBase,
|
|
1739
|
+
roots: removedStars.flatMap(
|
|
1740
|
+
(specifier) => resolveFile(paths[0], specifier) ?? []
|
|
1741
|
+
),
|
|
1742
|
+
resolveFile
|
|
1743
|
+
});
|
|
1744
|
+
const removedByStars = removedStars.flatMap(
|
|
1745
|
+
(specifier) => known(
|
|
1746
|
+
starNames({
|
|
1747
|
+
from: paths[0],
|
|
1748
|
+
specifier,
|
|
1749
|
+
summaryOf: (target) => baseSummaries.get(target),
|
|
1750
|
+
resolveFile
|
|
1751
|
+
})
|
|
1752
|
+
)
|
|
1753
|
+
);
|
|
1754
|
+
const headSummary = files.get(path);
|
|
1755
|
+
const stillExported = /* @__PURE__ */ new Set([
|
|
1756
|
+
...headSummary.exports,
|
|
1757
|
+
...removed.length + removedByStars.length === 0 ? [] : starSpecifiers(headSummary.reExports).flatMap(starNamesAtHead)
|
|
1758
|
+
]);
|
|
1759
|
+
const touchedNames = /* @__PURE__ */ new Set([
|
|
1760
|
+
...touched,
|
|
1761
|
+
...touchedStars.flatMap(starNamesAtHead)
|
|
1762
|
+
]);
|
|
1763
|
+
const removedNames = new Set(
|
|
1764
|
+
[...removed, ...removedByStars].filter(
|
|
1765
|
+
(name) => !stillExported.has(name)
|
|
1766
|
+
)
|
|
1767
|
+
);
|
|
1768
|
+
if (starsUnknown && !notAnalyzed.includes(path)) notAnalyzed.push(path);
|
|
1769
|
+
const exports = [
|
|
1770
|
+
...[...touchedNames].map((name) => ({ name, removed: false })),
|
|
1771
|
+
...[...removedNames].map((name) => ({ name, removed: true }))
|
|
1772
|
+
].flatMap(({ name, removed: isRemoved }) => {
|
|
1773
|
+
const importers = [...importersOf(graph, path, name)].filter((importer) => !changedSet.has(importer)).toSorted((left, right) => left.localeCompare(right)).map((importer) => ({
|
|
1774
|
+
path: importer,
|
|
1775
|
+
test: isAttachablePath(importer)
|
|
1776
|
+
}));
|
|
1777
|
+
return importers.length === 0 ? [] : [{ name, removed: isRemoved, importers }];
|
|
1778
|
+
});
|
|
1779
|
+
if (exports.length > 0) impacts.push({ path, exports });
|
|
1780
|
+
}
|
|
1781
|
+
return {
|
|
1782
|
+
tooLarge: false,
|
|
1783
|
+
files: impacts,
|
|
1784
|
+
notAnalyzed,
|
|
1785
|
+
unresolvedIn: [...graph.unresolvedIn].toSorted(
|
|
1786
|
+
(left, right) => left.localeCompare(right)
|
|
1787
|
+
)
|
|
1788
|
+
};
|
|
1789
|
+
};
|
|
1790
|
+
|
|
1791
|
+
export { createBindingsCache, defaultDiffLimits, git, gitFailedCode, gitOrNull, groupDiffPaths, maxTypescriptFiles, noPackages, packagesAtTree, packagesOnDisk, parseNumstat, readBranchDiff, readFileAtSha, readImpacts, readPullRequestDiff, readReadingOrder, reasonLine };
|
|
1792
|
+
//# sourceMappingURL=git-node.mjs.map
|