@blzsky/variant 0.2.1 → 0.3.0-canary.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/README.md +110 -56
- package/dist/{build-3DSBMMI3.js → build-GKWNLU4Q.js} +9 -8
- package/dist/{check-SIQJU6Z2.js → check-KO5VY67Z.js} +6 -6
- package/dist/{chunk-FA64KWJ2.js → chunk-7NC7AO75.js} +15 -28
- package/dist/{chunk-VYWPQKMS.js → chunk-ACTY42RE.js} +20 -0
- package/dist/{chunk-ZH2PQUNL.js → chunk-CN25SRKD.js} +6 -6
- package/dist/{chunk-43VLELTL.js → chunk-CWF6QXEW.js} +1 -1
- package/dist/{chunk-7OPTWU6E.js → chunk-DMDAJPFJ.js} +1 -1
- package/dist/{chunk-RC23JQKI.js → chunk-DSQDMFQ6.js} +9 -1
- package/dist/chunk-H5C4SE73.js +184 -0
- package/dist/chunk-IFFZ46YS.js +575 -0
- package/dist/chunk-IHXJ72XA.js +31 -0
- package/dist/chunk-KCEGA56S.js +670 -0
- package/dist/{chunk-FZEXZBXB.js → chunk-MAK3HG46.js} +41 -9
- package/dist/chunk-MT4BOAAV.js +167 -0
- package/dist/{chunk-TCISS2AS.js → chunk-NFFXYYM6.js} +120 -25
- package/dist/{chunk-26DHEXGS.js → chunk-RJBFCURK.js} +1 -1
- package/dist/{chunk-6BXSZPHC.js → chunk-RSIOYQOQ.js} +67 -8
- package/dist/chunk-SGP5JX6I.js +377 -0
- package/dist/chunk-X3VVGTEV.js +268 -0
- package/dist/{chunk-Q7RLUZSN.js → chunk-YII4TAKS.js} +10 -3
- package/dist/cli.js +62 -27
- package/dist/{diff-HSJBZFMC.js → diff-A3TQWWUJ.js} +5 -5
- package/dist/{doctor-B7MHFIYJ.js → doctor-6ONS6LKY.js} +4 -4
- package/dist/{env-I7JZRWDN.js → env-XF2US3CB.js} +6 -6
- package/dist/git-W6N3WMYQ.js +24 -0
- package/dist/impact-DPZIAJR4.js +263 -0
- package/dist/index.js +2 -2
- package/dist/init-VNK5E4H3.js +777 -0
- package/dist/{insight-JQLTGFYB.js → insight-LNYVMCAE.js} +7 -7
- package/dist/pr-6CYMM2XU.js +707 -0
- package/dist/{run-C6VAQOVM.js → run-44PKMOV5.js} +9 -8
- package/dist/{symbol-graph-E5GXKDFZ.js → symbol-graph-OB4AJCLW.js} +3 -3
- package/dist/{tsconfig-paths-E3UEDGXZ.js → tsconfig-paths-SV3PULCW.js} +3 -1
- package/dist/vitest.cjs +629 -0
- package/dist/vitest.d.cts +181 -0
- package/dist/vitest.d.ts +181 -0
- package/dist/vitest.js +356 -0
- package/dist/workspace-PIQXG4ZD.js +65 -0
- package/package.json +13 -2
- package/dist/chunk-DRMASVYN.js +0 -25
- package/dist/chunk-KSHWC2XN.js +0 -78
- package/dist/chunk-XTYVVUWW.js +0 -279
- package/dist/impact-PRZ6HEP5.js +0 -735
- package/dist/init-O2BY3FOY.js +0 -158
- package/dist/pr-Y724QAHE.js +0 -165
- package/dist/workspace-HRTFGWKN.js +0 -186
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
resolveCommit
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-RSIOYQOQ.js";
|
|
4
4
|
import {
|
|
5
5
|
collectExportedSurface,
|
|
6
6
|
significantText
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-DSQDMFQ6.js";
|
|
8
8
|
import {
|
|
9
9
|
GitRefError
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-ACTY42RE.js";
|
|
11
11
|
|
|
12
12
|
// src/core/vcs/base-ref.ts
|
|
13
13
|
async function assertBaseRef(cwd, ref) {
|
|
@@ -25,18 +25,22 @@ function createClassifier() {
|
|
|
25
25
|
const tsm2 = await import("ts-morph");
|
|
26
26
|
return {
|
|
27
27
|
tsm: tsm2,
|
|
28
|
-
project: new tsm2.Project({
|
|
28
|
+
project: new tsm2.Project({
|
|
29
|
+
useInMemoryFileSystem: true,
|
|
30
|
+
compilerOptions: { allowJs: true }
|
|
31
|
+
})
|
|
29
32
|
};
|
|
30
33
|
})();
|
|
31
34
|
const { tsm, project } = await host;
|
|
32
35
|
const id = nextId++;
|
|
36
|
+
const ext = sourceExtension(input.filePath);
|
|
33
37
|
const beforeFile = project.createSourceFile(
|
|
34
|
-
`__before_${id}
|
|
38
|
+
`__before_${id}${ext}`,
|
|
35
39
|
input.before,
|
|
36
40
|
{ overwrite: true }
|
|
37
41
|
);
|
|
38
42
|
const afterFile = project.createSourceFile(
|
|
39
|
-
`__after_${id}
|
|
43
|
+
`__after_${id}${ext}`,
|
|
40
44
|
input.after,
|
|
41
45
|
{ overwrite: true }
|
|
42
46
|
);
|
|
@@ -76,15 +80,23 @@ function compareSurfaces(input, beforeFile, afterFile, tsm) {
|
|
|
76
80
|
if (!afterApi.symbols.has(name)) removed.push(name);
|
|
77
81
|
}
|
|
78
82
|
const surfaceChanged = added.length > 0 || removed.length > 0 || changed.some((c) => c.kind !== "body");
|
|
83
|
+
const parseNotes = [
|
|
84
|
+
...hasParseErrors(beforeFile) ? ["base"] : [],
|
|
85
|
+
...hasParseErrors(afterFile) ? ["working-tree"] : []
|
|
86
|
+
].map(
|
|
87
|
+
(side) => `the ${side} version does not parse; classified as breaking without trusting the comparison`
|
|
88
|
+
);
|
|
79
89
|
let classification;
|
|
80
|
-
if (surfaceChanged) {
|
|
90
|
+
if (surfaceChanged || parseNotes.length > 0) {
|
|
81
91
|
classification = "breaking";
|
|
82
92
|
} else if (changed.length > 0 || significantText(input.before, tsm) !== significantText(input.after, tsm)) {
|
|
83
93
|
classification = "internal";
|
|
84
94
|
} else {
|
|
85
95
|
classification = "non-impacting";
|
|
86
96
|
}
|
|
87
|
-
const confidenceNotes = [
|
|
97
|
+
const confidenceNotes = [
|
|
98
|
+
.../* @__PURE__ */ new Set([...parseNotes, ...beforeApi.notes, ...afterApi.notes])
|
|
99
|
+
];
|
|
88
100
|
const confidence = Math.max(
|
|
89
101
|
0.3,
|
|
90
102
|
Math.round((1 - confidenceNotes.length * 0.15) * 100) / 100
|
|
@@ -94,9 +106,29 @@ function compareSurfaces(input, beforeFile, afterFile, tsm) {
|
|
|
94
106
|
classification,
|
|
95
107
|
exportedSymbols: { added, removed, changed },
|
|
96
108
|
confidence,
|
|
97
|
-
confidenceNotes
|
|
109
|
+
confidenceNotes,
|
|
110
|
+
syntaxErrors: parseNotes.length > 0
|
|
98
111
|
};
|
|
99
112
|
}
|
|
113
|
+
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
114
|
+
".ts",
|
|
115
|
+
".tsx",
|
|
116
|
+
".mts",
|
|
117
|
+
".cts",
|
|
118
|
+
".js",
|
|
119
|
+
".jsx",
|
|
120
|
+
".mjs",
|
|
121
|
+
".cjs"
|
|
122
|
+
]);
|
|
123
|
+
function sourceExtension(filePath) {
|
|
124
|
+
const dot = filePath.lastIndexOf(".");
|
|
125
|
+
const ext = dot === -1 ? "" : filePath.slice(dot);
|
|
126
|
+
return SOURCE_EXTENSIONS.has(ext) ? ext : ".ts";
|
|
127
|
+
}
|
|
128
|
+
function hasParseErrors(file) {
|
|
129
|
+
const { parseDiagnostics } = file.compilerNode;
|
|
130
|
+
return (parseDiagnostics?.length ?? 0) > 0;
|
|
131
|
+
}
|
|
100
132
|
|
|
101
133
|
export {
|
|
102
134
|
createClassifier,
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import {
|
|
2
|
+
appendReconciliation,
|
|
3
|
+
defaultHistoryDir,
|
|
4
|
+
readImpactPredictions
|
|
5
|
+
} from "./chunk-H5C4SE73.js";
|
|
6
|
+
import {
|
|
7
|
+
ImpactReportError
|
|
8
|
+
} from "./chunk-ACTY42RE.js";
|
|
9
|
+
|
|
10
|
+
// src/core/impact/verify.ts
|
|
11
|
+
import { realpathSync } from "fs";
|
|
12
|
+
import { readFile } from "fs/promises";
|
|
13
|
+
import path from "path";
|
|
14
|
+
function realpathOrSelf(target) {
|
|
15
|
+
try {
|
|
16
|
+
return realpathSync.native(target);
|
|
17
|
+
} catch {
|
|
18
|
+
return target;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function escapes(relative) {
|
|
22
|
+
return relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative);
|
|
23
|
+
}
|
|
24
|
+
function toRelativePosix(cwd, testPath, realpath) {
|
|
25
|
+
let relative = testPath;
|
|
26
|
+
if (path.isAbsolute(testPath)) {
|
|
27
|
+
relative = path.relative(cwd, testPath);
|
|
28
|
+
if (escapes(relative)) {
|
|
29
|
+
relative = path.relative(realpath(cwd), realpath(testPath));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return relative.replace(/\\/g, "/");
|
|
33
|
+
}
|
|
34
|
+
function parseTestRun(cwd, raw, realpath = realpathOrSelf) {
|
|
35
|
+
let parsed;
|
|
36
|
+
try {
|
|
37
|
+
parsed = JSON.parse(raw);
|
|
38
|
+
} catch {
|
|
39
|
+
throw new ImpactReportError("it is not valid JSON");
|
|
40
|
+
}
|
|
41
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
42
|
+
throw new ImpactReportError("it is not a JSON object");
|
|
43
|
+
}
|
|
44
|
+
const results = parsed.testResults;
|
|
45
|
+
if (!Array.isArray(results)) {
|
|
46
|
+
throw new ImpactReportError("it has no `testResults` array");
|
|
47
|
+
}
|
|
48
|
+
const ran = /* @__PURE__ */ new Set();
|
|
49
|
+
const failed = /* @__PURE__ */ new Set();
|
|
50
|
+
for (const entry of results) {
|
|
51
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
52
|
+
const { name, status } = entry;
|
|
53
|
+
if (typeof name !== "string") continue;
|
|
54
|
+
const file = toRelativePosix(cwd, name, realpath);
|
|
55
|
+
ran.add(file);
|
|
56
|
+
if (status === "failed") failed.add(file);
|
|
57
|
+
}
|
|
58
|
+
return { ran: [...ran].sort(), failed: [...failed].sort() };
|
|
59
|
+
}
|
|
60
|
+
function testRunFrom(cwd, results, realpath = realpathOrSelf) {
|
|
61
|
+
const ran = /* @__PURE__ */ new Set();
|
|
62
|
+
const failed = /* @__PURE__ */ new Set();
|
|
63
|
+
for (const result of results) {
|
|
64
|
+
const file = toRelativePosix(cwd, result.file, realpath);
|
|
65
|
+
ran.add(file);
|
|
66
|
+
if (result.failed) failed.add(file);
|
|
67
|
+
}
|
|
68
|
+
return { ran: [...ran].sort(), failed: [...failed].sort() };
|
|
69
|
+
}
|
|
70
|
+
function reconcile(prediction, failedTests) {
|
|
71
|
+
const selected = new Set(prediction.affectedTests);
|
|
72
|
+
const caught = [];
|
|
73
|
+
const falseSkips = [];
|
|
74
|
+
for (const test of failedTests) {
|
|
75
|
+
if (prediction.selectAll || selected.has(test)) caught.push(test);
|
|
76
|
+
else falseSkips.push(test);
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
caught,
|
|
80
|
+
falseSkips,
|
|
81
|
+
falseSkipRate: failedTests.length === 0 ? 0 : falseSkips.length / failedTests.length
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function selectPrediction(predictions, headSha) {
|
|
85
|
+
if (headSha !== void 0) {
|
|
86
|
+
for (let i = predictions.length - 1; i >= 0; i--) {
|
|
87
|
+
const candidate = predictions[i];
|
|
88
|
+
if (candidate?.headSha === headSha) {
|
|
89
|
+
return { prediction: candidate, matchedBy: "head-sha" };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const latest = predictions[predictions.length - 1];
|
|
95
|
+
return latest === void 0 ? null : { prediction: latest, matchedBy: "most-recent" };
|
|
96
|
+
}
|
|
97
|
+
async function readReport(cwd, reportPath) {
|
|
98
|
+
try {
|
|
99
|
+
return await readFile(path.resolve(cwd, reportPath), "utf8");
|
|
100
|
+
} catch (error) {
|
|
101
|
+
const code = error instanceof Error && "code" in error ? String(error.code) : "";
|
|
102
|
+
throw new ImpactReportError(
|
|
103
|
+
code === "ENOENT" ? `no file at ${reportPath}` : `${reportPath} could not be read${code === "" ? "" : ` (${code})`}`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async function verifyImpact(cwd, reportPaths, options = {}) {
|
|
108
|
+
const paths = typeof reportPaths === "string" ? [reportPaths] : reportPaths;
|
|
109
|
+
return verifyRun(
|
|
110
|
+
cwd,
|
|
111
|
+
async () => {
|
|
112
|
+
const reports = await Promise.all(
|
|
113
|
+
paths.map((reportPath) => readReport(cwd, reportPath))
|
|
114
|
+
);
|
|
115
|
+
return reports.map((raw) => parseTestRun(cwd, raw));
|
|
116
|
+
},
|
|
117
|
+
options
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
async function verifyRun(cwd, runs, options = {}) {
|
|
121
|
+
const historyDir = defaultHistoryDir(cwd);
|
|
122
|
+
const predictions = options.predictions ?? await readImpactPredictions(historyDir);
|
|
123
|
+
const match = selectPrediction(predictions, options.headSha);
|
|
124
|
+
if (match === null) return null;
|
|
125
|
+
const resolved = typeof runs === "function" ? await runs() : Array.isArray(runs) ? runs : [runs];
|
|
126
|
+
const failedTests = [
|
|
127
|
+
...new Set(resolved.flatMap((run) => run.failed))
|
|
128
|
+
].sort();
|
|
129
|
+
const ran = new Set(resolved.flatMap((run) => run.ran));
|
|
130
|
+
const selected = new Set(match.prediction.affectedTests);
|
|
131
|
+
const predictedRan = match.prediction.selectAll ? ran.size : [...ran].filter((test) => selected.has(test)).length;
|
|
132
|
+
const { caught, falseSkips, falseSkipRate } = reconcile(
|
|
133
|
+
match.prediction,
|
|
134
|
+
failedTests
|
|
135
|
+
);
|
|
136
|
+
const historyLogged = options.log === false ? true : await appendReconciliation(historyDir, {
|
|
137
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
138
|
+
headSha: match.prediction.headSha,
|
|
139
|
+
baseRef: match.prediction.baseRef,
|
|
140
|
+
predictedTests: match.prediction.affectedTests.length,
|
|
141
|
+
totalTests: match.prediction.totalTests,
|
|
142
|
+
failedTests: failedTests.length,
|
|
143
|
+
caught: caught.length,
|
|
144
|
+
falseSkips: falseSkips.length,
|
|
145
|
+
confidence: match.prediction.confidence,
|
|
146
|
+
selectAll: match.prediction.selectAll,
|
|
147
|
+
ranTests: ran.size,
|
|
148
|
+
predictedRan
|
|
149
|
+
});
|
|
150
|
+
return {
|
|
151
|
+
prediction: match.prediction,
|
|
152
|
+
matchedBy: match.matchedBy,
|
|
153
|
+
failedTests,
|
|
154
|
+
caught,
|
|
155
|
+
falseSkips,
|
|
156
|
+
falseSkipRate,
|
|
157
|
+
ranTests: ran.size,
|
|
158
|
+
predictedRan,
|
|
159
|
+
historyLogged
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export {
|
|
164
|
+
testRunFrom,
|
|
165
|
+
verifyImpact,
|
|
166
|
+
verifyRun
|
|
167
|
+
};
|
|
@@ -1,24 +1,52 @@
|
|
|
1
1
|
import {
|
|
2
2
|
collectExportedSurface
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-DSQDMFQ6.js";
|
|
4
4
|
import {
|
|
5
5
|
GraphError
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-ACTY42RE.js";
|
|
7
7
|
|
|
8
8
|
// src/core/semantic/symbol-graph.ts
|
|
9
9
|
import { createHash } from "crypto";
|
|
10
10
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
11
11
|
import path from "path";
|
|
12
|
-
var SYMBOL_GRAPH_VERSION =
|
|
12
|
+
var SYMBOL_GRAPH_VERSION = 5;
|
|
13
13
|
var GRAPH_FILENAME = "symbols.json";
|
|
14
14
|
var DEFAULT_INCLUDE = ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"];
|
|
15
|
+
var JAVASCRIPT_INCLUDE = ["**/*.js", "**/*.jsx", "**/*.mjs", "**/*.cjs"];
|
|
16
|
+
var JAVASCRIPT_PATHSPECS = ["*.js", "*.jsx", "*.mjs", "*.cjs"];
|
|
17
|
+
var MAX_JAVASCRIPT_BYTES = 512 * 1024;
|
|
15
18
|
var DEFAULT_IGNORE = [
|
|
16
|
-
"node_modules/**",
|
|
19
|
+
"**/node_modules/**",
|
|
17
20
|
".git/**",
|
|
18
21
|
".variant/**",
|
|
19
|
-
"dist/**",
|
|
20
|
-
"**/*.d.ts"
|
|
22
|
+
"**/dist/**",
|
|
23
|
+
"**/*.d.ts",
|
|
24
|
+
"**/*.d.mts",
|
|
25
|
+
"**/*.d.cts"
|
|
21
26
|
];
|
|
27
|
+
var JAVASCRIPT_FILE = /\.(?:[cm]?js|jsx)$/;
|
|
28
|
+
function isJavaScript(file) {
|
|
29
|
+
return JAVASCRIPT_FILE.test(file);
|
|
30
|
+
}
|
|
31
|
+
async function listJavaScript(cwd, ignore, fg) {
|
|
32
|
+
const { listRepositoryFiles } = await import("./git-W6N3WMYQ.js");
|
|
33
|
+
const tracked = await listRepositoryFiles(cwd, JAVASCRIPT_PATHSPECS);
|
|
34
|
+
if (tracked === null) {
|
|
35
|
+
return fg(JAVASCRIPT_INCLUDE, { cwd, onlyFiles: true, ignore });
|
|
36
|
+
}
|
|
37
|
+
const ignored = ignore.map(globToRegExp);
|
|
38
|
+
return tracked.filter(
|
|
39
|
+
(file) => !/\.min\.[cm]?js$/.test(file) && !ignored.some((pattern) => pattern.test(file))
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
function globToRegExp(pattern) {
|
|
43
|
+
const source = pattern.split("**/").map(
|
|
44
|
+
(part) => part.split("**").map(
|
|
45
|
+
(piece) => piece.split("*").map((text) => text.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join("[^/]*")
|
|
46
|
+
).join(".*")
|
|
47
|
+
).join("(?:.*/)?");
|
|
48
|
+
return new RegExp(`^${source}$`);
|
|
49
|
+
}
|
|
22
50
|
function defaultGraphDir(cwd) {
|
|
23
51
|
return path.join(cwd, ".variant", "graph");
|
|
24
52
|
}
|
|
@@ -26,11 +54,14 @@ async function buildSymbolGraph(cwd, options = {}) {
|
|
|
26
54
|
const fg = (await import("fast-glob")).default;
|
|
27
55
|
const include = options.include ?? DEFAULT_INCLUDE;
|
|
28
56
|
for (const p of include) assertSafePattern(p);
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
onlyFiles: true,
|
|
32
|
-
|
|
33
|
-
|
|
57
|
+
const ignore = [...DEFAULT_IGNORE, ...options.ignore ?? []];
|
|
58
|
+
const [typescript, javascript] = await Promise.all([
|
|
59
|
+
fg(include, { cwd, onlyFiles: true, ignore }),
|
|
60
|
+
options.include === void 0 ? listJavaScript(cwd, ignore, fg) : []
|
|
61
|
+
]);
|
|
62
|
+
const files = [
|
|
63
|
+
...new Set([...typescript, ...javascript].map(toPosix))
|
|
64
|
+
].sort();
|
|
34
65
|
const previous = options.previous?.version === SYMBOL_GRAPH_VERSION ? options.previous : null;
|
|
35
66
|
const stats = {
|
|
36
67
|
scanned: files.length,
|
|
@@ -40,6 +71,7 @@ async function buildSymbolGraph(cwd, options = {}) {
|
|
|
40
71
|
};
|
|
41
72
|
const contents = new Array(files.length);
|
|
42
73
|
const limit = Math.max(1, options.parallel ?? 32);
|
|
74
|
+
const read = options.readFile ?? ((absolutePath) => readFile(absolutePath, "utf8"));
|
|
43
75
|
let next = 0;
|
|
44
76
|
const worker = async () => {
|
|
45
77
|
while (true) {
|
|
@@ -47,7 +79,15 @@ async function buildSymbolGraph(cwd, options = {}) {
|
|
|
47
79
|
if (i >= files.length) return;
|
|
48
80
|
const relPath = files[i];
|
|
49
81
|
if (relPath === void 0) return;
|
|
50
|
-
|
|
82
|
+
try {
|
|
83
|
+
const content = await read(path.join(cwd, relPath));
|
|
84
|
+
if (isJavaScript(relPath) && content.length > MAX_JAVASCRIPT_BYTES) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
contents[i] = content;
|
|
88
|
+
} catch (err) {
|
|
89
|
+
if (err.code !== "ENOENT") throw err;
|
|
90
|
+
}
|
|
51
91
|
}
|
|
52
92
|
};
|
|
53
93
|
await Promise.all(
|
|
@@ -69,12 +109,19 @@ async function buildSymbolGraph(cwd, options = {}) {
|
|
|
69
109
|
}
|
|
70
110
|
if (tsm === void 0 || project === void 0) {
|
|
71
111
|
tsm = await import("ts-morph");
|
|
72
|
-
project = new tsm.Project({
|
|
112
|
+
project = new tsm.Project({
|
|
113
|
+
useInMemoryFileSystem: true,
|
|
114
|
+
compilerOptions: { allowJs: true }
|
|
115
|
+
});
|
|
73
116
|
}
|
|
74
117
|
const sourceFile = project.createSourceFile(relPath, content, {
|
|
75
118
|
overwrite: true
|
|
76
119
|
});
|
|
77
|
-
|
|
120
|
+
try {
|
|
121
|
+
out[relPath] = indexSourceFile(sourceFile, contentHash, tsm);
|
|
122
|
+
} finally {
|
|
123
|
+
project.removeSourceFile(sourceFile);
|
|
124
|
+
}
|
|
78
125
|
stats.parsed++;
|
|
79
126
|
}
|
|
80
127
|
if (previous !== null) {
|
|
@@ -188,24 +235,72 @@ function collectImports(file, tsm) {
|
|
|
188
235
|
names: named.length > 0 ? named : ["*"]
|
|
189
236
|
});
|
|
190
237
|
}
|
|
191
|
-
for (const
|
|
192
|
-
|
|
238
|
+
for (const decl of file.getDescendantsOfKind(
|
|
239
|
+
tsm.SyntaxKind.ImportEqualsDeclaration
|
|
240
|
+
)) {
|
|
241
|
+
const reference = decl.getModuleReference();
|
|
242
|
+
if (!tsm.Node.isExternalModuleReference(reference)) continue;
|
|
243
|
+
const expression = reference.getExpression();
|
|
244
|
+
if (expression === void 0 || !tsm.Node.isStringLiteral(expression)) {
|
|
193
245
|
continue;
|
|
194
246
|
}
|
|
247
|
+
imports.push({
|
|
248
|
+
module: expression.getLiteralValue(),
|
|
249
|
+
kind: "static",
|
|
250
|
+
typeOnly: decl.isTypeOnly(),
|
|
251
|
+
names: ["*"]
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
for (const call of file.getDescendantsOfKind(tsm.SyntaxKind.CallExpression)) {
|
|
255
|
+
const callee = call.getExpression();
|
|
256
|
+
const isImport = callee.getKind() === tsm.SyntaxKind.ImportKeyword;
|
|
257
|
+
const isRequire = tsm.Node.isIdentifier(callee) && callee.getText() === "require";
|
|
258
|
+
if (!isImport && !isRequire) continue;
|
|
195
259
|
const arg = call.getArguments()[0];
|
|
196
|
-
if (arg !== void 0 && tsm.Node.isStringLiteral(arg)) {
|
|
197
|
-
imports.push(
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
260
|
+
if (arg !== void 0 && (tsm.Node.isStringLiteral(arg) || tsm.Node.isNoSubstitutionTemplateLiteral(arg))) {
|
|
261
|
+
imports.push(
|
|
262
|
+
isImport ? {
|
|
263
|
+
module: arg.getLiteralValue(),
|
|
264
|
+
kind: "dynamic",
|
|
265
|
+
typeOnly: false,
|
|
266
|
+
names: []
|
|
267
|
+
} : {
|
|
268
|
+
module: arg.getLiteralValue(),
|
|
269
|
+
kind: "static",
|
|
270
|
+
typeOnly: false,
|
|
271
|
+
names: ["*"]
|
|
272
|
+
}
|
|
273
|
+
);
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
const prefix = arg === void 0 ? "" : staticPrefix(arg, tsm);
|
|
277
|
+
imports.push({
|
|
278
|
+
module: prefix,
|
|
279
|
+
kind: "pattern",
|
|
280
|
+
typeOnly: false,
|
|
281
|
+
names: []
|
|
282
|
+
});
|
|
283
|
+
if (prefix === "") {
|
|
284
|
+
notes.push(
|
|
285
|
+
`${isImport ? "dynamic import()" : "require()"} with a non-literal specifier`
|
|
286
|
+
);
|
|
205
287
|
}
|
|
206
288
|
}
|
|
207
289
|
return { imports, notes };
|
|
208
290
|
}
|
|
291
|
+
function staticPrefix(arg, tsm) {
|
|
292
|
+
if (tsm.Node.isTemplateExpression(arg)) {
|
|
293
|
+
return arg.getHead().getLiteralText();
|
|
294
|
+
}
|
|
295
|
+
if (tsm.Node.isBinaryExpression(arg) && arg.getOperatorToken().getKind() === tsm.SyntaxKind.PlusToken) {
|
|
296
|
+
const left = arg.getLeft();
|
|
297
|
+
if (tsm.Node.isStringLiteral(left) || tsm.Node.isNoSubstitutionTemplateLiteral(left)) {
|
|
298
|
+
return left.getLiteralValue();
|
|
299
|
+
}
|
|
300
|
+
return staticPrefix(left, tsm);
|
|
301
|
+
}
|
|
302
|
+
return "";
|
|
303
|
+
}
|
|
209
304
|
function sha256(text) {
|
|
210
305
|
return createHash("sha256").update(text).digest("hex");
|
|
211
306
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/core/vcs/git.ts
|
|
2
|
+
import path from "path";
|
|
2
3
|
function toPosix(relPath) {
|
|
3
4
|
return relPath.replace(/\\/g, "/");
|
|
4
5
|
}
|
|
@@ -16,11 +17,26 @@ function isOptionLike(ref) {
|
|
|
16
17
|
}
|
|
17
18
|
function toFileList(stdout) {
|
|
18
19
|
if (stdout === null) return null;
|
|
19
|
-
return stdout.split("\
|
|
20
|
+
return stdout.split("\0").filter((file) => file.length > 0);
|
|
21
|
+
}
|
|
22
|
+
async function listDiff(cwd, range) {
|
|
23
|
+
const [stdout, prefix] = await Promise.all([
|
|
24
|
+
git(cwd, ["diff", "--name-only", "-z", "--no-renames", range, "--"]),
|
|
25
|
+
git(cwd, ["rev-parse", "--show-prefix"])
|
|
26
|
+
]);
|
|
27
|
+
const files = toFileList(stdout);
|
|
28
|
+
const cwdFromRoot = prefix?.trim() ?? "";
|
|
29
|
+
if (files === null || cwdFromRoot === "") return files;
|
|
30
|
+
return files.map(
|
|
31
|
+
(file) => path.posix.relative(`/${cwdFromRoot}`, `/${file}`)
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
function blobAt(ref, relPath) {
|
|
35
|
+
return `${ref}:./${toPosix(relPath)}`;
|
|
20
36
|
}
|
|
21
37
|
async function readFileAtRef(cwd, ref, relPath) {
|
|
22
38
|
if (isOptionLike(ref)) return null;
|
|
23
|
-
return git(cwd, ["show",
|
|
39
|
+
return git(cwd, ["show", blobAt(ref, relPath)]);
|
|
24
40
|
}
|
|
25
41
|
var CAT_FILE_BATCH_SIZE = 256;
|
|
26
42
|
async function readFilesAtRef(cwd, ref, relPaths) {
|
|
@@ -34,7 +50,7 @@ async function readFilesAtRef(cwd, ref, relPaths) {
|
|
|
34
50
|
try {
|
|
35
51
|
const result = await execa("git", ["cat-file", "--batch"], {
|
|
36
52
|
cwd,
|
|
37
|
-
input: `${chunk.map((p) =>
|
|
53
|
+
input: `${chunk.map((p) => blobAt(ref, p)).join("\n")}
|
|
38
54
|
`,
|
|
39
55
|
encoding: "buffer",
|
|
40
56
|
// The response framing is byte-exact; execa's default of
|
|
@@ -97,15 +113,54 @@ async function resolveCommit(cwd, ref) {
|
|
|
97
113
|
const sha = stdout?.trim();
|
|
98
114
|
return sha === void 0 || sha === "" ? null : sha;
|
|
99
115
|
}
|
|
116
|
+
async function listRepositoryFiles(cwd, patterns) {
|
|
117
|
+
return toFileList(
|
|
118
|
+
await git(cwd, [
|
|
119
|
+
"ls-files",
|
|
120
|
+
"-z",
|
|
121
|
+
"--cached",
|
|
122
|
+
"--others",
|
|
123
|
+
"--exclude-standard",
|
|
124
|
+
"--",
|
|
125
|
+
...patterns
|
|
126
|
+
])
|
|
127
|
+
);
|
|
128
|
+
}
|
|
100
129
|
async function listChangedFiles(cwd, ref) {
|
|
101
130
|
if (isOptionLike(ref)) return null;
|
|
102
|
-
return
|
|
131
|
+
return listDiff(cwd, ref);
|
|
103
132
|
}
|
|
104
133
|
async function listChangedFilesSinceMergeBase(cwd, ref) {
|
|
105
134
|
if (isOptionLike(ref)) return null;
|
|
106
|
-
return
|
|
107
|
-
|
|
108
|
-
|
|
135
|
+
return listDiff(cwd, `${ref}...HEAD`);
|
|
136
|
+
}
|
|
137
|
+
async function mergeBase(cwd, a, b) {
|
|
138
|
+
if (isOptionLike(a) || isOptionLike(b)) return null;
|
|
139
|
+
const sha = (await git(cwd, ["merge-base", a, b]))?.trim();
|
|
140
|
+
return sha === void 0 || sha === "" ? null : sha;
|
|
141
|
+
}
|
|
142
|
+
var DEFAULT_BRANCH_GUESSES = [
|
|
143
|
+
"origin/main",
|
|
144
|
+
"origin/master",
|
|
145
|
+
"main",
|
|
146
|
+
"master"
|
|
147
|
+
];
|
|
148
|
+
async function defaultBranchRef(cwd) {
|
|
149
|
+
const head = (await git(cwd, [
|
|
150
|
+
"symbolic-ref",
|
|
151
|
+
"--quiet",
|
|
152
|
+
"--short",
|
|
153
|
+
"refs/remotes/origin/HEAD"
|
|
154
|
+
]))?.trim();
|
|
155
|
+
if (head !== void 0 && head !== "") return head;
|
|
156
|
+
for (const guess of DEFAULT_BRANCH_GUESSES) {
|
|
157
|
+
if (await resolveCommit(cwd, guess) !== null) return guess;
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
async function repositoryRoot(cwd) {
|
|
162
|
+
const top = (await git(cwd, ["rev-parse", "--show-toplevel"]))?.trim();
|
|
163
|
+
return top === void 0 || top === "" ? null : top;
|
|
109
164
|
}
|
|
110
165
|
|
|
111
166
|
export {
|
|
@@ -113,6 +168,10 @@ export {
|
|
|
113
168
|
readFilesAtRef,
|
|
114
169
|
readHeadSha,
|
|
115
170
|
resolveCommit,
|
|
171
|
+
listRepositoryFiles,
|
|
116
172
|
listChangedFiles,
|
|
117
|
-
listChangedFilesSinceMergeBase
|
|
173
|
+
listChangedFilesSinceMergeBase,
|
|
174
|
+
mergeBase,
|
|
175
|
+
defaultBranchRef,
|
|
176
|
+
repositoryRoot
|
|
118
177
|
};
|