@jskit-ai/architecture-inspector 0.1.0 → 0.1.2
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 +135 -13
- package/package.json +4 -2
- package/src/calls/cache.js +97 -0
- package/src/calls/evaluate.js +474 -0
- package/src/calls/external.js +123 -0
- package/src/calls/features.js +371 -0
- package/src/calls/functions.js +277 -0
- package/src/calls/index.js +343 -0
- package/src/calls/program.js +143 -0
- package/src/calls/resolve.js +658 -0
- package/src/calls/symbols.js +201 -0
- package/src/calls/transports.js +194 -0
- package/src/calls/types.js +9 -0
- package/src/cli.js +93 -14
- package/src/inspectVueArchitecture.js +146 -1035
- package/src/previewLocator.js +30 -0
- package/src/vue/modules.js +293 -0
- package/src/vue/pages.js +551 -0
- package/src/vue/routes.js +589 -0
- package/src/vue/syntax.js +222 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import { bindFeatures } from "./features.js";
|
|
3
|
+
import { collectFileFunctions, isFunctionLike, point } from "./functions.js";
|
|
4
|
+
import { createEvaluator } from "./evaluate.js";
|
|
5
|
+
import { loadCallProgram } from "./program.js";
|
|
6
|
+
import { createResolver } from "./resolve.js";
|
|
7
|
+
import { createSymbols } from "./symbols.js";
|
|
8
|
+
import { collectTransportSites, matchSite, routeTable, sitePatterns } from "./transports.js";
|
|
9
|
+
|
|
10
|
+
const MAX_CALLS_PER_FILE = 2000;
|
|
11
|
+
const MAX_CALLEE_LENGTH = 120;
|
|
12
|
+
|
|
13
|
+
function calleeText(call) {
|
|
14
|
+
return call.expression.getText().replace(/\s+/gu, " ").slice(0, MAX_CALLEE_LENGTH);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// The callee's own name token, so calls that start together (`a.b().c()`)
|
|
18
|
+
// keep distinct identities.
|
|
19
|
+
function callIdentityPosition(call) {
|
|
20
|
+
const callee = call.expression;
|
|
21
|
+
if (ts.isPropertyAccessExpression(callee)) {
|
|
22
|
+
return callee.name.getStart();
|
|
23
|
+
}
|
|
24
|
+
if (ts.isElementAccessExpression(callee)) {
|
|
25
|
+
return callee.argumentExpression.getStart();
|
|
26
|
+
}
|
|
27
|
+
return callee.getStart();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function rangeOf(sourceFile, node) {
|
|
31
|
+
return { start: point(sourceFile, node.getStart(sourceFile)), end: point(sourceFile, node.end) };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Function-level facts for the declared project scripts: per-file functions,
|
|
35
|
+
// resolved calls and call sites, transport operation uses, named action entry
|
|
36
|
+
// points and entry operation controllers. `reuse` holds
|
|
37
|
+
// earlier call records of files whose own and directly imported sources are
|
|
38
|
+
// unchanged; they are kept unless a target no longer exists or the sites
|
|
39
|
+
// other files' arguments resolve in them have changed.
|
|
40
|
+
function inspectCalls({
|
|
41
|
+
projectRoot,
|
|
42
|
+
sources,
|
|
43
|
+
hashes,
|
|
44
|
+
resolveImport,
|
|
45
|
+
entryOperations,
|
|
46
|
+
handlerRanges,
|
|
47
|
+
transports,
|
|
48
|
+
parentIdForFile,
|
|
49
|
+
prefixAliases,
|
|
50
|
+
diagnose,
|
|
51
|
+
reuse = new Map()
|
|
52
|
+
}) {
|
|
53
|
+
const loaded = loadCallProgram({ projectRoot, sources, resolveImport });
|
|
54
|
+
const { checker, sourceFiles, fileOf } = loaded;
|
|
55
|
+
// TypeScript recovers from syntax errors; such a file's functions and calls
|
|
56
|
+
// may be incomplete.
|
|
57
|
+
let complete = loaded.complete;
|
|
58
|
+
for (const [file, sourceFile] of sourceFiles) {
|
|
59
|
+
if (loaded.program.getSyntacticDiagnostics(sourceFile).length) {
|
|
60
|
+
complete = false;
|
|
61
|
+
diagnose("SCRIPT_SYNTAX_RECOVERED", "The script has syntax errors, so its functions and calls may be incomplete.", file);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const symbols = createSymbols({ checker, fileOf, resolveImport, isLibrary: loaded.isLibrary });
|
|
65
|
+
const evaluator = createEvaluator({ symbols, fileOf });
|
|
66
|
+
|
|
67
|
+
const functionsByFile = new Map();
|
|
68
|
+
const nodeById = new Map();
|
|
69
|
+
const recordByNode = new Map();
|
|
70
|
+
const moduleByFile = new Map();
|
|
71
|
+
const startByNode = new Map();
|
|
72
|
+
const functionNames = new Map();
|
|
73
|
+
const callsByFile = new Map();
|
|
74
|
+
const allCalls = [];
|
|
75
|
+
for (const [file, sourceFile] of sourceFiles) {
|
|
76
|
+
const collected = collectFileFunctions({ file, hash: hashes.get(file), sourceFile, checker });
|
|
77
|
+
functionsByFile.set(file, collected.functions);
|
|
78
|
+
moduleByFile.set(file, collected.module);
|
|
79
|
+
for (const { node, record, startPosition } of collected.nodes) {
|
|
80
|
+
recordByNode.set(node, record);
|
|
81
|
+
nodeById.set(record.id, node);
|
|
82
|
+
startByNode.set(node, startPosition);
|
|
83
|
+
if (record.name && record.kind !== "constructor") {
|
|
84
|
+
functionNames.set(record.name, [...(functionNames.get(record.name) || []), node]);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const calls = [];
|
|
88
|
+
const visit = (node) => {
|
|
89
|
+
if ((ts.isCallExpression(node) || ts.isNewExpression(node)) && node.expression) {
|
|
90
|
+
calls.push(node);
|
|
91
|
+
}
|
|
92
|
+
ts.forEachChild(node, visit);
|
|
93
|
+
};
|
|
94
|
+
visit(sourceFile);
|
|
95
|
+
callsByFile.set(file, calls);
|
|
96
|
+
allCalls.push(...calls);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const features = bindFeatures({ calls: allCalls.filter((call) => ts.isCallExpression(call)), symbols, evaluator });
|
|
100
|
+
const actionsById = new Map();
|
|
101
|
+
const unresolvedActionFiles = new Set();
|
|
102
|
+
for (const action of features.actions) {
|
|
103
|
+
if (!action.actionId) {
|
|
104
|
+
unresolvedActionFiles.add(fileOf(action.object));
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const known = actionsById.get(action.actionId) || [];
|
|
108
|
+
if (!known.some((entry) => entry.object === action.object)) {
|
|
109
|
+
actionsById.set(action.actionId, [...known, action]);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (const file of unresolvedActionFiles) {
|
|
113
|
+
diagnose("ACTION_ID_UNRESOLVED", "An action definition's id is not a literal, constant or literal-array element.", file);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const resolver = createResolver({
|
|
117
|
+
program: loaded.program,
|
|
118
|
+
checker,
|
|
119
|
+
symbols,
|
|
120
|
+
evaluator,
|
|
121
|
+
bindings: features.bindings,
|
|
122
|
+
actionsById,
|
|
123
|
+
fileOf,
|
|
124
|
+
functionNames
|
|
125
|
+
});
|
|
126
|
+
const functionId = (node) => recordByNode.get(node)?.id;
|
|
127
|
+
const functionIds = new Set(nodeById.keys());
|
|
128
|
+
const targetsExist = (entries) => entries.every(({ 1: targets }) => targets.every((target) => functionIds.has(target)));
|
|
129
|
+
const reusable = new Set([...reuse].filter(([file, cached]) => callsByFile.has(file) &&
|
|
130
|
+
targetsExist(cached.sites) && cached.calls.every((record) => record.targets.every((target) => functionIds.has(target))))
|
|
131
|
+
.map(([file]) => file));
|
|
132
|
+
// Each file's checker targets per call position, in call order, so argument
|
|
133
|
+
// flow into closures that run later sees the same call sites whether a file
|
|
134
|
+
// is reused or resolved. A reused file's checker sites depend only on its own
|
|
135
|
+
// and directly imported sources, so they come from the cache.
|
|
136
|
+
const sitesByFile = new Map();
|
|
137
|
+
const candidatesByFile = new Map();
|
|
138
|
+
for (const [file, calls] of callsByFile) {
|
|
139
|
+
if (reusable.has(file)) {
|
|
140
|
+
const cached = reuse.get(file);
|
|
141
|
+
const callAt = new Map(calls.map((call) => [call.getStart(), call]));
|
|
142
|
+
for (const [position, targets] of cached.sites) {
|
|
143
|
+
resolver.indexSite(callAt.get(position), targets.map((target) => nodeById.get(target)));
|
|
144
|
+
}
|
|
145
|
+
sitesByFile.set(file, cached.sites);
|
|
146
|
+
candidatesByFile.set(file, resolver.boundCandidates(cached.boundCandidates.map((position) => callAt.get(position))));
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const sites = [];
|
|
150
|
+
resolver.indexCalls(calls).forEach((targets, index) => {
|
|
151
|
+
if (targets.length) {
|
|
152
|
+
sites.push([calls[index].getStart(), targets.map(functionId)]);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
sitesByFile.set(file, sites);
|
|
156
|
+
candidatesByFile.set(file, resolver.boundCandidates(calls));
|
|
157
|
+
}
|
|
158
|
+
// The sites injection and argument flow resolve depend on other files'
|
|
159
|
+
// arguments, so they are resolved on every run, for every file. A site
|
|
160
|
+
// carries a third element when its targets are a guess.
|
|
161
|
+
const candidates = [...candidatesByFile.values()].flat();
|
|
162
|
+
const boundByFile = new Map([...callsByFile.keys()].map((file) => [file, []]));
|
|
163
|
+
resolver.indexBoundCalls(candidates).forEach(({ targets, probable }, index) => {
|
|
164
|
+
const call = candidates[index];
|
|
165
|
+
const file = fileOf(call);
|
|
166
|
+
const ids = targets.map(functionId);
|
|
167
|
+
if (ids.length && ids.every(Boolean) && !sitesByFile.get(file).some(([position]) => position === call.getStart())) {
|
|
168
|
+
boundByFile.get(file).push(probable ? [call.getStart(), ids, 1] : [call.getStart(), ids]);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
// A reused file keeps its records while its bound sites are the ones they
|
|
172
|
+
// were resolved with, and while every file its records reach keeps its own.
|
|
173
|
+
const sameSites = (left, right) => JSON.stringify(left) === JSON.stringify(right);
|
|
174
|
+
const reused = new Set([...reusable].filter((file) => sameSites(reuse.get(file).boundSites, boundByFile.get(file))));
|
|
175
|
+
const changedBound = new Set([...reusable].filter((file) => !reused.has(file)));
|
|
176
|
+
for (let changed = changedBound.size > 0; changed;) {
|
|
177
|
+
changed = false;
|
|
178
|
+
for (const file of reused) {
|
|
179
|
+
const reaches = reuse.get(file).calls.some((record) => record.targets.some((target) => changedBound.has(fileOf(nodeById.get(target)))));
|
|
180
|
+
if (reaches) {
|
|
181
|
+
reused.delete(file);
|
|
182
|
+
changedBound.add(file);
|
|
183
|
+
changed = true;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function enclosingFunction(node) {
|
|
189
|
+
const position = node.getStart();
|
|
190
|
+
for (let current = node.parent; current; current = current.parent) {
|
|
191
|
+
if (isFunctionLike(current) && startByNode.get(current) <= position) {
|
|
192
|
+
return recordByNode.get(current);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return moduleByFile.get(fileOf(node));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// What the cache keeps of a file's sites for the next run.
|
|
199
|
+
const fileSites = (file) => ({
|
|
200
|
+
sites: sitesByFile.get(file),
|
|
201
|
+
boundCandidates: candidatesByFile.get(file).map((call) => call.getStart()),
|
|
202
|
+
boundSites: boundByFile.get(file)
|
|
203
|
+
});
|
|
204
|
+
const fileRecords = new Map();
|
|
205
|
+
let truncated = false;
|
|
206
|
+
for (const [file, calls] of callsByFile) {
|
|
207
|
+
if (reused.has(file)) {
|
|
208
|
+
const cached = reuse.get(file);
|
|
209
|
+
fileRecords.set(file, { functions: functionsByFile.get(file), ...fileSites(file), calls: cached.calls, truncated: cached.truncated });
|
|
210
|
+
if (cached.truncated) {
|
|
211
|
+
truncated = true;
|
|
212
|
+
diagnose("CALLS_TRUNCATED", `Only the first ${MAX_CALLS_PER_FILE} calls of this file are recorded.`, file);
|
|
213
|
+
}
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const sourceFile = sourceFiles.get(file);
|
|
217
|
+
const records = [];
|
|
218
|
+
let fileTruncated = false;
|
|
219
|
+
const identities = new Set();
|
|
220
|
+
let count = 0;
|
|
221
|
+
for (const call of calls) {
|
|
222
|
+
const from = enclosingFunction(call);
|
|
223
|
+
// The call itself, then what it reaches through the functions it passes.
|
|
224
|
+
const results = from ? [resolver.resolve(call, file), resolver.passedCall(call)].filter(Boolean) : [];
|
|
225
|
+
if (count + results.length > MAX_CALLS_PER_FILE) {
|
|
226
|
+
fileTruncated = true;
|
|
227
|
+
truncated = true;
|
|
228
|
+
diagnose("CALLS_TRUNCATED", `Only the first ${MAX_CALLS_PER_FILE} calls of this file are recorded.`, file);
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
const position = point(sourceFile, callIdentityPosition(call));
|
|
232
|
+
for (const result of results) {
|
|
233
|
+
count += 1;
|
|
234
|
+
let id = `call:jskit:${file}#${position.line}:${position.column}`;
|
|
235
|
+
for (let suffix = 2; identities.has(id); suffix += 1) {
|
|
236
|
+
id = `call:jskit:${file}#${position.line}:${position.column}~${suffix}`;
|
|
237
|
+
}
|
|
238
|
+
identities.add(id);
|
|
239
|
+
records.push({
|
|
240
|
+
id,
|
|
241
|
+
resolution: result.resolution,
|
|
242
|
+
certainty: result.certainty,
|
|
243
|
+
method: result.method,
|
|
244
|
+
fromFunctionId: from.id,
|
|
245
|
+
targets: result.targets.map(functionId),
|
|
246
|
+
callee: calleeText(call),
|
|
247
|
+
evidence: [{ path: file, hash: hashes.get(file), ...rangeOf(sourceFile, call) }]
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
fileRecords.set(file, { functions: functionsByFile.get(file), ...fileSites(file), calls: records, truncated: fileTruncated });
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const routes = routeTable(entryOperations);
|
|
255
|
+
const operationUses = [];
|
|
256
|
+
for (const site of transports ? collectTransportSites({ calls: allCalls, symbols, evaluator, fileOf }) : []) {
|
|
257
|
+
const sourceFile = sourceFiles.get(site.file);
|
|
258
|
+
const from = enclosingFunction(site.call);
|
|
259
|
+
if (!from) {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
const position = point(sourceFile, site.call.getStart());
|
|
263
|
+
const baseId = `operation-use:${site.file}#${position.line}:${position.column}`;
|
|
264
|
+
const parentId = parentIdForFile(site.file);
|
|
265
|
+
const evidence = [{ path: site.file, hash: hashes.get(site.file), ...rangeOf(sourceFile, site.call) }];
|
|
266
|
+
const matched = matchSite({ patterns: sitePatterns(site, evaluator), methods: site.methods }, routes, prefixAliases)
|
|
267
|
+
.flatMap(({ method, matches }) => (matches.length ? matches : [{ method, route: null }]).map((match) => ({ method, ...match })));
|
|
268
|
+
const uses = matched.length ? matched : [{ method: null, route: null }];
|
|
269
|
+
uses.forEach((use, index) => {
|
|
270
|
+
operationUses.push({
|
|
271
|
+
id: uses.length === 1 ? baseId : `${baseId}:${use.method || "ANY"}:${index + 1}`,
|
|
272
|
+
...(parentId ? { parentId } : {}),
|
|
273
|
+
fromFunctionId: from.id,
|
|
274
|
+
resolution: use.route ? "resolved" : "unresolved",
|
|
275
|
+
...(use.route ? { key: use.route.key, keyCertainty: use.keyCertainty } : {}),
|
|
276
|
+
evidence
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const entryPoints = [];
|
|
282
|
+
for (const [actionId, definitions] of actionsById) {
|
|
283
|
+
const executes = [...new Set(definitions.flatMap((definition) => definition.execute))];
|
|
284
|
+
const first = definitions[0].object;
|
|
285
|
+
const file = fileOf(first);
|
|
286
|
+
entryPoints.push({
|
|
287
|
+
id: `entry:action:${actionId}`,
|
|
288
|
+
resolution: "resolved",
|
|
289
|
+
kind: "action",
|
|
290
|
+
title: actionId,
|
|
291
|
+
file,
|
|
292
|
+
functionIds: executes.map(functionId).filter(Boolean),
|
|
293
|
+
pageId: null,
|
|
294
|
+
componentId: null,
|
|
295
|
+
operationKey: null,
|
|
296
|
+
actionId,
|
|
297
|
+
evidence: definitions.map((definition) => {
|
|
298
|
+
const owner = fileOf(definition.object);
|
|
299
|
+
return { path: owner, hash: hashes.get(owner), ...rangeOf(sourceFiles.get(owner), definition.object) };
|
|
300
|
+
})
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function actionController(actionId) {
|
|
305
|
+
const definitions = actionsById.get(actionId) || [];
|
|
306
|
+
const executes = definitions.flatMap((definition) => definition.execute);
|
|
307
|
+
return definitions.length === 1 && executes.length === 1 ? functionId(executes[0]) : null;
|
|
308
|
+
}
|
|
309
|
+
// The function whose source range (start offset .. end offset) is given.
|
|
310
|
+
function functionAt({ file, start, end }) {
|
|
311
|
+
const sourceFile = sourceFiles.get(file);
|
|
312
|
+
let found = null;
|
|
313
|
+
const visit = (node) => {
|
|
314
|
+
if (found || node.end < start || node.pos > end) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (isFunctionLike(node) && node.getStart(sourceFile) === start && node.end === end) {
|
|
318
|
+
found = recordByNode.get(node);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
ts.forEachChild(node, visit);
|
|
322
|
+
};
|
|
323
|
+
if (sourceFile) {
|
|
324
|
+
visit(sourceFile);
|
|
325
|
+
}
|
|
326
|
+
return found?.id || null;
|
|
327
|
+
}
|
|
328
|
+
// Each entry operation's controller: its action's execute function, or the
|
|
329
|
+
// inline handler. Only plain records are returned, so the program can be
|
|
330
|
+
// released before the output is written.
|
|
331
|
+
const controllers = new Map();
|
|
332
|
+
for (const entry of entryOperations) {
|
|
333
|
+
const handler = handlerRanges.get(entry);
|
|
334
|
+
const controller = entry.actionId ? actionController(entry.actionId) : handler && functionAt(handler);
|
|
335
|
+
if (controller) {
|
|
336
|
+
controllers.set(entry, controller);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return { fileRecords, operationUses, entryPoints, controllers, complete, truncated };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export { inspectCalls };
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import { parse as parseSfc } from "@vue/compiler-sfc";
|
|
4
|
+
|
|
5
|
+
const SCRIPT_EXTENSIONS = Object.freeze({
|
|
6
|
+
".js": ts.Extension.Js,
|
|
7
|
+
".mjs": ts.Extension.Mjs,
|
|
8
|
+
".cjs": ts.Extension.Cjs,
|
|
9
|
+
".jsx": ts.Extension.Jsx,
|
|
10
|
+
".ts": ts.Extension.Ts,
|
|
11
|
+
".tsx": ts.Extension.Tsx
|
|
12
|
+
});
|
|
13
|
+
const LIBRARIES = Object.freeze(["lib.es2023.d.ts", "lib.dom.d.ts", "lib.dom.iterable.d.ts"]);
|
|
14
|
+
const COMPILER_OPTIONS = Object.freeze({
|
|
15
|
+
allowJs: true,
|
|
16
|
+
checkJs: false,
|
|
17
|
+
noEmit: true,
|
|
18
|
+
strict: false,
|
|
19
|
+
target: ts.ScriptTarget.ES2022,
|
|
20
|
+
module: ts.ModuleKind.ESNext,
|
|
21
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
22
|
+
jsx: ts.JsxEmit.Preserve,
|
|
23
|
+
skipLibCheck: true,
|
|
24
|
+
skipDefaultLibCheck: true,
|
|
25
|
+
allowArbitraryExtensions: true,
|
|
26
|
+
allowImportingTsExtensions: true,
|
|
27
|
+
maxNodeModuleJsDepth: 0,
|
|
28
|
+
types: [],
|
|
29
|
+
lib: LIBRARIES
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// A Vue component's script blocks at their original offsets; template and
|
|
33
|
+
// style text become spaces so every position matches the component file.
|
|
34
|
+
function vueScriptText(source, filePath) {
|
|
35
|
+
const { descriptor, errors } = parseSfc(source, { filename: filePath, sourceMap: false });
|
|
36
|
+
if (errors.length) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const characters = source.replace(/[^\n]/gu, " ").split("");
|
|
40
|
+
let extension = ".js";
|
|
41
|
+
for (const script of [descriptor.script, descriptor.scriptSetup].filter((entry) => entry && !entry.src)) {
|
|
42
|
+
const start = script.loc.start.offset;
|
|
43
|
+
for (let index = 0; index < script.content.length; index += 1) {
|
|
44
|
+
characters[start + index] = script.content[index];
|
|
45
|
+
}
|
|
46
|
+
if (script.lang === "ts" || script.lang === "tsx") {
|
|
47
|
+
extension = `.${script.lang}`;
|
|
48
|
+
} else if (script.lang === "jsx" && extension === ".js") {
|
|
49
|
+
extension = ".jsx";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { text: characters.join(""), extension };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// One TypeScript program over the declared, hash-verified project
|
|
56
|
+
// scripts only. Imports resolve through the inspector's own resolver
|
|
57
|
+
// (relative, aliases, declared package entry points); everything else, including
|
|
58
|
+
// every node_modules package, stays outside the program. TypeScript's default
|
|
59
|
+
// libraries remain so builtin calls are recognised and dropped.
|
|
60
|
+
function loadCallProgram({ projectRoot, sources, resolveImport }) {
|
|
61
|
+
const virtual = new Map();
|
|
62
|
+
const realToVirtual = new Map();
|
|
63
|
+
let complete = true;
|
|
64
|
+
for (const [file, source] of sources) {
|
|
65
|
+
const absolute = path.join(projectRoot, file);
|
|
66
|
+
let text = source;
|
|
67
|
+
let name = absolute;
|
|
68
|
+
if (file.endsWith(".vue")) {
|
|
69
|
+
const script = vueScriptText(source, file);
|
|
70
|
+
if (!script) {
|
|
71
|
+
complete = false;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
text = script.text;
|
|
75
|
+
name = `${absolute}${script.extension}`;
|
|
76
|
+
} else if (!SCRIPT_EXTENSIONS[path.extname(file)]) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
virtual.set(name, { file, text });
|
|
80
|
+
realToVirtual.set(file, name);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const libraryDirectory = path.dirname(ts.getDefaultLibFilePath(COMPILER_OPTIONS));
|
|
84
|
+
const isLibraryFile = (name) => path.dirname(name) === libraryDirectory && /^lib\.[\w.]*\.d\.ts$/u.test(path.basename(name));
|
|
85
|
+
const resolutions = new Map();
|
|
86
|
+
function resolveModule(specifier, containing) {
|
|
87
|
+
const from = virtual.get(containing)?.file;
|
|
88
|
+
if (!from) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
const key = `${path.posix.dirname(from)}\0${specifier}`;
|
|
92
|
+
if (!resolutions.has(key)) {
|
|
93
|
+
const target = resolveImport(from, specifier);
|
|
94
|
+
const name = target && realToVirtual.get(target);
|
|
95
|
+
const extension = name && SCRIPT_EXTENSIONS[path.extname(name)];
|
|
96
|
+
resolutions.set(key, extension ? { resolvedFileName: name, extension, isExternalLibraryImport: false } : undefined);
|
|
97
|
+
}
|
|
98
|
+
return resolutions.get(key);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const host = {
|
|
102
|
+
getSourceFile(name, languageVersion) {
|
|
103
|
+
const text = virtual.get(name)?.text ?? (isLibraryFile(name) ? ts.sys.readFile(name) : undefined);
|
|
104
|
+
return text === undefined ? undefined : ts.createSourceFile(name, text, languageVersion);
|
|
105
|
+
},
|
|
106
|
+
getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options),
|
|
107
|
+
writeFile: () => {},
|
|
108
|
+
getCurrentDirectory: () => projectRoot,
|
|
109
|
+
getCanonicalFileName: (name) => name,
|
|
110
|
+
useCaseSensitiveFileNames: () => true,
|
|
111
|
+
getNewLine: () => "\n",
|
|
112
|
+
fileExists: (name) => virtual.has(name) || (isLibraryFile(name) && ts.sys.fileExists(name)),
|
|
113
|
+
readFile: (name) => virtual.get(name)?.text ?? (isLibraryFile(name) ? ts.sys.readFile(name) : undefined),
|
|
114
|
+
directoryExists: (name) => name === libraryDirectory || name === projectRoot || name.startsWith(`${projectRoot}${path.sep}`),
|
|
115
|
+
getDirectories: () => [],
|
|
116
|
+
realpath: (name) => name,
|
|
117
|
+
resolveModuleNameLiterals: (literals, containing) =>
|
|
118
|
+
literals.map((literal) => ({ resolvedModule: resolveModule(literal.text, containing) })),
|
|
119
|
+
resolveTypeReferenceDirectiveReferences: (references) =>
|
|
120
|
+
references.map(() => ({ resolvedTypeReferenceDirective: undefined }))
|
|
121
|
+
};
|
|
122
|
+
const program = ts.createProgram({ rootNames: [...virtual.keys()], options: COMPILER_OPTIONS, host });
|
|
123
|
+
const sourceFiles = new Map();
|
|
124
|
+
for (const sourceFile of program.getSourceFiles()) {
|
|
125
|
+
const file = virtual.get(sourceFile.fileName)?.file;
|
|
126
|
+
if (file) {
|
|
127
|
+
sourceFiles.set(file, sourceFile);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
ts,
|
|
132
|
+
complete,
|
|
133
|
+
program,
|
|
134
|
+
checker: program.getTypeChecker(),
|
|
135
|
+
sourceFiles,
|
|
136
|
+
fileOf: (node) => virtual.get(node.getSourceFile().fileName)?.file ?? null,
|
|
137
|
+
isLibrary: (node) => program.isSourceFileDefaultLibrary(node.getSourceFile()),
|
|
138
|
+
programFiles: program.getSourceFiles().map((sourceFile) =>
|
|
139
|
+
virtual.get(sourceFile.fileName)?.file ?? path.relative(projectRoot, sourceFile.fileName))
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export { loadCallProgram };
|