foresthouse 1.0.0-dev.7 → 1.0.0-dev.8
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 +15 -2
- package/dist/cli.mjs +155 -118
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +71 -31
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/{tree-BX1YFcqX.mjs → react-CWIt8V0Y.mjs} +669 -570
- package/dist/react-CWIt8V0Y.mjs.map +1 -0
- package/package.json +2 -1
- package/dist/tree-BX1YFcqX.mjs.map +0 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { builtinModules } from "node:module";
|
|
2
|
-
import fs from "node:fs";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import ts from "typescript";
|
|
4
|
+
import fs from "node:fs";
|
|
5
5
|
import { parseSync, visitorKeys } from "oxc-parser";
|
|
6
6
|
import process$1 from "node:process";
|
|
7
|
-
//#region src/config.ts
|
|
7
|
+
//#region src/typescript/config.ts
|
|
8
8
|
function loadCompilerOptions(searchFrom, explicitConfigPath) {
|
|
9
9
|
const configPath = explicitConfigPath === void 0 ? findNearestConfig(searchFrom) : path.resolve(searchFrom, explicitConfigPath);
|
|
10
10
|
if (configPath === void 0) return { compilerOptions: defaultCompilerOptions() };
|
|
@@ -48,7 +48,18 @@ function formatDiagnostic(diagnostic) {
|
|
|
48
48
|
return ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
|
|
49
49
|
}
|
|
50
50
|
//#endregion
|
|
51
|
-
//#region src/
|
|
51
|
+
//#region src/analyzers/base.ts
|
|
52
|
+
var BaseAnalyzer = class {
|
|
53
|
+
constructor(entryFile, options) {
|
|
54
|
+
this.entryFile = entryFile;
|
|
55
|
+
this.options = options;
|
|
56
|
+
}
|
|
57
|
+
analyze() {
|
|
58
|
+
return this.doAnalyze();
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region src/utils/is-source-code-file.ts
|
|
52
63
|
const SOURCE_EXTENSIONS = new Set([
|
|
53
64
|
".js",
|
|
54
65
|
".jsx",
|
|
@@ -59,159 +70,24 @@ const SOURCE_EXTENSIONS = new Set([
|
|
|
59
70
|
".mts",
|
|
60
71
|
".cts"
|
|
61
72
|
]);
|
|
62
|
-
function normalizeFilePath(filePath) {
|
|
63
|
-
return path.normalize(filePath);
|
|
64
|
-
}
|
|
65
|
-
function toDisplayPath(filePath, cwd) {
|
|
66
|
-
const relativePath = path.relative(cwd, filePath);
|
|
67
|
-
if (relativePath === "") return ".";
|
|
68
|
-
const normalizedPath = relativePath.split(path.sep).join("/");
|
|
69
|
-
return normalizedPath.startsWith("..") ? filePath : normalizedPath;
|
|
70
|
-
}
|
|
71
73
|
function isSourceCodeFile(filePath) {
|
|
72
74
|
return SOURCE_EXTENSIONS.has(path.extname(filePath).toLowerCase());
|
|
73
75
|
}
|
|
74
76
|
//#endregion
|
|
75
|
-
//#region src/
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
79
|
-
const resolvedEntryPath = resolveExistingPath(cwd, entryFile);
|
|
80
|
-
const { compilerOptions, path: configPath } = loadCompilerOptions(path.dirname(resolvedEntryPath), options.configPath);
|
|
81
|
-
const host = {
|
|
82
|
-
fileExists: ts.sys.fileExists,
|
|
83
|
-
readFile: ts.sys.readFile,
|
|
84
|
-
directoryExists: ts.sys.directoryExists,
|
|
85
|
-
getCurrentDirectory: () => cwd,
|
|
86
|
-
getDirectories: ts.sys.getDirectories,
|
|
87
|
-
...ts.sys.realpath === void 0 ? {} : { realpath: ts.sys.realpath }
|
|
88
|
-
};
|
|
89
|
-
const nodes = /* @__PURE__ */ new Map();
|
|
90
|
-
const program = createProgram(resolvedEntryPath, compilerOptions, cwd);
|
|
91
|
-
visitFile(resolvedEntryPath, compilerOptions, host, program.getTypeChecker(), program, nodes);
|
|
92
|
-
return {
|
|
93
|
-
cwd,
|
|
94
|
-
entryId: resolvedEntryPath,
|
|
95
|
-
nodes,
|
|
96
|
-
...configPath === void 0 ? {} : { configPath }
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
function graphToSerializableTree(graph, options = {}) {
|
|
100
|
-
const visited = /* @__PURE__ */ new Set();
|
|
101
|
-
return serializeNode(graph.entryId, graph, visited, options.omitUnused ?? false);
|
|
102
|
-
}
|
|
103
|
-
function serializeNode(filePath, graph, visited, omitUnused) {
|
|
104
|
-
const node = graph.nodes.get(filePath);
|
|
105
|
-
const displayPath = toDisplayPath(filePath, graph.cwd);
|
|
106
|
-
if (node === void 0) return {
|
|
107
|
-
path: displayPath,
|
|
108
|
-
kind: "missing",
|
|
109
|
-
dependencies: []
|
|
110
|
-
};
|
|
111
|
-
if (visited.has(filePath)) return {
|
|
112
|
-
path: displayPath,
|
|
113
|
-
kind: "circular",
|
|
114
|
-
dependencies: []
|
|
115
|
-
};
|
|
116
|
-
visited.add(filePath);
|
|
117
|
-
const dependencies = node.dependencies.filter((dependency) => !omitUnused || !dependency.unused).map((dependency) => {
|
|
118
|
-
if (dependency.kind !== "source") return {
|
|
119
|
-
specifier: dependency.specifier,
|
|
120
|
-
referenceKind: dependency.referenceKind,
|
|
121
|
-
isTypeOnly: dependency.isTypeOnly,
|
|
122
|
-
unused: dependency.unused,
|
|
123
|
-
kind: dependency.kind,
|
|
124
|
-
target: dependency.kind === "missing" ? dependency.target : toDisplayPath(dependency.target, graph.cwd)
|
|
125
|
-
};
|
|
126
|
-
return {
|
|
127
|
-
specifier: dependency.specifier,
|
|
128
|
-
referenceKind: dependency.referenceKind,
|
|
129
|
-
isTypeOnly: dependency.isTypeOnly,
|
|
130
|
-
unused: dependency.unused,
|
|
131
|
-
kind: dependency.kind,
|
|
132
|
-
target: toDisplayPath(dependency.target, graph.cwd),
|
|
133
|
-
node: serializeNode(dependency.target, graph, new Set(visited), omitUnused)
|
|
134
|
-
};
|
|
135
|
-
});
|
|
136
|
-
return {
|
|
137
|
-
path: displayPath,
|
|
138
|
-
kind: filePath === graph.entryId ? "entry" : "source",
|
|
139
|
-
dependencies
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
function visitFile(filePath, compilerOptions, host, checker, program, nodes) {
|
|
143
|
-
const normalizedPath = normalizeFilePath(filePath);
|
|
144
|
-
if (nodes.has(normalizedPath)) return;
|
|
145
|
-
const dependencies = collectModuleReferences(program.getSourceFile(normalizedPath) ?? createSourceFile(normalizedPath), checker).map((reference) => resolveDependency(reference, normalizedPath, compilerOptions, host));
|
|
146
|
-
nodes.set(normalizedPath, {
|
|
147
|
-
id: normalizedPath,
|
|
148
|
-
dependencies
|
|
149
|
-
});
|
|
150
|
-
for (const dependency of dependencies) if (dependency.kind === "source") visitFile(dependency.target, compilerOptions, host, checker, program, nodes);
|
|
151
|
-
}
|
|
152
|
-
function collectModuleReferences(sourceFile, checker) {
|
|
153
|
-
const references = /* @__PURE__ */ new Map();
|
|
154
|
-
const unusedImports = collectUnusedImports(sourceFile, checker);
|
|
155
|
-
function addReference(specifier, referenceKind, isTypeOnly, unused) {
|
|
156
|
-
const key = `${referenceKind}:${isTypeOnly ? "type" : "value"}:${specifier}`;
|
|
157
|
-
const existing = references.get(key);
|
|
158
|
-
if (existing !== void 0) {
|
|
159
|
-
if (existing.unused && !unused) references.set(key, {
|
|
160
|
-
...existing,
|
|
161
|
-
unused: false
|
|
162
|
-
});
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
references.set(key, {
|
|
166
|
-
specifier,
|
|
167
|
-
referenceKind,
|
|
168
|
-
isTypeOnly,
|
|
169
|
-
unused
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
function visit(node) {
|
|
173
|
-
if (ts.isImportDeclaration(node) && ts.isStringLiteralLike(node.moduleSpecifier)) addReference(node.moduleSpecifier.text, "import", node.importClause?.isTypeOnly ?? false, unusedImports.get(node) ?? false);
|
|
174
|
-
else if (ts.isExportDeclaration(node) && node.moduleSpecifier !== void 0 && ts.isStringLiteralLike(node.moduleSpecifier)) addReference(node.moduleSpecifier.text, "export", node.isTypeOnly ?? false, false);
|
|
175
|
-
else if (ts.isImportEqualsDeclaration(node)) {
|
|
176
|
-
const moduleReference = node.moduleReference;
|
|
177
|
-
if (ts.isExternalModuleReference(moduleReference) && moduleReference.expression !== void 0 && ts.isStringLiteralLike(moduleReference.expression)) addReference(moduleReference.expression.text, "import-equals", false, false);
|
|
178
|
-
} else if (ts.isCallExpression(node)) {
|
|
179
|
-
if (node.expression.kind === ts.SyntaxKind.ImportKeyword && node.arguments.length === 1) {
|
|
180
|
-
const [argument] = node.arguments;
|
|
181
|
-
if (argument !== void 0 && ts.isStringLiteralLike(argument)) addReference(argument.text, "dynamic-import", false, false);
|
|
182
|
-
}
|
|
183
|
-
if (ts.isIdentifier(node.expression) && node.expression.text === "require" && node.arguments.length === 1) {
|
|
184
|
-
const [argument] = node.arguments;
|
|
185
|
-
if (argument !== void 0 && ts.isStringLiteralLike(argument)) addReference(argument.text, "require", false, false);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
ts.forEachChild(node, visit);
|
|
189
|
-
}
|
|
190
|
-
visit(sourceFile);
|
|
191
|
-
return [...references.values()];
|
|
192
|
-
}
|
|
193
|
-
function resolveDependency(reference, containingFile, compilerOptions, host) {
|
|
194
|
-
const specifier = reference.specifier;
|
|
195
|
-
if (BUILTIN_MODULES.has(specifier)) return createEdge(reference, "builtin", specifier);
|
|
196
|
-
const resolution = ts.resolveModuleName(specifier, containingFile, compilerOptions, host).resolvedModule;
|
|
197
|
-
if (resolution !== void 0) {
|
|
198
|
-
const resolvedPath = normalizeFilePath(resolution.resolvedFileName);
|
|
199
|
-
if (resolution.isExternalLibraryImport || resolvedPath.includes(`${path.sep}node_modules${path.sep}`)) return createEdge(reference, "external", specifier);
|
|
200
|
-
if (isSourceCodeFile(resolvedPath) && !resolvedPath.endsWith(".d.ts")) return createEdge(reference, "source", resolvedPath);
|
|
201
|
-
}
|
|
202
|
-
if (!specifier.startsWith(".") && !path.isAbsolute(specifier)) return createEdge(reference, "external", specifier);
|
|
203
|
-
return createEdge(reference, "missing", specifier);
|
|
77
|
+
//#region src/utils/normalize-file-path.ts
|
|
78
|
+
function normalizeFilePath(filePath) {
|
|
79
|
+
return path.normalize(filePath);
|
|
204
80
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
target
|
|
213
|
-
};
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/analyzers/import/entry.ts
|
|
83
|
+
function resolveExistingPath(cwd, entryFile) {
|
|
84
|
+
const normalizedPath = normalizeFilePath(path.resolve(cwd, entryFile));
|
|
85
|
+
if (!fs.existsSync(normalizedPath)) throw new Error(`Entry file not found: ${entryFile}`);
|
|
86
|
+
if (!isSourceCodeFile(normalizedPath)) throw new Error(`Entry file must be a JS/TS source file: ${entryFile}`);
|
|
87
|
+
return normalizedPath;
|
|
214
88
|
}
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/typescript/program.ts
|
|
215
91
|
function createProgram(entryFile, compilerOptions, cwd) {
|
|
216
92
|
const host = ts.createCompilerHost(compilerOptions, true);
|
|
217
93
|
host.getCurrentDirectory = () => cwd;
|
|
@@ -226,6 +102,29 @@ function createSourceFile(filePath) {
|
|
|
226
102
|
const sourceText = fs.readFileSync(filePath, "utf8");
|
|
227
103
|
return ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true, getScriptKind(filePath));
|
|
228
104
|
}
|
|
105
|
+
function createModuleResolutionHost(cwd) {
|
|
106
|
+
return {
|
|
107
|
+
fileExists: ts.sys.fileExists,
|
|
108
|
+
readFile: ts.sys.readFile,
|
|
109
|
+
directoryExists: ts.sys.directoryExists,
|
|
110
|
+
getCurrentDirectory: () => cwd,
|
|
111
|
+
getDirectories: ts.sys.getDirectories,
|
|
112
|
+
...ts.sys.realpath === void 0 ? {} : { realpath: ts.sys.realpath }
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function getScriptKind(filePath) {
|
|
116
|
+
switch (path.extname(filePath).toLowerCase()) {
|
|
117
|
+
case ".js":
|
|
118
|
+
case ".mjs":
|
|
119
|
+
case ".cjs": return ts.ScriptKind.JS;
|
|
120
|
+
case ".jsx": return ts.ScriptKind.JSX;
|
|
121
|
+
case ".tsx": return ts.ScriptKind.TSX;
|
|
122
|
+
case ".json": return ts.ScriptKind.JSON;
|
|
123
|
+
default: return ts.ScriptKind.TS;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/analyzers/import/unused.ts
|
|
229
128
|
function collectUnusedImports(sourceFile, checker) {
|
|
230
129
|
const importUsage = /* @__PURE__ */ new Map();
|
|
231
130
|
const symbolToImportDeclaration = /* @__PURE__ */ new Map();
|
|
@@ -293,169 +192,316 @@ function tryGetSymbolAtLocation(checker, node) {
|
|
|
293
192
|
return;
|
|
294
193
|
}
|
|
295
194
|
}
|
|
296
|
-
function getScriptKind(filePath) {
|
|
297
|
-
switch (path.extname(filePath).toLowerCase()) {
|
|
298
|
-
case ".js":
|
|
299
|
-
case ".mjs":
|
|
300
|
-
case ".cjs": return ts.ScriptKind.JS;
|
|
301
|
-
case ".jsx": return ts.ScriptKind.JSX;
|
|
302
|
-
case ".tsx": return ts.ScriptKind.TSX;
|
|
303
|
-
case ".json": return ts.ScriptKind.JSON;
|
|
304
|
-
default: return ts.ScriptKind.TS;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
function resolveExistingPath(cwd, entryFile) {
|
|
308
|
-
const normalizedPath = normalizeFilePath(path.resolve(cwd, entryFile));
|
|
309
|
-
if (!fs.existsSync(normalizedPath)) throw new Error(`Entry file not found: ${entryFile}`);
|
|
310
|
-
if (!isSourceCodeFile(normalizedPath)) throw new Error(`Entry file must be a JS/TS source file: ${entryFile}`);
|
|
311
|
-
return normalizedPath;
|
|
312
|
-
}
|
|
313
195
|
//#endregion
|
|
314
|
-
//#region src/
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const fileAnalyses = /* @__PURE__ */ new Map();
|
|
326
|
-
for (const filePath of [...reachableFiles].sort()) {
|
|
327
|
-
if (!isSourceCodeFile(filePath) || filePath.endsWith(".d.ts")) continue;
|
|
328
|
-
const sourceText = fs.readFileSync(filePath, "utf8");
|
|
329
|
-
const parseResult = parseSync(filePath, sourceText, {
|
|
330
|
-
astType: "ts",
|
|
331
|
-
sourceType: "unambiguous"
|
|
332
|
-
});
|
|
333
|
-
const dependencyNode = dependencyGraph.nodes.get(filePath);
|
|
334
|
-
const sourceDependencies = /* @__PURE__ */ new Map();
|
|
335
|
-
dependencyNode?.dependencies.forEach((dependency) => {
|
|
336
|
-
if (dependency.kind === "source") sourceDependencies.set(dependency.specifier, dependency.target);
|
|
337
|
-
});
|
|
338
|
-
fileAnalyses.set(filePath, analyzeReactFile(parseResult.program, filePath, sourceText, filePath === dependencyGraph.entryId, sourceDependencies));
|
|
339
|
-
}
|
|
340
|
-
const nodes = /* @__PURE__ */ new Map();
|
|
341
|
-
for (const fileAnalysis of fileAnalyses.values()) for (const symbol of fileAnalysis.symbolsById.values()) nodes.set(symbol.id, {
|
|
342
|
-
id: symbol.id,
|
|
343
|
-
name: symbol.name,
|
|
344
|
-
kind: symbol.kind,
|
|
345
|
-
filePath: symbol.filePath,
|
|
346
|
-
exportNames: [...symbol.exportNames].sort(),
|
|
347
|
-
usages: []
|
|
348
|
-
});
|
|
349
|
-
for (const fileAnalysis of fileAnalyses.values()) fileAnalysis.importsByLocalName.forEach((binding, localName) => {
|
|
350
|
-
if (binding.sourcePath !== void 0) return;
|
|
351
|
-
if (!isHookName(localName) && !isHookName(binding.importedName)) return;
|
|
352
|
-
const externalNode = createExternalHookNode(binding, localName);
|
|
353
|
-
if (!nodes.has(externalNode.id)) nodes.set(externalNode.id, externalNode);
|
|
354
|
-
});
|
|
355
|
-
for (const fileAnalysis of fileAnalyses.values()) for (const symbol of fileAnalysis.symbolsById.values()) {
|
|
356
|
-
const usages = /* @__PURE__ */ new Map();
|
|
357
|
-
symbol.componentReferences.forEach((referenceName) => {
|
|
358
|
-
const targetId = resolveReactReference(fileAnalysis, fileAnalyses, referenceName, "component");
|
|
359
|
-
if (targetId !== void 0 && targetId !== symbol.id) usages.set(`render:${targetId}`, {
|
|
360
|
-
kind: "render",
|
|
361
|
-
target: targetId
|
|
362
|
-
});
|
|
363
|
-
});
|
|
364
|
-
symbol.hookReferences.forEach((referenceName) => {
|
|
365
|
-
const targetId = resolveReactReference(fileAnalysis, fileAnalyses, referenceName, "hook");
|
|
366
|
-
if (targetId !== void 0 && targetId !== symbol.id) usages.set(`hook:${targetId}`, {
|
|
367
|
-
kind: "hook-call",
|
|
368
|
-
target: targetId
|
|
196
|
+
//#region src/analyzers/import/references.ts
|
|
197
|
+
function collectModuleReferences(sourceFile, checker) {
|
|
198
|
+
const references = /* @__PURE__ */ new Map();
|
|
199
|
+
const unusedImports = collectUnusedImports(sourceFile, checker);
|
|
200
|
+
function addReference(specifier, referenceKind, isTypeOnly, unused) {
|
|
201
|
+
const key = `${referenceKind}:${isTypeOnly ? "type" : "value"}:${specifier}`;
|
|
202
|
+
const existing = references.get(key);
|
|
203
|
+
if (existing !== void 0) {
|
|
204
|
+
if (existing.unused && !unused) references.set(key, {
|
|
205
|
+
...existing,
|
|
206
|
+
unused: false
|
|
369
207
|
});
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
references.set(key, {
|
|
211
|
+
specifier,
|
|
212
|
+
referenceKind,
|
|
213
|
+
isTypeOnly,
|
|
214
|
+
unused
|
|
377
215
|
});
|
|
378
216
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
if (
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
217
|
+
function visit(node) {
|
|
218
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteralLike(node.moduleSpecifier)) addReference(node.moduleSpecifier.text, "import", node.importClause?.isTypeOnly ?? false, unusedImports.get(node) ?? false);
|
|
219
|
+
else if (ts.isExportDeclaration(node) && node.moduleSpecifier !== void 0 && ts.isStringLiteralLike(node.moduleSpecifier)) addReference(node.moduleSpecifier.text, "export", node.isTypeOnly ?? false, false);
|
|
220
|
+
else if (ts.isImportEqualsDeclaration(node)) {
|
|
221
|
+
const moduleReference = node.moduleReference;
|
|
222
|
+
if (ts.isExternalModuleReference(moduleReference) && moduleReference.expression !== void 0 && ts.isStringLiteralLike(moduleReference.expression)) addReference(moduleReference.expression.text, "import-equals", false, false);
|
|
223
|
+
} else if (ts.isCallExpression(node)) {
|
|
224
|
+
if (node.expression.kind === ts.SyntaxKind.ImportKeyword && node.arguments.length === 1) {
|
|
225
|
+
const [argument] = node.arguments;
|
|
226
|
+
if (argument !== void 0 && ts.isStringLiteralLike(argument)) addReference(argument.text, "dynamic-import", false, false);
|
|
227
|
+
}
|
|
228
|
+
if (ts.isIdentifier(node.expression) && node.expression.text === "require" && node.arguments.length === 1) {
|
|
229
|
+
const [argument] = node.arguments;
|
|
230
|
+
if (argument !== void 0 && ts.isStringLiteralLike(argument)) addReference(argument.text, "require", false, false);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
ts.forEachChild(node, visit);
|
|
388
234
|
}
|
|
389
|
-
|
|
390
|
-
return
|
|
391
|
-
cwd: dependencyGraph.cwd,
|
|
392
|
-
entryId: dependencyGraph.entryId,
|
|
393
|
-
nodes,
|
|
394
|
-
entries
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
function graphToSerializableReactTree(graph, options = {}) {
|
|
398
|
-
const filter = options.filter ?? "all";
|
|
399
|
-
const entries = getReactUsageEntries(graph, filter);
|
|
400
|
-
const roots = entries.length > 0 ? entries.map((entry) => serializeReactUsageNode(entry.target, graph, filter, /* @__PURE__ */ new Set())) : getReactUsageRoots(graph, filter).map((rootId) => serializeReactUsageNode(rootId, graph, filter, /* @__PURE__ */ new Set()));
|
|
401
|
-
return {
|
|
402
|
-
kind: "react-usage",
|
|
403
|
-
entries: entries.map((entry) => serializeReactUsageEntry(entry, graph, filter)),
|
|
404
|
-
roots
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
|
-
function getReactUsageEntries(graph, filter = "all") {
|
|
408
|
-
return graph.entries.filter((entry) => {
|
|
409
|
-
const targetNode = graph.nodes.get(entry.target);
|
|
410
|
-
return targetNode !== void 0 && matchesReactFilter(targetNode, filter);
|
|
411
|
-
});
|
|
235
|
+
visit(sourceFile);
|
|
236
|
+
return [...references.values()];
|
|
412
237
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
238
|
+
//#endregion
|
|
239
|
+
//#region src/analyzers/import/resolver.ts
|
|
240
|
+
const BUILTIN_MODULES = new Set(builtinModules.flatMap((name) => [name, `node:${name}`]));
|
|
241
|
+
function resolveDependency(reference, containingFile, compilerOptions, host) {
|
|
242
|
+
const specifier = reference.specifier;
|
|
243
|
+
if (BUILTIN_MODULES.has(specifier)) return createEdge(reference, "builtin", specifier);
|
|
244
|
+
const resolution = ts.resolveModuleName(specifier, containingFile, compilerOptions, host).resolvedModule;
|
|
245
|
+
if (resolution !== void 0) {
|
|
246
|
+
const resolvedPath = normalizeFilePath(resolution.resolvedFileName);
|
|
247
|
+
if (resolution.isExternalLibraryImport || resolvedPath.includes(`${path.sep}node_modules${path.sep}`)) return createEdge(reference, "external", specifier);
|
|
248
|
+
if (isSourceCodeFile(resolvedPath) && !resolvedPath.endsWith(".d.ts")) return createEdge(reference, "source", resolvedPath);
|
|
249
|
+
}
|
|
250
|
+
if (!specifier.startsWith(".") && !path.isAbsolute(specifier)) return createEdge(reference, "external", specifier);
|
|
251
|
+
return createEdge(reference, "missing", specifier);
|
|
252
|
+
}
|
|
253
|
+
function createEdge(reference, kind, target) {
|
|
254
|
+
return {
|
|
255
|
+
specifier: reference.specifier,
|
|
256
|
+
referenceKind: reference.referenceKind,
|
|
257
|
+
isTypeOnly: reference.isTypeOnly,
|
|
258
|
+
unused: reference.unused,
|
|
259
|
+
kind,
|
|
260
|
+
target
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
//#endregion
|
|
264
|
+
//#region src/analyzers/import/graph.ts
|
|
265
|
+
function buildDependencyGraph(entryPath, compilerOptions, cwd) {
|
|
266
|
+
return new DependencyGraphBuilder(entryPath, compilerOptions, cwd).build();
|
|
267
|
+
}
|
|
268
|
+
var DependencyGraphBuilder = class {
|
|
269
|
+
host;
|
|
270
|
+
nodes = /* @__PURE__ */ new Map();
|
|
271
|
+
program;
|
|
272
|
+
checker;
|
|
273
|
+
constructor(entryPath, compilerOptions, cwd) {
|
|
274
|
+
this.entryPath = entryPath;
|
|
275
|
+
this.compilerOptions = compilerOptions;
|
|
276
|
+
this.host = createModuleResolutionHost(cwd);
|
|
277
|
+
this.program = createProgram(entryPath, compilerOptions, cwd);
|
|
278
|
+
this.checker = this.program.getTypeChecker();
|
|
279
|
+
}
|
|
280
|
+
build() {
|
|
281
|
+
this.visitFile(this.entryPath);
|
|
282
|
+
return this.nodes;
|
|
283
|
+
}
|
|
284
|
+
visitFile(filePath) {
|
|
285
|
+
const normalizedPath = normalizeFilePath(filePath);
|
|
286
|
+
if (this.nodes.has(normalizedPath)) return;
|
|
287
|
+
const dependencies = collectModuleReferences(this.program.getSourceFile(normalizedPath) ?? createSourceFile(normalizedPath), this.checker).map((reference) => resolveDependency(reference, normalizedPath, this.compilerOptions, this.host));
|
|
288
|
+
this.nodes.set(normalizedPath, {
|
|
289
|
+
id: normalizedPath,
|
|
290
|
+
dependencies
|
|
291
|
+
});
|
|
292
|
+
for (const dependency of dependencies) if (dependency.kind === "source") this.visitFile(dependency.target);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
//#endregion
|
|
296
|
+
//#region src/analyzers/import/index.ts
|
|
297
|
+
function analyzeDependencies(entryFile, options = {}) {
|
|
298
|
+
return new ImportAnalyzer(entryFile, options).analyze();
|
|
299
|
+
}
|
|
300
|
+
var ImportAnalyzer = class extends BaseAnalyzer {
|
|
301
|
+
cwd;
|
|
302
|
+
entryPath;
|
|
303
|
+
constructor(entryFile, options) {
|
|
304
|
+
super(entryFile, options);
|
|
305
|
+
this.cwd = path.resolve(options.cwd ?? process.cwd());
|
|
306
|
+
this.entryPath = resolveExistingPath(this.cwd, entryFile);
|
|
307
|
+
}
|
|
308
|
+
doAnalyze() {
|
|
309
|
+
const { compilerOptions, path: configPath } = loadCompilerOptions(path.dirname(this.entryPath), this.options.configPath);
|
|
310
|
+
const nodes = buildDependencyGraph(this.entryPath, compilerOptions, this.cwd);
|
|
311
|
+
return {
|
|
312
|
+
cwd: this.cwd,
|
|
313
|
+
entryId: this.entryPath,
|
|
314
|
+
nodes,
|
|
315
|
+
...configPath === void 0 ? {} : { configPath }
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
//#endregion
|
|
320
|
+
//#region src/analyzers/react/bindings.ts
|
|
321
|
+
function collectImportsAndExports(statement, sourceDependencies, symbolsByName, importsByLocalName, exportsByName) {
|
|
322
|
+
switch (statement.type) {
|
|
323
|
+
case "ImportDeclaration":
|
|
324
|
+
collectImportBindings(statement, sourceDependencies, importsByLocalName);
|
|
325
|
+
return;
|
|
326
|
+
case "ExportNamedDeclaration":
|
|
327
|
+
collectNamedExports(statement, symbolsByName, exportsByName);
|
|
328
|
+
return;
|
|
329
|
+
case "ExportDefaultDeclaration":
|
|
330
|
+
collectDefaultExport(statement, symbolsByName, exportsByName);
|
|
331
|
+
return;
|
|
332
|
+
default: return;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
function collectImportBindings(declaration, sourceDependencies, importsByLocalName) {
|
|
336
|
+
if (declaration.importKind === "type") return;
|
|
337
|
+
const sourceSpecifier = declaration.source.value;
|
|
338
|
+
const sourcePath = sourceDependencies.get(declaration.source.value);
|
|
339
|
+
declaration.specifiers.forEach((specifier) => {
|
|
340
|
+
const binding = getImportBinding(specifier, sourceSpecifier, sourcePath);
|
|
341
|
+
if (binding === void 0) return;
|
|
342
|
+
importsByLocalName.set(binding.localName, {
|
|
343
|
+
importedName: binding.importedName,
|
|
344
|
+
sourceSpecifier: binding.sourceSpecifier,
|
|
345
|
+
...binding.sourcePath === void 0 ? {} : { sourcePath: binding.sourcePath }
|
|
424
346
|
});
|
|
425
347
|
});
|
|
426
|
-
const roots = filteredNodes.filter((node) => (inboundCounts.get(node.id) ?? 0) === 0).map((node) => node.id);
|
|
427
|
-
if (roots.length > 0) return roots.sort((left, right) => compareReactNodeIds(left, right, graph.nodes));
|
|
428
|
-
return filteredNodes.map((node) => node.id).sort((left, right) => compareReactNodeIds(left, right, graph.nodes));
|
|
429
348
|
}
|
|
430
|
-
function
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
return
|
|
349
|
+
function getImportBinding(specifier, sourceSpecifier, sourcePath) {
|
|
350
|
+
if (specifier.type === "ImportSpecifier") {
|
|
351
|
+
if (specifier.importKind === "type") return;
|
|
352
|
+
return {
|
|
353
|
+
localName: specifier.local.name,
|
|
354
|
+
importedName: toModuleExportName(specifier.imported),
|
|
355
|
+
sourceSpecifier,
|
|
356
|
+
...sourcePath === void 0 ? {} : { sourcePath }
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
if (specifier.type === "ImportDefaultSpecifier") return {
|
|
360
|
+
localName: specifier.local.name,
|
|
361
|
+
importedName: "default",
|
|
362
|
+
sourceSpecifier,
|
|
363
|
+
...sourcePath === void 0 ? {} : { sourcePath }
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
function collectNamedExports(declaration, symbolsByName, exportsByName) {
|
|
367
|
+
if (declaration.exportKind === "type") return;
|
|
368
|
+
if (declaration.declaration !== null) {
|
|
369
|
+
if (declaration.declaration.type === "FunctionDeclaration") {
|
|
370
|
+
const name = declaration.declaration.id?.name;
|
|
371
|
+
if (name !== void 0) addExportBinding(name, name, symbolsByName, exportsByName);
|
|
372
|
+
} else if (declaration.declaration.type === "VariableDeclaration") declaration.declaration.declarations.forEach((declarator) => {
|
|
373
|
+
if (declarator.id.type === "Identifier") addExportBinding(declarator.id.name, declarator.id.name, symbolsByName, exportsByName);
|
|
374
|
+
});
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (declaration.source !== null) return;
|
|
378
|
+
declaration.specifiers.forEach((specifier) => {
|
|
379
|
+
if (specifier.exportKind === "type") return;
|
|
380
|
+
addExportBinding(toModuleExportName(specifier.local), toModuleExportName(specifier.exported), symbolsByName, exportsByName);
|
|
434
381
|
});
|
|
435
382
|
}
|
|
436
|
-
function
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
383
|
+
function collectDefaultExport(declaration, symbolsByName, exportsByName) {
|
|
384
|
+
if (declaration.declaration.type === "FunctionDeclaration" || declaration.declaration.type === "FunctionExpression") {
|
|
385
|
+
const localName = declaration.declaration.id?.name;
|
|
386
|
+
if (localName !== void 0) addExportBinding(localName, "default", symbolsByName, exportsByName);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (declaration.declaration.type === "Identifier") {
|
|
390
|
+
addExportBinding(declaration.declaration.name, "default", symbolsByName, exportsByName);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (declaration.declaration.type === "ArrowFunctionExpression") addExportBinding("default", "default", symbolsByName, exportsByName);
|
|
394
|
+
}
|
|
395
|
+
function addExportBinding(localName, exportedName, symbolsByName, exportsByName) {
|
|
396
|
+
const symbol = symbolsByName.get(localName);
|
|
397
|
+
if (symbol === void 0) return;
|
|
398
|
+
symbol.exportNames.add(exportedName);
|
|
399
|
+
exportsByName.set(exportedName, symbol.id);
|
|
400
|
+
}
|
|
401
|
+
function toModuleExportName(name) {
|
|
402
|
+
return name.type === "Literal" ? name.value : name.name;
|
|
403
|
+
}
|
|
404
|
+
//#endregion
|
|
405
|
+
//#region src/analyzers/react/walk.ts
|
|
406
|
+
const FUNCTION_NODE_TYPES = new Set([
|
|
407
|
+
"FunctionDeclaration",
|
|
408
|
+
"FunctionExpression",
|
|
409
|
+
"ArrowFunctionExpression",
|
|
410
|
+
"TSDeclareFunction",
|
|
411
|
+
"TSEmptyBodyFunctionExpression"
|
|
412
|
+
]);
|
|
413
|
+
function walkReactUsageTree(root, visit) {
|
|
414
|
+
walkNode(root, visit, true);
|
|
415
|
+
}
|
|
416
|
+
function walkNode(node, visit, allowNestedFunctions = false) {
|
|
417
|
+
visit(node);
|
|
418
|
+
const keys = visitorKeys[node.type];
|
|
419
|
+
if (keys === void 0) return;
|
|
420
|
+
keys.forEach((key) => {
|
|
421
|
+
const value = node[key];
|
|
422
|
+
walkChild(value, visit, allowNestedFunctions);
|
|
440
423
|
});
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
424
|
+
}
|
|
425
|
+
function walkChild(value, visit, allowNestedFunctions) {
|
|
426
|
+
if (Array.isArray(value)) {
|
|
427
|
+
value.forEach((entry) => {
|
|
428
|
+
walkChild(entry, visit, allowNestedFunctions);
|
|
429
|
+
});
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (!isNode(value)) return;
|
|
433
|
+
if (!allowNestedFunctions && FUNCTION_NODE_TYPES.has(value.type)) return;
|
|
434
|
+
walkNode(value, visit, false);
|
|
435
|
+
}
|
|
436
|
+
function isNode(value) {
|
|
437
|
+
return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
|
|
438
|
+
}
|
|
439
|
+
function classifyReactSymbol(name, declaration) {
|
|
440
|
+
if (isHookName(name)) return "hook";
|
|
441
|
+
if (isComponentName(name) && returnsReactElement(declaration)) return "component";
|
|
442
|
+
}
|
|
443
|
+
function containsReactElementLikeExpression(expression) {
|
|
444
|
+
let found = false;
|
|
445
|
+
walkNode(expression, (node) => {
|
|
446
|
+
if (node.type === "JSXElement" || node.type === "JSXFragment" || node.type === "CallExpression" && isReactCreateElementCall(node)) found = true;
|
|
446
447
|
});
|
|
447
|
-
|
|
448
|
-
|
|
448
|
+
return found;
|
|
449
|
+
}
|
|
450
|
+
function getComponentReferenceName(node) {
|
|
451
|
+
const name = getJsxName(node.openingElement.name);
|
|
452
|
+
return name !== void 0 && isComponentName(name) ? name : void 0;
|
|
453
|
+
}
|
|
454
|
+
function getHookReferenceName(node) {
|
|
455
|
+
const calleeName = getIdentifierName(node.callee);
|
|
456
|
+
return calleeName !== void 0 && isHookName(calleeName) ? calleeName : void 0;
|
|
457
|
+
}
|
|
458
|
+
function getCreateElementComponentReferenceName(node) {
|
|
459
|
+
if (!isReactCreateElementCall(node)) return;
|
|
460
|
+
const [firstArgument] = node.arguments;
|
|
461
|
+
if (firstArgument === void 0 || firstArgument.type !== "Identifier") return;
|
|
462
|
+
return isComponentName(firstArgument.name) ? firstArgument.name : void 0;
|
|
463
|
+
}
|
|
464
|
+
function isHookName(name) {
|
|
465
|
+
return /^use[A-Z0-9]/.test(name);
|
|
466
|
+
}
|
|
467
|
+
function isComponentName(name) {
|
|
468
|
+
return /^[A-Z]/.test(name);
|
|
469
|
+
}
|
|
470
|
+
function returnsReactElement(declaration) {
|
|
471
|
+
if (declaration.type === "ArrowFunctionExpression" && declaration.expression) return containsReactElementLikeExpression(declaration.body);
|
|
472
|
+
const body = declaration.body;
|
|
473
|
+
if (body === null) return false;
|
|
474
|
+
let found = false;
|
|
475
|
+
walkReactUsageTree(body, (node) => {
|
|
476
|
+
if (node.type !== "ReturnStatement" || node.argument === null) return;
|
|
477
|
+
if (containsReactElementLikeExpression(node.argument)) found = true;
|
|
449
478
|
});
|
|
450
|
-
return
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
479
|
+
return found;
|
|
480
|
+
}
|
|
481
|
+
function isReactCreateElementCall(node) {
|
|
482
|
+
const callee = unwrapExpression(node.callee);
|
|
483
|
+
if (callee.type !== "MemberExpression" || callee.computed) return false;
|
|
484
|
+
return callee.object.type === "Identifier" && callee.object.name === "React" && callee.property.name === "createElement";
|
|
485
|
+
}
|
|
486
|
+
function getJsxName(name) {
|
|
487
|
+
if (name.type === "JSXIdentifier") return name.name;
|
|
488
|
+
}
|
|
489
|
+
function getIdentifierName(expression) {
|
|
490
|
+
const unwrapped = unwrapExpression(expression);
|
|
491
|
+
return unwrapped.type === "Identifier" ? unwrapped.name : void 0;
|
|
492
|
+
}
|
|
493
|
+
function unwrapExpression(expression) {
|
|
494
|
+
let current = expression;
|
|
495
|
+
while (true) {
|
|
496
|
+
if (current.type === "ParenthesizedExpression" || current.type === "TSAsExpression" || current.type === "TSSatisfiesExpression" || current.type === "TSTypeAssertion" || current.type === "TSNonNullExpression") {
|
|
497
|
+
current = current.expression;
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
return current;
|
|
501
|
+
}
|
|
458
502
|
}
|
|
503
|
+
//#endregion
|
|
504
|
+
//#region src/analyzers/react/entries.ts
|
|
459
505
|
function collectEntryUsages(program, filePath, sourceText, includeNestedFunctions) {
|
|
460
506
|
const entries = /* @__PURE__ */ new Map();
|
|
461
507
|
program.body.forEach((statement) => {
|
|
@@ -531,6 +577,11 @@ function offsetToLineAndColumn(sourceText, offset) {
|
|
|
531
577
|
column
|
|
532
578
|
};
|
|
533
579
|
}
|
|
580
|
+
function comparePendingReactUsageEntries(left, right) {
|
|
581
|
+
return left.location.filePath.localeCompare(right.location.filePath) || left.location.line - right.location.line || left.location.column - right.location.column || left.kind.localeCompare(right.kind) || left.referenceName.localeCompare(right.referenceName);
|
|
582
|
+
}
|
|
583
|
+
//#endregion
|
|
584
|
+
//#region src/analyzers/react/symbols.ts
|
|
534
585
|
function collectTopLevelReactSymbols(statement, filePath, symbolsByName) {
|
|
535
586
|
switch (statement.type) {
|
|
536
587
|
case "FunctionDeclaration":
|
|
@@ -585,86 +636,8 @@ function createPendingSymbol(filePath, name, kind, declaration) {
|
|
|
585
636
|
hookReferences: /* @__PURE__ */ new Set()
|
|
586
637
|
};
|
|
587
638
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
case "ImportDeclaration":
|
|
591
|
-
collectImportBindings(statement, sourceDependencies, importsByLocalName);
|
|
592
|
-
return;
|
|
593
|
-
case "ExportNamedDeclaration":
|
|
594
|
-
collectNamedExports(statement, symbolsByName, exportsByName);
|
|
595
|
-
return;
|
|
596
|
-
case "ExportDefaultDeclaration":
|
|
597
|
-
collectDefaultExport(statement, symbolsByName, exportsByName);
|
|
598
|
-
return;
|
|
599
|
-
default: return;
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
function collectImportBindings(declaration, sourceDependencies, importsByLocalName) {
|
|
603
|
-
if (declaration.importKind === "type") return;
|
|
604
|
-
const sourceSpecifier = declaration.source.value;
|
|
605
|
-
const sourcePath = sourceDependencies.get(declaration.source.value);
|
|
606
|
-
declaration.specifiers.forEach((specifier) => {
|
|
607
|
-
const binding = getImportBinding(specifier, sourceSpecifier, sourcePath);
|
|
608
|
-
if (binding === void 0) return;
|
|
609
|
-
importsByLocalName.set(binding.localName, {
|
|
610
|
-
importedName: binding.importedName,
|
|
611
|
-
sourceSpecifier: binding.sourceSpecifier,
|
|
612
|
-
...binding.sourcePath === void 0 ? {} : { sourcePath: binding.sourcePath }
|
|
613
|
-
});
|
|
614
|
-
});
|
|
615
|
-
}
|
|
616
|
-
function getImportBinding(specifier, sourceSpecifier, sourcePath) {
|
|
617
|
-
if (specifier.type === "ImportSpecifier") {
|
|
618
|
-
if (specifier.importKind === "type") return;
|
|
619
|
-
return {
|
|
620
|
-
localName: specifier.local.name,
|
|
621
|
-
importedName: toModuleExportName(specifier.imported),
|
|
622
|
-
sourceSpecifier,
|
|
623
|
-
...sourcePath === void 0 ? {} : { sourcePath }
|
|
624
|
-
};
|
|
625
|
-
}
|
|
626
|
-
if (specifier.type === "ImportDefaultSpecifier") return {
|
|
627
|
-
localName: specifier.local.name,
|
|
628
|
-
importedName: "default",
|
|
629
|
-
sourceSpecifier,
|
|
630
|
-
...sourcePath === void 0 ? {} : { sourcePath }
|
|
631
|
-
};
|
|
632
|
-
}
|
|
633
|
-
function collectNamedExports(declaration, symbolsByName, exportsByName) {
|
|
634
|
-
if (declaration.exportKind === "type") return;
|
|
635
|
-
if (declaration.declaration !== null) {
|
|
636
|
-
if (declaration.declaration.type === "FunctionDeclaration") {
|
|
637
|
-
const name = declaration.declaration.id?.name;
|
|
638
|
-
if (name !== void 0) addExportBinding(name, name, symbolsByName, exportsByName);
|
|
639
|
-
} else if (declaration.declaration.type === "VariableDeclaration") declaration.declaration.declarations.forEach((declarator) => {
|
|
640
|
-
if (declarator.id.type === "Identifier") addExportBinding(declarator.id.name, declarator.id.name, symbolsByName, exportsByName);
|
|
641
|
-
});
|
|
642
|
-
return;
|
|
643
|
-
}
|
|
644
|
-
if (declaration.source !== null) return;
|
|
645
|
-
declaration.specifiers.forEach((specifier) => {
|
|
646
|
-
if (specifier.exportKind === "type") return;
|
|
647
|
-
addExportBinding(toModuleExportName(specifier.local), toModuleExportName(specifier.exported), symbolsByName, exportsByName);
|
|
648
|
-
});
|
|
649
|
-
}
|
|
650
|
-
function collectDefaultExport(declaration, symbolsByName, exportsByName) {
|
|
651
|
-
if (declaration.declaration.type === "FunctionDeclaration" || declaration.declaration.type === "FunctionExpression") {
|
|
652
|
-
const localName = declaration.declaration.id?.name;
|
|
653
|
-
if (localName !== void 0) addExportBinding(localName, "default", symbolsByName, exportsByName);
|
|
654
|
-
return;
|
|
655
|
-
}
|
|
656
|
-
if (declaration.declaration.type === "Identifier") {
|
|
657
|
-
addExportBinding(declaration.declaration.name, "default", symbolsByName, exportsByName);
|
|
658
|
-
return;
|
|
659
|
-
}
|
|
660
|
-
if (declaration.declaration.type === "ArrowFunctionExpression") addExportBinding("default", "default", symbolsByName, exportsByName);
|
|
661
|
-
}
|
|
662
|
-
function addExportBinding(localName, exportedName, symbolsByName, exportsByName) {
|
|
663
|
-
const symbol = symbolsByName.get(localName);
|
|
664
|
-
if (symbol === void 0) return;
|
|
665
|
-
symbol.exportNames.add(exportedName);
|
|
666
|
-
exportsByName.set(exportedName, symbol.id);
|
|
667
|
-
}
|
|
639
|
+
//#endregion
|
|
640
|
+
//#region src/analyzers/react/usage.ts
|
|
668
641
|
function analyzeSymbolUsages(symbol) {
|
|
669
642
|
const root = symbol.declaration.type === "ArrowFunctionExpression" ? symbol.declaration.body : symbol.declaration.body;
|
|
670
643
|
if (root === null) return;
|
|
@@ -682,90 +655,33 @@ function analyzeSymbolUsages(symbol) {
|
|
|
682
655
|
}
|
|
683
656
|
});
|
|
684
657
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
const body = declaration.body;
|
|
692
|
-
if (body === null) return false;
|
|
693
|
-
let found = false;
|
|
694
|
-
walkReactUsageTree(body, (node) => {
|
|
695
|
-
if (node.type !== "ReturnStatement" || node.argument === null) return;
|
|
696
|
-
if (containsReactElementLikeExpression(node.argument)) found = true;
|
|
658
|
+
//#endregion
|
|
659
|
+
//#region src/analyzers/react/file.ts
|
|
660
|
+
function analyzeReactFile(program, filePath, sourceText, includeNestedRenderEntries, sourceDependencies) {
|
|
661
|
+
const symbolsByName = /* @__PURE__ */ new Map();
|
|
662
|
+
program.body.forEach((statement) => {
|
|
663
|
+
collectTopLevelReactSymbols(statement, filePath, symbolsByName);
|
|
697
664
|
});
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
if (node.type === "JSXElement" || node.type === "JSXFragment" || node.type === "CallExpression" && isReactCreateElementCall(node)) found = true;
|
|
665
|
+
const importsByLocalName = /* @__PURE__ */ new Map();
|
|
666
|
+
const exportsByName = /* @__PURE__ */ new Map();
|
|
667
|
+
const entryUsages = collectEntryUsages(program, filePath, sourceText, includeNestedRenderEntries);
|
|
668
|
+
program.body.forEach((statement) => {
|
|
669
|
+
collectImportsAndExports(statement, sourceDependencies, symbolsByName, importsByLocalName, exportsByName);
|
|
704
670
|
});
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
function getComponentReferenceName(node) {
|
|
708
|
-
const name = getJsxName(node.openingElement.name);
|
|
709
|
-
return name !== void 0 && isComponentName(name) ? name : void 0;
|
|
710
|
-
}
|
|
711
|
-
function getHookReferenceName(node) {
|
|
712
|
-
const calleeName = getIdentifierName(node.callee);
|
|
713
|
-
return calleeName !== void 0 && isHookName(calleeName) ? calleeName : void 0;
|
|
714
|
-
}
|
|
715
|
-
function getCreateElementComponentReferenceName(node) {
|
|
716
|
-
if (!isReactCreateElementCall(node)) return;
|
|
717
|
-
const [firstArgument] = node.arguments;
|
|
718
|
-
if (firstArgument === void 0 || firstArgument.type !== "Identifier") return;
|
|
719
|
-
return isComponentName(firstArgument.name) ? firstArgument.name : void 0;
|
|
720
|
-
}
|
|
721
|
-
function isReactCreateElementCall(node) {
|
|
722
|
-
const callee = unwrapExpression(node.callee);
|
|
723
|
-
if (callee.type !== "MemberExpression" || callee.computed) return false;
|
|
724
|
-
return callee.object.type === "Identifier" && callee.object.name === "React" && callee.property.name === "createElement";
|
|
725
|
-
}
|
|
726
|
-
function getJsxName(name) {
|
|
727
|
-
if (name.type === "JSXIdentifier") return name.name;
|
|
728
|
-
}
|
|
729
|
-
function getIdentifierName(expression) {
|
|
730
|
-
const unwrapped = unwrapExpression(expression);
|
|
731
|
-
return unwrapped.type === "Identifier" ? unwrapped.name : void 0;
|
|
732
|
-
}
|
|
733
|
-
function unwrapExpression(expression) {
|
|
734
|
-
let current = expression;
|
|
735
|
-
while (true) {
|
|
736
|
-
if (current.type === "ParenthesizedExpression" || current.type === "TSAsExpression" || current.type === "TSSatisfiesExpression" || current.type === "TSTypeAssertion" || current.type === "TSNonNullExpression") {
|
|
737
|
-
current = current.expression;
|
|
738
|
-
continue;
|
|
739
|
-
}
|
|
740
|
-
return current;
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
function walkReactUsageTree(root, visit) {
|
|
744
|
-
walkNode(root, visit, true);
|
|
745
|
-
}
|
|
746
|
-
function walkNode(node, visit, allowNestedFunctions = false) {
|
|
747
|
-
visit(node);
|
|
748
|
-
const keys = visitorKeys[node.type];
|
|
749
|
-
if (keys === void 0) return;
|
|
750
|
-
keys.forEach((key) => {
|
|
751
|
-
const value = node[key];
|
|
752
|
-
walkChild(value, visit, allowNestedFunctions);
|
|
671
|
+
symbolsByName.forEach((symbol) => {
|
|
672
|
+
analyzeSymbolUsages(symbol);
|
|
753
673
|
});
|
|
674
|
+
return {
|
|
675
|
+
filePath,
|
|
676
|
+
importsByLocalName,
|
|
677
|
+
exportsByName,
|
|
678
|
+
entryUsages,
|
|
679
|
+
symbolsById: new Map([...symbolsByName.values()].map((symbol) => [symbol.id, symbol])),
|
|
680
|
+
symbolsByName
|
|
681
|
+
};
|
|
754
682
|
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
value.forEach((entry) => {
|
|
758
|
-
walkChild(entry, visit, allowNestedFunctions);
|
|
759
|
-
});
|
|
760
|
-
return;
|
|
761
|
-
}
|
|
762
|
-
if (!isNode(value)) return;
|
|
763
|
-
if (!allowNestedFunctions && FUNCTION_NODE_TYPES.has(value.type)) return;
|
|
764
|
-
walkNode(value, visit, false);
|
|
765
|
-
}
|
|
766
|
-
function isNode(value) {
|
|
767
|
-
return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
|
|
768
|
-
}
|
|
683
|
+
//#endregion
|
|
684
|
+
//#region src/analyzers/react/references.ts
|
|
769
685
|
function resolveReactReference(fileAnalysis, fileAnalyses, name, kind) {
|
|
770
686
|
const localSymbol = fileAnalysis.symbolsByName.get(name);
|
|
771
687
|
if (localSymbol !== void 0 && localSymbol.kind === kind) return localSymbol.id;
|
|
@@ -778,6 +694,14 @@ function resolveReactReference(fileAnalysis, fileAnalyses, name, kind) {
|
|
|
778
694
|
if (targetId === void 0) return;
|
|
779
695
|
return sourceFileAnalysis.symbolsById.get(targetId)?.kind === kind ? targetId : void 0;
|
|
780
696
|
}
|
|
697
|
+
function addExternalHookNodes(fileAnalyses, nodes) {
|
|
698
|
+
for (const fileAnalysis of fileAnalyses.values()) fileAnalysis.importsByLocalName.forEach((binding, localName) => {
|
|
699
|
+
if (binding.sourcePath !== void 0) return;
|
|
700
|
+
if (!isHookName(localName) && !isHookName(binding.importedName)) return;
|
|
701
|
+
const externalNode = createExternalHookNode(binding, localName);
|
|
702
|
+
if (!nodes.has(externalNode.id)) nodes.set(externalNode.id, externalNode);
|
|
703
|
+
});
|
|
704
|
+
}
|
|
781
705
|
function createExternalHookNode(binding, localName) {
|
|
782
706
|
const name = getExternalHookName(binding, localName);
|
|
783
707
|
return {
|
|
@@ -795,54 +719,6 @@ function getExternalHookNodeId(binding, localName) {
|
|
|
795
719
|
function getExternalHookName(binding, localName) {
|
|
796
720
|
return binding.importedName === "default" ? localName : binding.importedName;
|
|
797
721
|
}
|
|
798
|
-
function getFilteredReactUsageNodes(graph, filter) {
|
|
799
|
-
return [...graph.nodes.values()].filter((node) => matchesReactFilter(node, filter)).sort((left, right) => compareReactNodes(left, right));
|
|
800
|
-
}
|
|
801
|
-
function matchesReactFilter(node, filter) {
|
|
802
|
-
return filter === "all" || node.kind === filter;
|
|
803
|
-
}
|
|
804
|
-
function serializeReactUsageNode(nodeId, graph, filter, visited) {
|
|
805
|
-
const node = graph.nodes.get(nodeId);
|
|
806
|
-
if (node === void 0) return {
|
|
807
|
-
id: nodeId,
|
|
808
|
-
name: nodeId,
|
|
809
|
-
symbolKind: "circular",
|
|
810
|
-
filePath: "",
|
|
811
|
-
exportNames: [],
|
|
812
|
-
usages: []
|
|
813
|
-
};
|
|
814
|
-
if (visited.has(nodeId)) return {
|
|
815
|
-
id: node.id,
|
|
816
|
-
name: node.name,
|
|
817
|
-
symbolKind: "circular",
|
|
818
|
-
filePath: toDisplayPath(node.filePath, graph.cwd),
|
|
819
|
-
exportNames: node.exportNames,
|
|
820
|
-
usages: []
|
|
821
|
-
};
|
|
822
|
-
const nextVisited = new Set(visited);
|
|
823
|
-
nextVisited.add(nodeId);
|
|
824
|
-
return {
|
|
825
|
-
id: node.id,
|
|
826
|
-
name: node.name,
|
|
827
|
-
symbolKind: node.kind,
|
|
828
|
-
filePath: toDisplayPath(node.filePath, graph.cwd),
|
|
829
|
-
exportNames: node.exportNames,
|
|
830
|
-
usages: getFilteredUsages(node, graph, filter).map((usage) => ({
|
|
831
|
-
kind: usage.kind,
|
|
832
|
-
targetId: usage.target,
|
|
833
|
-
node: serializeReactUsageNode(usage.target, graph, filter, nextVisited)
|
|
834
|
-
}))
|
|
835
|
-
};
|
|
836
|
-
}
|
|
837
|
-
function serializeReactUsageEntry(entry, graph, filter) {
|
|
838
|
-
return {
|
|
839
|
-
targetId: entry.target,
|
|
840
|
-
filePath: toDisplayPath(entry.location.filePath, graph.cwd),
|
|
841
|
-
line: entry.location.line,
|
|
842
|
-
column: entry.location.column,
|
|
843
|
-
node: serializeReactUsageNode(entry.target, graph, filter, /* @__PURE__ */ new Set())
|
|
844
|
-
};
|
|
845
|
-
}
|
|
846
722
|
function compareReactNodeIds(leftId, rightId, nodes) {
|
|
847
723
|
const left = nodes.get(leftId);
|
|
848
724
|
const right = nodes.get(rightId);
|
|
@@ -852,20 +728,138 @@ function compareReactNodeIds(leftId, rightId, nodes) {
|
|
|
852
728
|
function compareReactUsageEntries(left, right, nodes) {
|
|
853
729
|
return left.location.filePath.localeCompare(right.location.filePath) || left.location.line - right.location.line || left.location.column - right.location.column || compareReactNodeIds(left.target, right.target, nodes);
|
|
854
730
|
}
|
|
855
|
-
function comparePendingReactUsageEntries(left, right) {
|
|
856
|
-
return left.location.filePath.localeCompare(right.location.filePath) || left.location.line - right.location.line || left.location.column - right.location.column || left.kind.localeCompare(right.kind) || left.referenceName.localeCompare(right.referenceName);
|
|
857
|
-
}
|
|
858
731
|
function compareReactNodes(left, right) {
|
|
859
732
|
return left.filePath.localeCompare(right.filePath) || left.name.localeCompare(right.name) || left.kind.localeCompare(right.kind);
|
|
860
733
|
}
|
|
861
|
-
|
|
862
|
-
|
|
734
|
+
//#endregion
|
|
735
|
+
//#region src/analyzers/react/index.ts
|
|
736
|
+
function analyzeReactUsage(entryFile, options = {}) {
|
|
737
|
+
return new ReactAnalyzer(entryFile, options).analyze();
|
|
738
|
+
}
|
|
739
|
+
var ReactAnalyzer = class extends BaseAnalyzer {
|
|
740
|
+
doAnalyze() {
|
|
741
|
+
const dependencyGraph = analyzeDependencies(this.entryFile, this.options);
|
|
742
|
+
const fileAnalyses = this.collectFileAnalyses(dependencyGraph);
|
|
743
|
+
const nodes = this.createNodes(fileAnalyses);
|
|
744
|
+
this.attachUsages(fileAnalyses, nodes);
|
|
745
|
+
const entries = this.collectEntries(fileAnalyses, nodes);
|
|
746
|
+
return {
|
|
747
|
+
cwd: dependencyGraph.cwd,
|
|
748
|
+
entryId: dependencyGraph.entryId,
|
|
749
|
+
nodes,
|
|
750
|
+
entries
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
collectFileAnalyses(dependencyGraph) {
|
|
754
|
+
const reachableFiles = new Set([dependencyGraph.entryId, ...dependencyGraph.nodes.keys()]);
|
|
755
|
+
const fileAnalyses = /* @__PURE__ */ new Map();
|
|
756
|
+
for (const filePath of [...reachableFiles].sort()) {
|
|
757
|
+
if (!isSourceCodeFile(filePath) || filePath.endsWith(".d.ts")) continue;
|
|
758
|
+
const sourceText = fs.readFileSync(filePath, "utf8");
|
|
759
|
+
const parseResult = parseSync(filePath, sourceText, {
|
|
760
|
+
astType: "ts",
|
|
761
|
+
sourceType: "unambiguous"
|
|
762
|
+
});
|
|
763
|
+
const dependencyNode = dependencyGraph.nodes.get(filePath);
|
|
764
|
+
const sourceDependencies = /* @__PURE__ */ new Map();
|
|
765
|
+
dependencyNode?.dependencies.forEach((dependency) => {
|
|
766
|
+
if (dependency.kind === "source") sourceDependencies.set(dependency.specifier, dependency.target);
|
|
767
|
+
});
|
|
768
|
+
fileAnalyses.set(filePath, analyzeReactFile(parseResult.program, filePath, sourceText, filePath === dependencyGraph.entryId, sourceDependencies));
|
|
769
|
+
}
|
|
770
|
+
return fileAnalyses;
|
|
771
|
+
}
|
|
772
|
+
createNodes(fileAnalyses) {
|
|
773
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
774
|
+
for (const fileAnalysis of fileAnalyses.values()) for (const symbol of fileAnalysis.symbolsById.values()) nodes.set(symbol.id, {
|
|
775
|
+
id: symbol.id,
|
|
776
|
+
name: symbol.name,
|
|
777
|
+
kind: symbol.kind,
|
|
778
|
+
filePath: symbol.filePath,
|
|
779
|
+
exportNames: [...symbol.exportNames].sort(),
|
|
780
|
+
usages: []
|
|
781
|
+
});
|
|
782
|
+
addExternalHookNodes(fileAnalyses, nodes);
|
|
783
|
+
return nodes;
|
|
784
|
+
}
|
|
785
|
+
attachUsages(fileAnalyses, nodes) {
|
|
786
|
+
for (const fileAnalysis of fileAnalyses.values()) for (const symbol of fileAnalysis.symbolsById.values()) {
|
|
787
|
+
const usages = /* @__PURE__ */ new Map();
|
|
788
|
+
symbol.componentReferences.forEach((referenceName) => {
|
|
789
|
+
const targetId = resolveReactReference(fileAnalysis, fileAnalyses, referenceName, "component");
|
|
790
|
+
if (targetId !== void 0 && targetId !== symbol.id) usages.set(`render:${targetId}`, {
|
|
791
|
+
kind: "render",
|
|
792
|
+
target: targetId
|
|
793
|
+
});
|
|
794
|
+
});
|
|
795
|
+
symbol.hookReferences.forEach((referenceName) => {
|
|
796
|
+
const targetId = resolveReactReference(fileAnalysis, fileAnalyses, referenceName, "hook");
|
|
797
|
+
if (targetId !== void 0 && targetId !== symbol.id) usages.set(`hook:${targetId}`, {
|
|
798
|
+
kind: "hook-call",
|
|
799
|
+
target: targetId
|
|
800
|
+
});
|
|
801
|
+
});
|
|
802
|
+
const node = nodes.get(symbol.id);
|
|
803
|
+
if (node === void 0) continue;
|
|
804
|
+
const sortedUsages = [...usages.values()].sort((left, right) => compareReactNodeIds(left.target, right.target, nodes));
|
|
805
|
+
nodes.set(symbol.id, {
|
|
806
|
+
...node,
|
|
807
|
+
usages: sortedUsages
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
collectEntries(fileAnalyses, nodes) {
|
|
812
|
+
const entriesByKey = /* @__PURE__ */ new Map();
|
|
813
|
+
for (const fileAnalysis of fileAnalyses.values()) for (const entry of fileAnalysis.entryUsages) {
|
|
814
|
+
const targetId = resolveReactReference(fileAnalysis, fileAnalyses, entry.referenceName, entry.kind);
|
|
815
|
+
if (targetId === void 0) continue;
|
|
816
|
+
const key = `${entry.location.filePath}:${entry.location.line}:${entry.location.column}:${targetId}`;
|
|
817
|
+
entriesByKey.set(key, {
|
|
818
|
+
target: targetId,
|
|
819
|
+
location: entry.location
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
return [...entriesByKey.values()].sort((left, right) => compareReactUsageEntries(left, right, nodes));
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
//#endregion
|
|
826
|
+
//#region src/analyzers/react/queries.ts
|
|
827
|
+
function getReactUsageEntries(graph, filter = "all") {
|
|
828
|
+
return graph.entries.filter((entry) => {
|
|
829
|
+
const targetNode = graph.nodes.get(entry.target);
|
|
830
|
+
return targetNode !== void 0 && matchesReactFilter(targetNode, filter);
|
|
831
|
+
});
|
|
863
832
|
}
|
|
864
|
-
function
|
|
865
|
-
|
|
833
|
+
function getReactUsageRoots(graph, filter = "all") {
|
|
834
|
+
const entries = getReactUsageEntries(graph, filter);
|
|
835
|
+
if (entries.length > 0) return [...new Set(entries.map((entry) => entry.target))];
|
|
836
|
+
const filteredNodes = getFilteredReactUsageNodes(graph, filter);
|
|
837
|
+
const inboundCounts = /* @__PURE__ */ new Map();
|
|
838
|
+
filteredNodes.forEach((node) => {
|
|
839
|
+
inboundCounts.set(node.id, 0);
|
|
840
|
+
});
|
|
841
|
+
filteredNodes.forEach((node) => {
|
|
842
|
+
getFilteredUsages(node, graph, filter).forEach((usage) => {
|
|
843
|
+
inboundCounts.set(usage.target, (inboundCounts.get(usage.target) ?? 0) + 1);
|
|
844
|
+
});
|
|
845
|
+
});
|
|
846
|
+
const roots = filteredNodes.filter((node) => (inboundCounts.get(node.id) ?? 0) === 0).map((node) => node.id);
|
|
847
|
+
if (roots.length > 0) return roots.sort((left, right) => compareReactNodeIds(left, right, graph.nodes));
|
|
848
|
+
return filteredNodes.map((node) => node.id).sort((left, right) => compareReactNodeIds(left, right, graph.nodes));
|
|
866
849
|
}
|
|
867
|
-
function
|
|
868
|
-
return
|
|
850
|
+
function getFilteredUsages(node, graph, filter = "all") {
|
|
851
|
+
return node.usages.filter((usage) => {
|
|
852
|
+
const targetNode = graph.nodes.get(usage.target);
|
|
853
|
+
return targetNode !== void 0 && matchesReactFilter(targetNode, filter);
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
function getFilteredReactUsageNodes(graph, filter) {
|
|
857
|
+
return [...graph.nodes.values()].filter((node) => matchesReactFilter(node, filter)).sort((left, right) => {
|
|
858
|
+
return left.filePath.localeCompare(right.filePath) || left.name.localeCompare(right.name) || left.kind.localeCompare(right.kind);
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
function matchesReactFilter(node, filter) {
|
|
862
|
+
return filter === "all" || node.kind === filter;
|
|
869
863
|
}
|
|
870
864
|
//#endregion
|
|
871
865
|
//#region src/color.ts
|
|
@@ -891,7 +885,72 @@ function formatReactSymbolLabel(name, kind, enabled) {
|
|
|
891
885
|
return `${kind === "component" ? ANSI_COMPONENT : ANSI_HOOK}${label}${ANSI_RESET}`;
|
|
892
886
|
}
|
|
893
887
|
//#endregion
|
|
894
|
-
//#region src/
|
|
888
|
+
//#region src/utils/to-display-path.ts
|
|
889
|
+
function toDisplayPath(filePath, cwd) {
|
|
890
|
+
const relativePath = path.relative(cwd, filePath);
|
|
891
|
+
if (relativePath === "") return ".";
|
|
892
|
+
const normalizedPath = relativePath.split(path.sep).join("/");
|
|
893
|
+
return normalizedPath.startsWith("..") ? filePath : normalizedPath;
|
|
894
|
+
}
|
|
895
|
+
//#endregion
|
|
896
|
+
//#region src/output/ascii/import.ts
|
|
897
|
+
function printDependencyTree(graph, options = {}) {
|
|
898
|
+
const cwd = options.cwd ?? graph.cwd;
|
|
899
|
+
const color = resolveColorSupport(options.color);
|
|
900
|
+
const includeExternals = options.includeExternals ?? false;
|
|
901
|
+
const omitUnused = options.omitUnused ?? false;
|
|
902
|
+
const rootLines = [toDisplayPath(graph.entryId, cwd)];
|
|
903
|
+
const visited = new Set([graph.entryId]);
|
|
904
|
+
const entryNode = graph.nodes.get(graph.entryId);
|
|
905
|
+
if (entryNode === void 0) return rootLines.join("\n");
|
|
906
|
+
const rootDependencies = filterDependencies(entryNode.dependencies, includeExternals, omitUnused);
|
|
907
|
+
rootDependencies.forEach((dependency, index) => {
|
|
908
|
+
rootLines.push(...renderDependency(dependency, graph, visited, "", index === rootDependencies.length - 1, includeExternals, omitUnused, color, cwd));
|
|
909
|
+
});
|
|
910
|
+
return rootLines.join("\n");
|
|
911
|
+
}
|
|
912
|
+
function renderDependency(dependency, graph, visited, prefix, isLast, includeExternals, omitUnused, color, cwd) {
|
|
913
|
+
const branch = `${prefix}${isLast ? "└─ " : "├─ "}`;
|
|
914
|
+
const label = formatDependencyLabel(dependency, cwd, color);
|
|
915
|
+
if (dependency.kind !== "source") return [`${branch}${label}`];
|
|
916
|
+
if (visited.has(dependency.target)) return [`${branch}${label} (circular)`];
|
|
917
|
+
const childNode = graph.nodes.get(dependency.target);
|
|
918
|
+
if (childNode === void 0) return [`${branch}${label}`];
|
|
919
|
+
const nextPrefix = `${prefix}${isLast ? " " : "│ "}`;
|
|
920
|
+
const nextVisited = new Set(visited);
|
|
921
|
+
nextVisited.add(dependency.target);
|
|
922
|
+
const childLines = [`${branch}${label}`];
|
|
923
|
+
const childDependencies = filterDependencies(childNode.dependencies, includeExternals, omitUnused);
|
|
924
|
+
childDependencies.forEach((childDependency, index) => {
|
|
925
|
+
childLines.push(...renderDependency(childDependency, graph, nextVisited, nextPrefix, index === childDependencies.length - 1, includeExternals, omitUnused, color, cwd));
|
|
926
|
+
});
|
|
927
|
+
return childLines;
|
|
928
|
+
}
|
|
929
|
+
function filterDependencies(dependencies, includeExternals, omitUnused) {
|
|
930
|
+
return dependencies.filter((dependency) => {
|
|
931
|
+
if (omitUnused && dependency.unused) return false;
|
|
932
|
+
if (dependency.kind === "source" || dependency.kind === "missing") return true;
|
|
933
|
+
return includeExternals;
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
function formatDependencyLabel(dependency, cwd, color) {
|
|
937
|
+
const prefixes = [];
|
|
938
|
+
if (dependency.isTypeOnly) prefixes.push("type");
|
|
939
|
+
if (dependency.referenceKind === "require") prefixes.push("require");
|
|
940
|
+
else if (dependency.referenceKind === "dynamic-import") prefixes.push("dynamic");
|
|
941
|
+
else if (dependency.referenceKind === "export") prefixes.push("re-export");
|
|
942
|
+
else if (dependency.referenceKind === "import-equals") prefixes.push("import=");
|
|
943
|
+
const annotation = prefixes.length > 0 ? `[${prefixes.join(", ")}] ` : "";
|
|
944
|
+
if (dependency.kind === "source") return colorizeUnusedMarker(withUnusedSuffix(`${annotation}${toDisplayPath(dependency.target, cwd)}`, dependency.unused), color);
|
|
945
|
+
if (dependency.kind === "missing") return colorizeUnusedMarker(withUnusedSuffix(`${annotation}${dependency.specifier} [missing]`, dependency.unused), color);
|
|
946
|
+
if (dependency.kind === "builtin") return colorizeUnusedMarker(withUnusedSuffix(`${annotation}${dependency.target} [builtin]`, dependency.unused), color);
|
|
947
|
+
return colorizeUnusedMarker(withUnusedSuffix(`${annotation}${dependency.target} [external]`, dependency.unused), color);
|
|
948
|
+
}
|
|
949
|
+
function withUnusedSuffix(label, unused) {
|
|
950
|
+
return unused ? `${label} (unused)` : label;
|
|
951
|
+
}
|
|
952
|
+
//#endregion
|
|
953
|
+
//#region src/output/ascii/react.ts
|
|
895
954
|
function printReactUsageTree(graph, options = {}) {
|
|
896
955
|
const cwd = options.cwd ?? graph.cwd;
|
|
897
956
|
const color = resolveColorSupport(options.color);
|
|
@@ -950,65 +1009,105 @@ function formatReactEntryLabel(entry, cwd) {
|
|
|
950
1009
|
return `${toDisplayPath(entry.location.filePath, cwd)}:${entry.location.line}:${entry.location.column}`;
|
|
951
1010
|
}
|
|
952
1011
|
//#endregion
|
|
953
|
-
//#region src/
|
|
954
|
-
function
|
|
955
|
-
const
|
|
956
|
-
|
|
957
|
-
const includeExternals = options.includeExternals ?? false;
|
|
958
|
-
const omitUnused = options.omitUnused ?? false;
|
|
959
|
-
const rootLines = [toDisplayPath(graph.entryId, cwd)];
|
|
960
|
-
const visited = new Set([graph.entryId]);
|
|
961
|
-
const entryNode = graph.nodes.get(graph.entryId);
|
|
962
|
-
if (entryNode === void 0) return rootLines.join("\n");
|
|
963
|
-
const rootDependencies = filterDependencies(entryNode.dependencies, includeExternals, omitUnused);
|
|
964
|
-
rootDependencies.forEach((dependency, index) => {
|
|
965
|
-
const lines = renderDependency(dependency, graph, visited, "", index === rootDependencies.length - 1, includeExternals, omitUnused, color, cwd);
|
|
966
|
-
rootLines.push(...lines);
|
|
967
|
-
});
|
|
968
|
-
return rootLines.join("\n");
|
|
1012
|
+
//#region src/output/json/import.ts
|
|
1013
|
+
function graphToSerializableTree(graph, options = {}) {
|
|
1014
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1015
|
+
return serializeNode(graph.entryId, graph, visited, options.omitUnused ?? false);
|
|
969
1016
|
}
|
|
970
|
-
function
|
|
971
|
-
const
|
|
972
|
-
const
|
|
973
|
-
if (
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1017
|
+
function serializeNode(filePath, graph, visited, omitUnused) {
|
|
1018
|
+
const node = graph.nodes.get(filePath);
|
|
1019
|
+
const displayPath = toDisplayPath(filePath, graph.cwd);
|
|
1020
|
+
if (node === void 0) return {
|
|
1021
|
+
path: displayPath,
|
|
1022
|
+
kind: "missing",
|
|
1023
|
+
dependencies: []
|
|
1024
|
+
};
|
|
1025
|
+
if (visited.has(filePath)) return {
|
|
1026
|
+
path: displayPath,
|
|
1027
|
+
kind: "circular",
|
|
1028
|
+
dependencies: []
|
|
1029
|
+
};
|
|
1030
|
+
visited.add(filePath);
|
|
1031
|
+
const dependencies = node.dependencies.filter((dependency) => !omitUnused || !dependency.unused).map((dependency) => {
|
|
1032
|
+
if (dependency.kind !== "source") return {
|
|
1033
|
+
specifier: dependency.specifier,
|
|
1034
|
+
referenceKind: dependency.referenceKind,
|
|
1035
|
+
isTypeOnly: dependency.isTypeOnly,
|
|
1036
|
+
unused: dependency.unused,
|
|
1037
|
+
kind: dependency.kind,
|
|
1038
|
+
target: dependency.kind === "missing" ? dependency.target : toDisplayPath(dependency.target, graph.cwd)
|
|
1039
|
+
};
|
|
1040
|
+
return {
|
|
1041
|
+
specifier: dependency.specifier,
|
|
1042
|
+
referenceKind: dependency.referenceKind,
|
|
1043
|
+
isTypeOnly: dependency.isTypeOnly,
|
|
1044
|
+
unused: dependency.unused,
|
|
1045
|
+
kind: dependency.kind,
|
|
1046
|
+
target: toDisplayPath(dependency.target, graph.cwd),
|
|
1047
|
+
node: serializeNode(dependency.target, graph, new Set(visited), omitUnused)
|
|
1048
|
+
};
|
|
985
1049
|
});
|
|
986
|
-
return
|
|
1050
|
+
return {
|
|
1051
|
+
path: displayPath,
|
|
1052
|
+
kind: filePath === graph.entryId ? "entry" : "source",
|
|
1053
|
+
dependencies
|
|
1054
|
+
};
|
|
987
1055
|
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1056
|
+
//#endregion
|
|
1057
|
+
//#region src/output/json/react.ts
|
|
1058
|
+
function graphToSerializableReactTree(graph, options = {}) {
|
|
1059
|
+
const filter = options.filter ?? "all";
|
|
1060
|
+
const entries = getReactUsageEntries(graph, filter);
|
|
1061
|
+
const roots = entries.length > 0 ? entries.map((entry) => serializeReactUsageNode(entry.target, graph, filter, /* @__PURE__ */ new Set())) : getReactUsageRoots(graph, filter).map((rootId) => serializeReactUsageNode(rootId, graph, filter, /* @__PURE__ */ new Set()));
|
|
1062
|
+
return {
|
|
1063
|
+
kind: "react-usage",
|
|
1064
|
+
entries: entries.map((entry) => serializeReactUsageEntry(entry, graph, filter)),
|
|
1065
|
+
roots
|
|
1066
|
+
};
|
|
994
1067
|
}
|
|
995
|
-
function
|
|
996
|
-
const
|
|
997
|
-
if (
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
if (
|
|
1006
|
-
|
|
1068
|
+
function serializeReactUsageNode(nodeId, graph, filter, visited) {
|
|
1069
|
+
const node = graph.nodes.get(nodeId);
|
|
1070
|
+
if (node === void 0) return {
|
|
1071
|
+
id: nodeId,
|
|
1072
|
+
name: nodeId,
|
|
1073
|
+
symbolKind: "circular",
|
|
1074
|
+
filePath: "",
|
|
1075
|
+
exportNames: [],
|
|
1076
|
+
usages: []
|
|
1077
|
+
};
|
|
1078
|
+
if (visited.has(nodeId)) return {
|
|
1079
|
+
id: node.id,
|
|
1080
|
+
name: node.name,
|
|
1081
|
+
symbolKind: "circular",
|
|
1082
|
+
filePath: toDisplayPath(node.filePath, graph.cwd),
|
|
1083
|
+
exportNames: node.exportNames,
|
|
1084
|
+
usages: []
|
|
1085
|
+
};
|
|
1086
|
+
const nextVisited = new Set(visited);
|
|
1087
|
+
nextVisited.add(nodeId);
|
|
1088
|
+
return {
|
|
1089
|
+
id: node.id,
|
|
1090
|
+
name: node.name,
|
|
1091
|
+
symbolKind: node.kind,
|
|
1092
|
+
filePath: toDisplayPath(node.filePath, graph.cwd),
|
|
1093
|
+
exportNames: node.exportNames,
|
|
1094
|
+
usages: getFilteredUsages(node, graph, filter).map((usage) => ({
|
|
1095
|
+
kind: usage.kind,
|
|
1096
|
+
targetId: usage.target,
|
|
1097
|
+
node: serializeReactUsageNode(usage.target, graph, filter, nextVisited)
|
|
1098
|
+
}))
|
|
1099
|
+
};
|
|
1007
1100
|
}
|
|
1008
|
-
function
|
|
1009
|
-
return
|
|
1101
|
+
function serializeReactUsageEntry(entry, graph, filter) {
|
|
1102
|
+
return {
|
|
1103
|
+
targetId: entry.target,
|
|
1104
|
+
filePath: toDisplayPath(entry.location.filePath, graph.cwd),
|
|
1105
|
+
line: entry.location.line,
|
|
1106
|
+
column: entry.location.column,
|
|
1107
|
+
node: serializeReactUsageNode(entry.target, graph, filter, /* @__PURE__ */ new Set())
|
|
1108
|
+
};
|
|
1010
1109
|
}
|
|
1011
1110
|
//#endregion
|
|
1012
|
-
export {
|
|
1111
|
+
export { getFilteredUsages as a, analyzeReactUsage as c, printDependencyTree as i, analyzeDependencies as l, graphToSerializableTree as n, getReactUsageEntries as o, printReactUsageTree as r, getReactUsageRoots as s, graphToSerializableReactTree as t };
|
|
1013
1112
|
|
|
1014
|
-
//# sourceMappingURL=
|
|
1113
|
+
//# sourceMappingURL=react-CWIt8V0Y.mjs.map
|