foresthouse 1.0.0-dev.1 → 1.0.0-dev.3
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/dist/cli.mjs +42 -4
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +40 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/tree-DbnWcUnV.mjs +851 -0
- package/dist/tree-DbnWcUnV.mjs.map +1 -0
- package/package.json +3 -2
- package/dist/tree-BiiNljTI.mjs +0 -276
- package/dist/tree-BiiNljTI.mjs.map +0 -1
|
@@ -0,0 +1,851 @@
|
|
|
1
|
+
import { builtinModules } from "node:module";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import ts from "typescript";
|
|
5
|
+
import { parseSync, visitorKeys } from "oxc-parser";
|
|
6
|
+
//#region src/config.ts
|
|
7
|
+
function loadCompilerOptions(searchFrom, explicitConfigPath) {
|
|
8
|
+
const configPath = explicitConfigPath === void 0 ? findNearestConfig(searchFrom) : path.resolve(searchFrom, explicitConfigPath);
|
|
9
|
+
if (configPath === void 0) return { compilerOptions: defaultCompilerOptions() };
|
|
10
|
+
const readResult = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
11
|
+
if (readResult.error !== void 0) throw new Error(`Failed to read TypeScript config at ${configPath}: ${formatDiagnostic(readResult.error)}`);
|
|
12
|
+
const parsed = ts.parseJsonConfigFileContent(readResult.config, ts.sys, path.dirname(configPath), defaultCompilerOptions(), configPath);
|
|
13
|
+
if (parsed.errors.length > 0) {
|
|
14
|
+
const [firstError] = parsed.errors;
|
|
15
|
+
if (firstError === void 0) throw new Error(`Failed to parse TypeScript config at ${configPath}.`);
|
|
16
|
+
throw new Error(`Failed to parse TypeScript config at ${configPath}: ${formatDiagnostic(firstError)}`);
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
path: configPath,
|
|
20
|
+
compilerOptions: parsed.options
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function findNearestConfig(searchFrom) {
|
|
24
|
+
let currentDirectory = path.resolve(searchFrom);
|
|
25
|
+
while (true) {
|
|
26
|
+
const tsconfigPath = path.join(currentDirectory, "tsconfig.json");
|
|
27
|
+
if (ts.sys.fileExists(tsconfigPath)) return tsconfigPath;
|
|
28
|
+
const jsconfigPath = path.join(currentDirectory, "jsconfig.json");
|
|
29
|
+
if (ts.sys.fileExists(jsconfigPath)) return jsconfigPath;
|
|
30
|
+
const parentDirectory = path.dirname(currentDirectory);
|
|
31
|
+
if (parentDirectory === currentDirectory) return;
|
|
32
|
+
currentDirectory = parentDirectory;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function defaultCompilerOptions() {
|
|
36
|
+
return {
|
|
37
|
+
allowJs: true,
|
|
38
|
+
jsx: ts.JsxEmit.ReactJSX,
|
|
39
|
+
module: ts.ModuleKind.NodeNext,
|
|
40
|
+
moduleResolution: ts.ModuleResolutionKind.NodeNext,
|
|
41
|
+
target: ts.ScriptTarget.ESNext,
|
|
42
|
+
resolveJsonModule: true,
|
|
43
|
+
esModuleInterop: true
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function formatDiagnostic(diagnostic) {
|
|
47
|
+
return ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/path-utils.ts
|
|
51
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
52
|
+
".js",
|
|
53
|
+
".jsx",
|
|
54
|
+
".ts",
|
|
55
|
+
".tsx",
|
|
56
|
+
".mjs",
|
|
57
|
+
".cjs",
|
|
58
|
+
".mts",
|
|
59
|
+
".cts"
|
|
60
|
+
]);
|
|
61
|
+
function normalizeFilePath(filePath) {
|
|
62
|
+
return path.normalize(filePath);
|
|
63
|
+
}
|
|
64
|
+
function toDisplayPath(filePath, cwd) {
|
|
65
|
+
const relativePath = path.relative(cwd, filePath);
|
|
66
|
+
if (relativePath === "") return ".";
|
|
67
|
+
const normalizedPath = relativePath.split(path.sep).join("/");
|
|
68
|
+
return normalizedPath.startsWith("..") ? filePath : normalizedPath;
|
|
69
|
+
}
|
|
70
|
+
function isSourceCodeFile(filePath) {
|
|
71
|
+
return SOURCE_EXTENSIONS.has(path.extname(filePath).toLowerCase());
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/analyzer.ts
|
|
75
|
+
const BUILTIN_MODULES = new Set(builtinModules.flatMap((name) => [name, `node:${name}`]));
|
|
76
|
+
function analyzeDependencies(entryFile, options = {}) {
|
|
77
|
+
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
78
|
+
const resolvedEntryPath = resolveExistingPath(cwd, entryFile);
|
|
79
|
+
const { compilerOptions, path: configPath } = loadCompilerOptions(path.dirname(resolvedEntryPath), options.configPath);
|
|
80
|
+
const host = {
|
|
81
|
+
fileExists: ts.sys.fileExists,
|
|
82
|
+
readFile: ts.sys.readFile,
|
|
83
|
+
directoryExists: ts.sys.directoryExists,
|
|
84
|
+
getCurrentDirectory: () => cwd,
|
|
85
|
+
getDirectories: ts.sys.getDirectories,
|
|
86
|
+
...ts.sys.realpath === void 0 ? {} : { realpath: ts.sys.realpath }
|
|
87
|
+
};
|
|
88
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
89
|
+
const program = createProgram(resolvedEntryPath, compilerOptions, cwd);
|
|
90
|
+
visitFile(resolvedEntryPath, compilerOptions, host, program.getTypeChecker(), program, nodes);
|
|
91
|
+
return {
|
|
92
|
+
cwd,
|
|
93
|
+
entryId: resolvedEntryPath,
|
|
94
|
+
nodes,
|
|
95
|
+
...configPath === void 0 ? {} : { configPath }
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function graphToSerializableTree(graph, options = {}) {
|
|
99
|
+
const visited = /* @__PURE__ */ new Set();
|
|
100
|
+
return serializeNode(graph.entryId, graph, visited, options.omitUnused ?? false);
|
|
101
|
+
}
|
|
102
|
+
function serializeNode(filePath, graph, visited, omitUnused) {
|
|
103
|
+
const node = graph.nodes.get(filePath);
|
|
104
|
+
const displayPath = toDisplayPath(filePath, graph.cwd);
|
|
105
|
+
if (node === void 0) return {
|
|
106
|
+
path: displayPath,
|
|
107
|
+
kind: "missing",
|
|
108
|
+
dependencies: []
|
|
109
|
+
};
|
|
110
|
+
if (visited.has(filePath)) return {
|
|
111
|
+
path: displayPath,
|
|
112
|
+
kind: "circular",
|
|
113
|
+
dependencies: []
|
|
114
|
+
};
|
|
115
|
+
visited.add(filePath);
|
|
116
|
+
const dependencies = node.dependencies.filter((dependency) => !omitUnused || !dependency.unused).map((dependency) => {
|
|
117
|
+
if (dependency.kind !== "source") return {
|
|
118
|
+
specifier: dependency.specifier,
|
|
119
|
+
referenceKind: dependency.referenceKind,
|
|
120
|
+
isTypeOnly: dependency.isTypeOnly,
|
|
121
|
+
unused: dependency.unused,
|
|
122
|
+
kind: dependency.kind,
|
|
123
|
+
target: dependency.kind === "missing" ? dependency.target : toDisplayPath(dependency.target, graph.cwd)
|
|
124
|
+
};
|
|
125
|
+
return {
|
|
126
|
+
specifier: dependency.specifier,
|
|
127
|
+
referenceKind: dependency.referenceKind,
|
|
128
|
+
isTypeOnly: dependency.isTypeOnly,
|
|
129
|
+
unused: dependency.unused,
|
|
130
|
+
kind: dependency.kind,
|
|
131
|
+
target: toDisplayPath(dependency.target, graph.cwd),
|
|
132
|
+
node: serializeNode(dependency.target, graph, new Set(visited), omitUnused)
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
return {
|
|
136
|
+
path: displayPath,
|
|
137
|
+
kind: filePath === graph.entryId ? "entry" : "source",
|
|
138
|
+
dependencies
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function visitFile(filePath, compilerOptions, host, checker, program, nodes) {
|
|
142
|
+
const normalizedPath = normalizeFilePath(filePath);
|
|
143
|
+
if (nodes.has(normalizedPath)) return;
|
|
144
|
+
const dependencies = collectModuleReferences(program.getSourceFile(normalizedPath) ?? createSourceFile(normalizedPath), checker).map((reference) => resolveDependency(reference, normalizedPath, compilerOptions, host));
|
|
145
|
+
nodes.set(normalizedPath, {
|
|
146
|
+
id: normalizedPath,
|
|
147
|
+
dependencies
|
|
148
|
+
});
|
|
149
|
+
for (const dependency of dependencies) if (dependency.kind === "source") visitFile(dependency.target, compilerOptions, host, checker, program, nodes);
|
|
150
|
+
}
|
|
151
|
+
function collectModuleReferences(sourceFile, checker) {
|
|
152
|
+
const references = /* @__PURE__ */ new Map();
|
|
153
|
+
const unusedImports = collectUnusedImports(sourceFile, checker);
|
|
154
|
+
function addReference(specifier, referenceKind, isTypeOnly, unused) {
|
|
155
|
+
const key = `${referenceKind}:${isTypeOnly ? "type" : "value"}:${specifier}`;
|
|
156
|
+
const existing = references.get(key);
|
|
157
|
+
if (existing !== void 0) {
|
|
158
|
+
if (existing.unused && !unused) references.set(key, {
|
|
159
|
+
...existing,
|
|
160
|
+
unused: false
|
|
161
|
+
});
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
references.set(key, {
|
|
165
|
+
specifier,
|
|
166
|
+
referenceKind,
|
|
167
|
+
isTypeOnly,
|
|
168
|
+
unused
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
function visit(node) {
|
|
172
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteralLike(node.moduleSpecifier)) addReference(node.moduleSpecifier.text, "import", node.importClause?.isTypeOnly ?? false, unusedImports.get(node) ?? false);
|
|
173
|
+
else if (ts.isExportDeclaration(node) && node.moduleSpecifier !== void 0 && ts.isStringLiteralLike(node.moduleSpecifier)) addReference(node.moduleSpecifier.text, "export", node.isTypeOnly ?? false, false);
|
|
174
|
+
else if (ts.isImportEqualsDeclaration(node)) {
|
|
175
|
+
const moduleReference = node.moduleReference;
|
|
176
|
+
if (ts.isExternalModuleReference(moduleReference) && moduleReference.expression !== void 0 && ts.isStringLiteralLike(moduleReference.expression)) addReference(moduleReference.expression.text, "import-equals", false, false);
|
|
177
|
+
} else if (ts.isCallExpression(node)) {
|
|
178
|
+
if (node.expression.kind === ts.SyntaxKind.ImportKeyword && node.arguments.length === 1) {
|
|
179
|
+
const [argument] = node.arguments;
|
|
180
|
+
if (argument !== void 0 && ts.isStringLiteralLike(argument)) addReference(argument.text, "dynamic-import", false, false);
|
|
181
|
+
}
|
|
182
|
+
if (ts.isIdentifier(node.expression) && node.expression.text === "require" && node.arguments.length === 1) {
|
|
183
|
+
const [argument] = node.arguments;
|
|
184
|
+
if (argument !== void 0 && ts.isStringLiteralLike(argument)) addReference(argument.text, "require", false, false);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
ts.forEachChild(node, visit);
|
|
188
|
+
}
|
|
189
|
+
visit(sourceFile);
|
|
190
|
+
return [...references.values()];
|
|
191
|
+
}
|
|
192
|
+
function resolveDependency(reference, containingFile, compilerOptions, host) {
|
|
193
|
+
const specifier = reference.specifier;
|
|
194
|
+
if (BUILTIN_MODULES.has(specifier)) return createEdge(reference, "builtin", specifier);
|
|
195
|
+
const resolution = ts.resolveModuleName(specifier, containingFile, compilerOptions, host).resolvedModule;
|
|
196
|
+
if (resolution !== void 0) {
|
|
197
|
+
const resolvedPath = normalizeFilePath(resolution.resolvedFileName);
|
|
198
|
+
if (resolution.isExternalLibraryImport || resolvedPath.includes(`${path.sep}node_modules${path.sep}`)) return createEdge(reference, "external", specifier);
|
|
199
|
+
if (isSourceCodeFile(resolvedPath) && !resolvedPath.endsWith(".d.ts")) return createEdge(reference, "source", resolvedPath);
|
|
200
|
+
}
|
|
201
|
+
if (!specifier.startsWith(".") && !path.isAbsolute(specifier)) return createEdge(reference, "external", specifier);
|
|
202
|
+
return createEdge(reference, "missing", specifier);
|
|
203
|
+
}
|
|
204
|
+
function createEdge(reference, kind, target) {
|
|
205
|
+
return {
|
|
206
|
+
specifier: reference.specifier,
|
|
207
|
+
referenceKind: reference.referenceKind,
|
|
208
|
+
isTypeOnly: reference.isTypeOnly,
|
|
209
|
+
unused: reference.unused,
|
|
210
|
+
kind,
|
|
211
|
+
target
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function createProgram(entryFile, compilerOptions, cwd) {
|
|
215
|
+
const host = ts.createCompilerHost(compilerOptions, true);
|
|
216
|
+
host.getCurrentDirectory = () => cwd;
|
|
217
|
+
if (ts.sys.realpath !== void 0) host.realpath = ts.sys.realpath;
|
|
218
|
+
return ts.createProgram({
|
|
219
|
+
rootNames: [entryFile],
|
|
220
|
+
options: compilerOptions,
|
|
221
|
+
host
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
function createSourceFile(filePath) {
|
|
225
|
+
const sourceText = fs.readFileSync(filePath, "utf8");
|
|
226
|
+
return ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true, getScriptKind(filePath));
|
|
227
|
+
}
|
|
228
|
+
function collectUnusedImports(sourceFile, checker) {
|
|
229
|
+
const importUsage = /* @__PURE__ */ new Map();
|
|
230
|
+
const symbolToImportDeclaration = /* @__PURE__ */ new Map();
|
|
231
|
+
const importedLocalNames = /* @__PURE__ */ new Set();
|
|
232
|
+
sourceFile.statements.forEach((statement) => {
|
|
233
|
+
if (!ts.isImportDeclaration(statement) || statement.importClause === void 0) return;
|
|
234
|
+
const identifiers = getImportBindingIdentifiers(statement.importClause);
|
|
235
|
+
if (identifiers.length === 0) return;
|
|
236
|
+
importUsage.set(statement, {
|
|
237
|
+
canTrack: false,
|
|
238
|
+
used: false
|
|
239
|
+
});
|
|
240
|
+
identifiers.forEach((identifier) => {
|
|
241
|
+
importedLocalNames.add(identifier.text);
|
|
242
|
+
const symbol = tryGetSymbolAtLocation(checker, identifier);
|
|
243
|
+
if (symbol === void 0) return;
|
|
244
|
+
symbolToImportDeclaration.set(symbol, statement);
|
|
245
|
+
const state = importUsage.get(statement);
|
|
246
|
+
if (state !== void 0) state.canTrack = true;
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
function visit(node) {
|
|
250
|
+
if (ts.isImportDeclaration(node)) return;
|
|
251
|
+
if (ts.isIdentifier(node) && importedLocalNames.has(node.text) && isReferenceIdentifier(node)) {
|
|
252
|
+
const symbol = tryGetSymbolAtLocation(checker, node);
|
|
253
|
+
const declaration = symbol === void 0 ? void 0 : symbolToImportDeclaration.get(symbol);
|
|
254
|
+
if (declaration !== void 0) {
|
|
255
|
+
const state = importUsage.get(declaration);
|
|
256
|
+
if (state !== void 0) state.used = true;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
ts.forEachChild(node, visit);
|
|
260
|
+
}
|
|
261
|
+
visit(sourceFile);
|
|
262
|
+
return new Map([...importUsage.entries()].map(([declaration, state]) => [declaration, state.canTrack && !state.used]));
|
|
263
|
+
}
|
|
264
|
+
function getImportBindingIdentifiers(importClause) {
|
|
265
|
+
const identifiers = [];
|
|
266
|
+
if (importClause.name !== void 0) identifiers.push(importClause.name);
|
|
267
|
+
const namedBindings = importClause.namedBindings;
|
|
268
|
+
if (namedBindings === void 0) return identifiers;
|
|
269
|
+
if (ts.isNamespaceImport(namedBindings)) {
|
|
270
|
+
identifiers.push(namedBindings.name);
|
|
271
|
+
return identifiers;
|
|
272
|
+
}
|
|
273
|
+
namedBindings.elements.forEach((element) => {
|
|
274
|
+
identifiers.push(element.name);
|
|
275
|
+
});
|
|
276
|
+
return identifiers;
|
|
277
|
+
}
|
|
278
|
+
function isReferenceIdentifier(node) {
|
|
279
|
+
const parent = node.parent;
|
|
280
|
+
if (ts.isPropertyAccessExpression(parent) && parent.name === node) return false;
|
|
281
|
+
if (ts.isQualifiedName(parent) && parent.right === node) return false;
|
|
282
|
+
if (ts.isPropertyAssignment(parent) && parent.name === node) return false;
|
|
283
|
+
if (ts.isBindingElement(parent) && parent.propertyName === node) return false;
|
|
284
|
+
if (ts.isJsxAttribute(parent) && parent.name === node) return false;
|
|
285
|
+
if (ts.isExportSpecifier(parent)) return parent.propertyName === node || parent.propertyName === void 0;
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
function tryGetSymbolAtLocation(checker, node) {
|
|
289
|
+
try {
|
|
290
|
+
return checker.getSymbolAtLocation(node);
|
|
291
|
+
} catch {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function getScriptKind(filePath) {
|
|
296
|
+
switch (path.extname(filePath).toLowerCase()) {
|
|
297
|
+
case ".js":
|
|
298
|
+
case ".mjs":
|
|
299
|
+
case ".cjs": return ts.ScriptKind.JS;
|
|
300
|
+
case ".jsx": return ts.ScriptKind.JSX;
|
|
301
|
+
case ".tsx": return ts.ScriptKind.TSX;
|
|
302
|
+
case ".json": return ts.ScriptKind.JSON;
|
|
303
|
+
default: return ts.ScriptKind.TS;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function resolveExistingPath(cwd, entryFile) {
|
|
307
|
+
const normalizedPath = normalizeFilePath(path.resolve(cwd, entryFile));
|
|
308
|
+
if (!fs.existsSync(normalizedPath)) throw new Error(`Entry file not found: ${entryFile}`);
|
|
309
|
+
if (!isSourceCodeFile(normalizedPath)) throw new Error(`Entry file must be a JS/TS source file: ${entryFile}`);
|
|
310
|
+
return normalizedPath;
|
|
311
|
+
}
|
|
312
|
+
//#endregion
|
|
313
|
+
//#region src/react-analyzer.ts
|
|
314
|
+
const FUNCTION_NODE_TYPES = new Set([
|
|
315
|
+
"FunctionDeclaration",
|
|
316
|
+
"FunctionExpression",
|
|
317
|
+
"ArrowFunctionExpression",
|
|
318
|
+
"TSDeclareFunction",
|
|
319
|
+
"TSEmptyBodyFunctionExpression"
|
|
320
|
+
]);
|
|
321
|
+
function analyzeReactUsage(entryFile, options = {}) {
|
|
322
|
+
const dependencyGraph = analyzeDependencies(entryFile, options);
|
|
323
|
+
const reachableFiles = new Set([dependencyGraph.entryId, ...dependencyGraph.nodes.keys()]);
|
|
324
|
+
const fileAnalyses = /* @__PURE__ */ new Map();
|
|
325
|
+
for (const filePath of [...reachableFiles].sort()) {
|
|
326
|
+
if (!isSourceCodeFile(filePath) || filePath.endsWith(".d.ts")) continue;
|
|
327
|
+
const parseResult = parseSync(filePath, fs.readFileSync(filePath, "utf8"), {
|
|
328
|
+
astType: "ts",
|
|
329
|
+
sourceType: "unambiguous"
|
|
330
|
+
});
|
|
331
|
+
const dependencyNode = dependencyGraph.nodes.get(filePath);
|
|
332
|
+
const sourceDependencies = /* @__PURE__ */ new Map();
|
|
333
|
+
dependencyNode?.dependencies.forEach((dependency) => {
|
|
334
|
+
if (dependency.kind === "source") sourceDependencies.set(dependency.specifier, dependency.target);
|
|
335
|
+
});
|
|
336
|
+
fileAnalyses.set(filePath, analyzeReactFile(parseResult.program, filePath, sourceDependencies));
|
|
337
|
+
}
|
|
338
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
339
|
+
for (const fileAnalysis of fileAnalyses.values()) for (const symbol of fileAnalysis.symbolsById.values()) nodes.set(symbol.id, {
|
|
340
|
+
id: symbol.id,
|
|
341
|
+
name: symbol.name,
|
|
342
|
+
kind: symbol.kind,
|
|
343
|
+
filePath: symbol.filePath,
|
|
344
|
+
exportNames: [...symbol.exportNames].sort(),
|
|
345
|
+
usages: []
|
|
346
|
+
});
|
|
347
|
+
for (const fileAnalysis of fileAnalyses.values()) fileAnalysis.importsByLocalName.forEach((binding, localName) => {
|
|
348
|
+
if (binding.sourcePath !== void 0) return;
|
|
349
|
+
if (!isHookName(localName) && !isHookName(binding.importedName)) return;
|
|
350
|
+
const externalNode = createExternalHookNode(binding, localName);
|
|
351
|
+
if (!nodes.has(externalNode.id)) nodes.set(externalNode.id, externalNode);
|
|
352
|
+
});
|
|
353
|
+
for (const fileAnalysis of fileAnalyses.values()) for (const symbol of fileAnalysis.symbolsById.values()) {
|
|
354
|
+
const usages = /* @__PURE__ */ new Map();
|
|
355
|
+
symbol.componentReferences.forEach((referenceName) => {
|
|
356
|
+
const targetId = resolveReactReference(fileAnalysis, fileAnalyses, referenceName, "component");
|
|
357
|
+
if (targetId !== void 0 && targetId !== symbol.id) usages.set(`render:${targetId}`, {
|
|
358
|
+
kind: "render",
|
|
359
|
+
target: targetId
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
symbol.hookReferences.forEach((referenceName) => {
|
|
363
|
+
const targetId = resolveReactReference(fileAnalysis, fileAnalyses, referenceName, "hook");
|
|
364
|
+
if (targetId !== void 0 && targetId !== symbol.id) usages.set(`hook:${targetId}`, {
|
|
365
|
+
kind: "hook-call",
|
|
366
|
+
target: targetId
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
const node = nodes.get(symbol.id);
|
|
370
|
+
if (node === void 0) continue;
|
|
371
|
+
const sortedUsages = [...usages.values()].sort((left, right) => compareReactNodeIds(left.target, right.target, nodes));
|
|
372
|
+
nodes.set(symbol.id, {
|
|
373
|
+
...node,
|
|
374
|
+
usages: sortedUsages
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
cwd: dependencyGraph.cwd,
|
|
379
|
+
entryId: dependencyGraph.entryId,
|
|
380
|
+
nodes
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
function graphToSerializableReactTree(graph, options = {}) {
|
|
384
|
+
return {
|
|
385
|
+
kind: "react-usage",
|
|
386
|
+
roots: getReactUsageRoots(graph, options.filter).map((rootId) => serializeReactUsageNode(rootId, graph, options.filter ?? "all", /* @__PURE__ */ new Set()))
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
function getReactUsageRoots(graph, filter = "all") {
|
|
390
|
+
const filteredNodes = getFilteredReactUsageNodes(graph, filter);
|
|
391
|
+
const inboundCounts = /* @__PURE__ */ new Map();
|
|
392
|
+
filteredNodes.forEach((node) => {
|
|
393
|
+
inboundCounts.set(node.id, 0);
|
|
394
|
+
});
|
|
395
|
+
filteredNodes.forEach((node) => {
|
|
396
|
+
getFilteredUsages(node, graph, filter).forEach((usage) => {
|
|
397
|
+
inboundCounts.set(usage.target, (inboundCounts.get(usage.target) ?? 0) + 1);
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
const roots = filteredNodes.filter((node) => (inboundCounts.get(node.id) ?? 0) === 0).map((node) => node.id);
|
|
401
|
+
if (roots.length > 0) return roots.sort((left, right) => compareReactNodeIds(left, right, graph.nodes));
|
|
402
|
+
return filteredNodes.map((node) => node.id).sort((left, right) => compareReactNodeIds(left, right, graph.nodes));
|
|
403
|
+
}
|
|
404
|
+
function getFilteredUsages(node, graph, filter = "all") {
|
|
405
|
+
return node.usages.filter((usage) => {
|
|
406
|
+
const targetNode = graph.nodes.get(usage.target);
|
|
407
|
+
return targetNode !== void 0 && matchesReactFilter(targetNode, filter);
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
function analyzeReactFile(program, filePath, sourceDependencies) {
|
|
411
|
+
const symbolsByName = /* @__PURE__ */ new Map();
|
|
412
|
+
program.body.forEach((statement) => {
|
|
413
|
+
collectTopLevelReactSymbols(statement, filePath, symbolsByName);
|
|
414
|
+
});
|
|
415
|
+
const importsByLocalName = /* @__PURE__ */ new Map();
|
|
416
|
+
const exportsByName = /* @__PURE__ */ new Map();
|
|
417
|
+
program.body.forEach((statement) => {
|
|
418
|
+
collectImportsAndExports(statement, sourceDependencies, symbolsByName, importsByLocalName, exportsByName);
|
|
419
|
+
});
|
|
420
|
+
symbolsByName.forEach((symbol) => {
|
|
421
|
+
analyzeSymbolUsages(symbol);
|
|
422
|
+
});
|
|
423
|
+
return {
|
|
424
|
+
filePath,
|
|
425
|
+
importsByLocalName,
|
|
426
|
+
exportsByName,
|
|
427
|
+
symbolsById: new Map([...symbolsByName.values()].map((symbol) => [symbol.id, symbol])),
|
|
428
|
+
symbolsByName
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
function collectTopLevelReactSymbols(statement, filePath, symbolsByName) {
|
|
432
|
+
switch (statement.type) {
|
|
433
|
+
case "FunctionDeclaration":
|
|
434
|
+
addFunctionSymbol(statement, filePath, symbolsByName);
|
|
435
|
+
return;
|
|
436
|
+
case "VariableDeclaration":
|
|
437
|
+
statement.declarations.forEach((declarator) => {
|
|
438
|
+
addVariableSymbol(declarator, filePath, symbolsByName);
|
|
439
|
+
});
|
|
440
|
+
return;
|
|
441
|
+
case "ExportNamedDeclaration":
|
|
442
|
+
if (statement.declaration !== null) collectTopLevelReactSymbols(statement.declaration, filePath, symbolsByName);
|
|
443
|
+
return;
|
|
444
|
+
case "ExportDefaultDeclaration":
|
|
445
|
+
addDefaultExportSymbol(statement, filePath, symbolsByName);
|
|
446
|
+
return;
|
|
447
|
+
default: return;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
function addFunctionSymbol(declaration, filePath, symbolsByName) {
|
|
451
|
+
const name = declaration.id?.name;
|
|
452
|
+
if (name === void 0) return;
|
|
453
|
+
const kind = classifyReactSymbol(name, declaration);
|
|
454
|
+
if (kind === void 0) return;
|
|
455
|
+
symbolsByName.set(name, createPendingSymbol(filePath, name, kind, declaration));
|
|
456
|
+
}
|
|
457
|
+
function addVariableSymbol(declarator, filePath, symbolsByName) {
|
|
458
|
+
if (declarator.id.type !== "Identifier" || declarator.init === null) return;
|
|
459
|
+
if (declarator.init.type !== "ArrowFunctionExpression" && declarator.init.type !== "FunctionExpression") return;
|
|
460
|
+
const name = declarator.id.name;
|
|
461
|
+
const kind = classifyReactSymbol(name, declarator.init);
|
|
462
|
+
if (kind === void 0) return;
|
|
463
|
+
symbolsByName.set(name, createPendingSymbol(filePath, name, kind, declarator.init));
|
|
464
|
+
}
|
|
465
|
+
function addDefaultExportSymbol(declaration, filePath, symbolsByName) {
|
|
466
|
+
if (declaration.declaration.type === "FunctionDeclaration" || declaration.declaration.type === "FunctionExpression") addFunctionSymbol(declaration.declaration, filePath, symbolsByName);
|
|
467
|
+
else if (declaration.declaration.type === "ArrowFunctionExpression") {
|
|
468
|
+
const name = "default";
|
|
469
|
+
const kind = declaration.declaration.body ? classifyReactSymbol(name, declaration.declaration) : void 0;
|
|
470
|
+
if (kind !== void 0) symbolsByName.set(name, createPendingSymbol(filePath, name, kind, declaration.declaration));
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
function createPendingSymbol(filePath, name, kind, declaration) {
|
|
474
|
+
return {
|
|
475
|
+
id: `${filePath}#${kind}:${name}`,
|
|
476
|
+
name,
|
|
477
|
+
kind,
|
|
478
|
+
filePath,
|
|
479
|
+
declaration,
|
|
480
|
+
exportNames: /* @__PURE__ */ new Set(),
|
|
481
|
+
componentReferences: /* @__PURE__ */ new Set(),
|
|
482
|
+
hookReferences: /* @__PURE__ */ new Set()
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
function collectImportsAndExports(statement, sourceDependencies, symbolsByName, importsByLocalName, exportsByName) {
|
|
486
|
+
switch (statement.type) {
|
|
487
|
+
case "ImportDeclaration":
|
|
488
|
+
collectImportBindings(statement, sourceDependencies, importsByLocalName);
|
|
489
|
+
return;
|
|
490
|
+
case "ExportNamedDeclaration":
|
|
491
|
+
collectNamedExports(statement, symbolsByName, exportsByName);
|
|
492
|
+
return;
|
|
493
|
+
case "ExportDefaultDeclaration":
|
|
494
|
+
collectDefaultExport(statement, symbolsByName, exportsByName);
|
|
495
|
+
return;
|
|
496
|
+
default: return;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
function collectImportBindings(declaration, sourceDependencies, importsByLocalName) {
|
|
500
|
+
if (declaration.importKind === "type") return;
|
|
501
|
+
const sourceSpecifier = declaration.source.value;
|
|
502
|
+
const sourcePath = sourceDependencies.get(declaration.source.value);
|
|
503
|
+
declaration.specifiers.forEach((specifier) => {
|
|
504
|
+
const binding = getImportBinding(specifier, sourceSpecifier, sourcePath);
|
|
505
|
+
if (binding === void 0) return;
|
|
506
|
+
importsByLocalName.set(binding.localName, {
|
|
507
|
+
importedName: binding.importedName,
|
|
508
|
+
sourceSpecifier: binding.sourceSpecifier,
|
|
509
|
+
...binding.sourcePath === void 0 ? {} : { sourcePath: binding.sourcePath }
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
function getImportBinding(specifier, sourceSpecifier, sourcePath) {
|
|
514
|
+
if (specifier.type === "ImportSpecifier") {
|
|
515
|
+
if (specifier.importKind === "type") return;
|
|
516
|
+
return {
|
|
517
|
+
localName: specifier.local.name,
|
|
518
|
+
importedName: toModuleExportName(specifier.imported),
|
|
519
|
+
sourceSpecifier,
|
|
520
|
+
...sourcePath === void 0 ? {} : { sourcePath }
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
if (specifier.type === "ImportDefaultSpecifier") return {
|
|
524
|
+
localName: specifier.local.name,
|
|
525
|
+
importedName: "default",
|
|
526
|
+
sourceSpecifier,
|
|
527
|
+
...sourcePath === void 0 ? {} : { sourcePath }
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
function collectNamedExports(declaration, symbolsByName, exportsByName) {
|
|
531
|
+
if (declaration.exportKind === "type") return;
|
|
532
|
+
if (declaration.declaration !== null) {
|
|
533
|
+
if (declaration.declaration.type === "FunctionDeclaration") {
|
|
534
|
+
const name = declaration.declaration.id?.name;
|
|
535
|
+
if (name !== void 0) addExportBinding(name, name, symbolsByName, exportsByName);
|
|
536
|
+
} else if (declaration.declaration.type === "VariableDeclaration") declaration.declaration.declarations.forEach((declarator) => {
|
|
537
|
+
if (declarator.id.type === "Identifier") addExportBinding(declarator.id.name, declarator.id.name, symbolsByName, exportsByName);
|
|
538
|
+
});
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
if (declaration.source !== null) return;
|
|
542
|
+
declaration.specifiers.forEach((specifier) => {
|
|
543
|
+
if (specifier.exportKind === "type") return;
|
|
544
|
+
addExportBinding(toModuleExportName(specifier.local), toModuleExportName(specifier.exported), symbolsByName, exportsByName);
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
function collectDefaultExport(declaration, symbolsByName, exportsByName) {
|
|
548
|
+
if (declaration.declaration.type === "FunctionDeclaration" || declaration.declaration.type === "FunctionExpression") {
|
|
549
|
+
const localName = declaration.declaration.id?.name;
|
|
550
|
+
if (localName !== void 0) addExportBinding(localName, "default", symbolsByName, exportsByName);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (declaration.declaration.type === "Identifier") {
|
|
554
|
+
addExportBinding(declaration.declaration.name, "default", symbolsByName, exportsByName);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
if (declaration.declaration.type === "ArrowFunctionExpression") addExportBinding("default", "default", symbolsByName, exportsByName);
|
|
558
|
+
}
|
|
559
|
+
function addExportBinding(localName, exportedName, symbolsByName, exportsByName) {
|
|
560
|
+
const symbol = symbolsByName.get(localName);
|
|
561
|
+
if (symbol === void 0) return;
|
|
562
|
+
symbol.exportNames.add(exportedName);
|
|
563
|
+
exportsByName.set(exportedName, symbol.id);
|
|
564
|
+
}
|
|
565
|
+
function analyzeSymbolUsages(symbol) {
|
|
566
|
+
const root = symbol.declaration.type === "ArrowFunctionExpression" ? symbol.declaration.body : symbol.declaration.body;
|
|
567
|
+
if (root === null) return;
|
|
568
|
+
walkReactUsageTree(root, (node) => {
|
|
569
|
+
if (node.type === "JSXElement") {
|
|
570
|
+
const name = getComponentReferenceName(node);
|
|
571
|
+
if (name !== void 0) symbol.componentReferences.add(name);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
if (node.type === "CallExpression") {
|
|
575
|
+
const hookReference = getHookReferenceName(node);
|
|
576
|
+
if (hookReference !== void 0) symbol.hookReferences.add(hookReference);
|
|
577
|
+
const componentReference = getCreateElementComponentReferenceName(node);
|
|
578
|
+
if (componentReference !== void 0) symbol.componentReferences.add(componentReference);
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
function classifyReactSymbol(name, declaration) {
|
|
583
|
+
if (isHookName(name)) return "hook";
|
|
584
|
+
if (isComponentName(name) && returnsReactElement(declaration)) return "component";
|
|
585
|
+
}
|
|
586
|
+
function returnsReactElement(declaration) {
|
|
587
|
+
if (declaration.type === "ArrowFunctionExpression" && declaration.expression) return containsReactElementLikeExpression(declaration.body);
|
|
588
|
+
const body = declaration.body;
|
|
589
|
+
if (body === null) return false;
|
|
590
|
+
let found = false;
|
|
591
|
+
walkReactUsageTree(body, (node) => {
|
|
592
|
+
if (node.type !== "ReturnStatement" || node.argument === null) return;
|
|
593
|
+
if (containsReactElementLikeExpression(node.argument)) found = true;
|
|
594
|
+
});
|
|
595
|
+
return found;
|
|
596
|
+
}
|
|
597
|
+
function containsReactElementLikeExpression(expression) {
|
|
598
|
+
let found = false;
|
|
599
|
+
walkNode(expression, (node) => {
|
|
600
|
+
if (node.type === "JSXElement" || node.type === "JSXFragment" || node.type === "CallExpression" && isReactCreateElementCall(node)) found = true;
|
|
601
|
+
});
|
|
602
|
+
return found;
|
|
603
|
+
}
|
|
604
|
+
function getComponentReferenceName(node) {
|
|
605
|
+
const name = getJsxName(node.openingElement.name);
|
|
606
|
+
return name !== void 0 && isComponentName(name) ? name : void 0;
|
|
607
|
+
}
|
|
608
|
+
function getHookReferenceName(node) {
|
|
609
|
+
const calleeName = getIdentifierName(node.callee);
|
|
610
|
+
return calleeName !== void 0 && isHookName(calleeName) ? calleeName : void 0;
|
|
611
|
+
}
|
|
612
|
+
function getCreateElementComponentReferenceName(node) {
|
|
613
|
+
if (!isReactCreateElementCall(node)) return;
|
|
614
|
+
const [firstArgument] = node.arguments;
|
|
615
|
+
if (firstArgument === void 0 || firstArgument.type !== "Identifier") return;
|
|
616
|
+
return isComponentName(firstArgument.name) ? firstArgument.name : void 0;
|
|
617
|
+
}
|
|
618
|
+
function isReactCreateElementCall(node) {
|
|
619
|
+
const callee = unwrapExpression(node.callee);
|
|
620
|
+
if (callee.type !== "MemberExpression" || callee.computed) return false;
|
|
621
|
+
return callee.object.type === "Identifier" && callee.object.name === "React" && callee.property.name === "createElement";
|
|
622
|
+
}
|
|
623
|
+
function getJsxName(name) {
|
|
624
|
+
if (name.type === "JSXIdentifier") return name.name;
|
|
625
|
+
}
|
|
626
|
+
function getIdentifierName(expression) {
|
|
627
|
+
const unwrapped = unwrapExpression(expression);
|
|
628
|
+
return unwrapped.type === "Identifier" ? unwrapped.name : void 0;
|
|
629
|
+
}
|
|
630
|
+
function unwrapExpression(expression) {
|
|
631
|
+
let current = expression;
|
|
632
|
+
while (true) {
|
|
633
|
+
if (current.type === "ParenthesizedExpression" || current.type === "TSAsExpression" || current.type === "TSSatisfiesExpression" || current.type === "TSTypeAssertion" || current.type === "TSNonNullExpression") {
|
|
634
|
+
current = current.expression;
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
return current;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
function walkReactUsageTree(root, visit) {
|
|
641
|
+
walkNode(root, visit, true);
|
|
642
|
+
}
|
|
643
|
+
function walkNode(node, visit, allowNestedFunctions = false) {
|
|
644
|
+
visit(node);
|
|
645
|
+
const keys = visitorKeys[node.type];
|
|
646
|
+
if (keys === void 0) return;
|
|
647
|
+
keys.forEach((key) => {
|
|
648
|
+
const value = node[key];
|
|
649
|
+
walkChild(value, visit, allowNestedFunctions);
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
function walkChild(value, visit, allowNestedFunctions) {
|
|
653
|
+
if (Array.isArray(value)) {
|
|
654
|
+
value.forEach((entry) => {
|
|
655
|
+
walkChild(entry, visit, allowNestedFunctions);
|
|
656
|
+
});
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
if (!isNode(value)) return;
|
|
660
|
+
if (!allowNestedFunctions && FUNCTION_NODE_TYPES.has(value.type)) return;
|
|
661
|
+
walkNode(value, visit, false);
|
|
662
|
+
}
|
|
663
|
+
function isNode(value) {
|
|
664
|
+
return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
|
|
665
|
+
}
|
|
666
|
+
function resolveReactReference(fileAnalysis, fileAnalyses, name, kind) {
|
|
667
|
+
const localSymbol = fileAnalysis.symbolsByName.get(name);
|
|
668
|
+
if (localSymbol !== void 0 && localSymbol.kind === kind) return localSymbol.id;
|
|
669
|
+
const importBinding = fileAnalysis.importsByLocalName.get(name);
|
|
670
|
+
if (importBinding === void 0) return;
|
|
671
|
+
if (importBinding.sourcePath === void 0) return kind === "hook" ? getExternalHookNodeId(importBinding, name) : void 0;
|
|
672
|
+
const sourceFileAnalysis = fileAnalyses.get(importBinding.sourcePath);
|
|
673
|
+
if (sourceFileAnalysis === void 0) return;
|
|
674
|
+
const targetId = sourceFileAnalysis.exportsByName.get(importBinding.importedName);
|
|
675
|
+
if (targetId === void 0) return;
|
|
676
|
+
return sourceFileAnalysis.symbolsById.get(targetId)?.kind === kind ? targetId : void 0;
|
|
677
|
+
}
|
|
678
|
+
function createExternalHookNode(binding, localName) {
|
|
679
|
+
const name = getExternalHookName(binding, localName);
|
|
680
|
+
return {
|
|
681
|
+
id: getExternalHookNodeId(binding, localName),
|
|
682
|
+
name,
|
|
683
|
+
kind: "hook",
|
|
684
|
+
filePath: binding.sourceSpecifier,
|
|
685
|
+
exportNames: [binding.importedName],
|
|
686
|
+
usages: []
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
function getExternalHookNodeId(binding, localName) {
|
|
690
|
+
return `external:${binding.sourceSpecifier}#hook:${getExternalHookName(binding, localName)}`;
|
|
691
|
+
}
|
|
692
|
+
function getExternalHookName(binding, localName) {
|
|
693
|
+
return binding.importedName === "default" ? localName : binding.importedName;
|
|
694
|
+
}
|
|
695
|
+
function getFilteredReactUsageNodes(graph, filter) {
|
|
696
|
+
return [...graph.nodes.values()].filter((node) => matchesReactFilter(node, filter)).sort((left, right) => compareReactNodes(left, right));
|
|
697
|
+
}
|
|
698
|
+
function matchesReactFilter(node, filter) {
|
|
699
|
+
return filter === "all" || node.kind === filter;
|
|
700
|
+
}
|
|
701
|
+
function serializeReactUsageNode(nodeId, graph, filter, visited) {
|
|
702
|
+
const node = graph.nodes.get(nodeId);
|
|
703
|
+
if (node === void 0) return {
|
|
704
|
+
id: nodeId,
|
|
705
|
+
name: nodeId,
|
|
706
|
+
symbolKind: "circular",
|
|
707
|
+
filePath: "",
|
|
708
|
+
exportNames: [],
|
|
709
|
+
usages: []
|
|
710
|
+
};
|
|
711
|
+
if (visited.has(nodeId)) return {
|
|
712
|
+
id: node.id,
|
|
713
|
+
name: node.name,
|
|
714
|
+
symbolKind: "circular",
|
|
715
|
+
filePath: toDisplayPath(node.filePath, graph.cwd),
|
|
716
|
+
exportNames: node.exportNames,
|
|
717
|
+
usages: []
|
|
718
|
+
};
|
|
719
|
+
const nextVisited = new Set(visited);
|
|
720
|
+
nextVisited.add(nodeId);
|
|
721
|
+
return {
|
|
722
|
+
id: node.id,
|
|
723
|
+
name: node.name,
|
|
724
|
+
symbolKind: node.kind,
|
|
725
|
+
filePath: toDisplayPath(node.filePath, graph.cwd),
|
|
726
|
+
exportNames: node.exportNames,
|
|
727
|
+
usages: getFilteredUsages(node, graph, filter).map((usage) => ({
|
|
728
|
+
kind: usage.kind,
|
|
729
|
+
targetId: usage.target,
|
|
730
|
+
node: serializeReactUsageNode(usage.target, graph, filter, nextVisited)
|
|
731
|
+
}))
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
function compareReactNodeIds(leftId, rightId, nodes) {
|
|
735
|
+
const left = nodes.get(leftId);
|
|
736
|
+
const right = nodes.get(rightId);
|
|
737
|
+
if (left === void 0 || right === void 0) return leftId.localeCompare(rightId);
|
|
738
|
+
return compareReactNodes(left, right);
|
|
739
|
+
}
|
|
740
|
+
function compareReactNodes(left, right) {
|
|
741
|
+
return left.filePath.localeCompare(right.filePath) || left.name.localeCompare(right.name) || left.kind.localeCompare(right.kind);
|
|
742
|
+
}
|
|
743
|
+
function toModuleExportName(name) {
|
|
744
|
+
return name.type === "Literal" ? name.value : name.name;
|
|
745
|
+
}
|
|
746
|
+
function isHookName(name) {
|
|
747
|
+
return /^use[A-Z0-9]/.test(name);
|
|
748
|
+
}
|
|
749
|
+
function isComponentName(name) {
|
|
750
|
+
return /^[A-Z]/.test(name);
|
|
751
|
+
}
|
|
752
|
+
//#endregion
|
|
753
|
+
//#region src/react-tree.ts
|
|
754
|
+
function printReactUsageTree(graph, options = {}) {
|
|
755
|
+
const cwd = options.cwd ?? graph.cwd;
|
|
756
|
+
const filter = options.filter ?? "all";
|
|
757
|
+
const roots = getReactUsageRoots(graph, filter);
|
|
758
|
+
if (roots.length === 0) return "No React symbols found.";
|
|
759
|
+
const lines = [];
|
|
760
|
+
roots.forEach((rootId, index) => {
|
|
761
|
+
const root = graph.nodes.get(rootId);
|
|
762
|
+
if (root === void 0) return;
|
|
763
|
+
lines.push(formatReactNodeLabel(root, cwd));
|
|
764
|
+
const usages = getFilteredUsages(root, graph, filter);
|
|
765
|
+
usages.forEach((usage, usageIndex) => {
|
|
766
|
+
lines.push(...renderUsage(usage, graph, cwd, filter, new Set([root.id]), "", usageIndex === usages.length - 1));
|
|
767
|
+
});
|
|
768
|
+
if (index < roots.length - 1) lines.push("");
|
|
769
|
+
});
|
|
770
|
+
return lines.join("\n");
|
|
771
|
+
}
|
|
772
|
+
function renderUsage(usage, graph, cwd, filter, visited, prefix, isLast) {
|
|
773
|
+
const branch = `${prefix}${isLast ? "└─ " : "├─ "}`;
|
|
774
|
+
const target = graph.nodes.get(usage.target);
|
|
775
|
+
if (target === void 0) return [`${branch}${usage.target}`];
|
|
776
|
+
if (visited.has(target.id)) return [`${branch}${formatReactNodeLabel(target, cwd)} (circular)`];
|
|
777
|
+
const childLines = [`${branch}${formatReactNodeLabel(target, cwd)}`];
|
|
778
|
+
const nextVisited = new Set(visited);
|
|
779
|
+
nextVisited.add(target.id);
|
|
780
|
+
const nextPrefix = `${prefix}${isLast ? " " : "│ "}`;
|
|
781
|
+
const childUsages = getFilteredUsages(target, graph, filter);
|
|
782
|
+
childUsages.forEach((childUsage, index) => {
|
|
783
|
+
childLines.push(...renderUsage(childUsage, graph, cwd, filter, nextVisited, nextPrefix, index === childUsages.length - 1));
|
|
784
|
+
});
|
|
785
|
+
return childLines;
|
|
786
|
+
}
|
|
787
|
+
function formatReactNodeLabel(node, cwd) {
|
|
788
|
+
return `${node.name} [${node.kind}] (${toDisplayPath(node.filePath, cwd)})`;
|
|
789
|
+
}
|
|
790
|
+
//#endregion
|
|
791
|
+
//#region src/tree.ts
|
|
792
|
+
function printDependencyTree(graph, options = {}) {
|
|
793
|
+
const cwd = options.cwd ?? graph.cwd;
|
|
794
|
+
const includeExternals = options.includeExternals ?? false;
|
|
795
|
+
const omitUnused = options.omitUnused ?? false;
|
|
796
|
+
const rootLines = [toDisplayPath(graph.entryId, cwd)];
|
|
797
|
+
const visited = new Set([graph.entryId]);
|
|
798
|
+
const entryNode = graph.nodes.get(graph.entryId);
|
|
799
|
+
if (entryNode === void 0) return rootLines.join("\n");
|
|
800
|
+
const rootDependencies = filterDependencies(entryNode.dependencies, includeExternals, omitUnused);
|
|
801
|
+
rootDependencies.forEach((dependency, index) => {
|
|
802
|
+
const lines = renderDependency(dependency, graph, visited, "", index === rootDependencies.length - 1, includeExternals, omitUnused, cwd);
|
|
803
|
+
rootLines.push(...lines);
|
|
804
|
+
});
|
|
805
|
+
return rootLines.join("\n");
|
|
806
|
+
}
|
|
807
|
+
function renderDependency(dependency, graph, visited, prefix, isLast, includeExternals, omitUnused, cwd) {
|
|
808
|
+
const branch = `${prefix}${isLast ? "└─ " : "├─ "}`;
|
|
809
|
+
const label = formatDependencyLabel(dependency, graph, cwd);
|
|
810
|
+
if (dependency.kind !== "source") return [`${branch}${label}`];
|
|
811
|
+
if (visited.has(dependency.target)) return [`${branch}${label} (circular)`];
|
|
812
|
+
const childNode = graph.nodes.get(dependency.target);
|
|
813
|
+
if (childNode === void 0) return [`${branch}${label}`];
|
|
814
|
+
const nextPrefix = `${prefix}${isLast ? " " : "│ "}`;
|
|
815
|
+
const nextVisited = new Set(visited);
|
|
816
|
+
nextVisited.add(dependency.target);
|
|
817
|
+
const childLines = [`${branch}${label}`];
|
|
818
|
+
const childDependencies = filterDependencies(childNode.dependencies, includeExternals, omitUnused);
|
|
819
|
+
childDependencies.forEach((childDependency, index) => {
|
|
820
|
+
const isChildLast = index === childDependencies.length - 1;
|
|
821
|
+
childLines.push(...renderDependency(childDependency, graph, nextVisited, nextPrefix, isChildLast, includeExternals, omitUnused, cwd));
|
|
822
|
+
});
|
|
823
|
+
return childLines;
|
|
824
|
+
}
|
|
825
|
+
function filterDependencies(dependencies, includeExternals, omitUnused) {
|
|
826
|
+
return dependencies.filter((dependency) => {
|
|
827
|
+
if (omitUnused && dependency.unused) return false;
|
|
828
|
+
if (dependency.kind === "source" || dependency.kind === "missing") return true;
|
|
829
|
+
return includeExternals;
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
function formatDependencyLabel(dependency, _graph, cwd) {
|
|
833
|
+
const prefixes = [];
|
|
834
|
+
if (dependency.isTypeOnly) prefixes.push("type");
|
|
835
|
+
if (dependency.referenceKind === "require") prefixes.push("require");
|
|
836
|
+
else if (dependency.referenceKind === "dynamic-import") prefixes.push("dynamic");
|
|
837
|
+
else if (dependency.referenceKind === "export") prefixes.push("re-export");
|
|
838
|
+
else if (dependency.referenceKind === "import-equals") prefixes.push("import=");
|
|
839
|
+
const annotation = prefixes.length > 0 ? `[${prefixes.join(", ")}] ` : "";
|
|
840
|
+
if (dependency.kind === "source") return withUnusedSuffix(`${annotation}${toDisplayPath(dependency.target, cwd)}`, dependency.unused);
|
|
841
|
+
if (dependency.kind === "missing") return withUnusedSuffix(`${annotation}${dependency.specifier} [missing]`, dependency.unused);
|
|
842
|
+
if (dependency.kind === "builtin") return withUnusedSuffix(`${annotation}${dependency.target} [builtin]`, dependency.unused);
|
|
843
|
+
return withUnusedSuffix(`${annotation}${dependency.target} [external]`, dependency.unused);
|
|
844
|
+
}
|
|
845
|
+
function withUnusedSuffix(label, unused) {
|
|
846
|
+
return unused ? `${label} (unused)` : label;
|
|
847
|
+
}
|
|
848
|
+
//#endregion
|
|
849
|
+
export { graphToSerializableReactTree as a, getReactUsageRoots as i, printReactUsageTree as n, analyzeDependencies as o, analyzeReactUsage as r, graphToSerializableTree as s, printDependencyTree as t };
|
|
850
|
+
|
|
851
|
+
//# sourceMappingURL=tree-DbnWcUnV.mjs.map
|