foresthouse 1.0.0-dev.7 → 1.0.0-dev.9
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 +73 -31
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/{tree-BX1YFcqX.mjs → react-BHPy_fw5.mjs} +693 -577
- package/dist/react-BHPy_fw5.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);
|
|
@@ -850,28 +726,150 @@ function compareReactNodeIds(leftId, rightId, nodes) {
|
|
|
850
726
|
return compareReactNodes(left, right);
|
|
851
727
|
}
|
|
852
728
|
function compareReactUsageEntries(left, right, nodes) {
|
|
853
|
-
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
|
-
}
|
|
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);
|
|
729
|
+
return left.location.filePath.localeCompare(right.location.filePath) || left.location.line - right.location.line || left.location.column - right.location.column || left.referenceName.localeCompare(right.referenceName) || compareReactNodeIds(left.target, right.target, nodes);
|
|
857
730
|
}
|
|
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}:${referenceName}`, {
|
|
791
|
+
kind: "render",
|
|
792
|
+
target: targetId,
|
|
793
|
+
referenceName
|
|
794
|
+
});
|
|
795
|
+
});
|
|
796
|
+
symbol.hookReferences.forEach((referenceName) => {
|
|
797
|
+
const targetId = resolveReactReference(fileAnalysis, fileAnalyses, referenceName, "hook");
|
|
798
|
+
if (targetId !== void 0 && targetId !== symbol.id) usages.set(`hook:${targetId}:${referenceName}`, {
|
|
799
|
+
kind: "hook-call",
|
|
800
|
+
target: targetId,
|
|
801
|
+
referenceName
|
|
802
|
+
});
|
|
803
|
+
});
|
|
804
|
+
const node = nodes.get(symbol.id);
|
|
805
|
+
if (node === void 0) continue;
|
|
806
|
+
const sortedUsages = [...usages.values()].sort((left, right) => compareReactNodeIds(left.target, right.target, nodes));
|
|
807
|
+
nodes.set(symbol.id, {
|
|
808
|
+
...node,
|
|
809
|
+
usages: sortedUsages
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
collectEntries(fileAnalyses, nodes) {
|
|
814
|
+
const entriesByKey = /* @__PURE__ */ new Map();
|
|
815
|
+
for (const fileAnalysis of fileAnalyses.values()) for (const entry of fileAnalysis.entryUsages) {
|
|
816
|
+
const targetId = resolveReactReference(fileAnalysis, fileAnalyses, entry.referenceName, entry.kind);
|
|
817
|
+
if (targetId === void 0) continue;
|
|
818
|
+
const key = `${entry.location.filePath}:${entry.location.line}:${entry.location.column}:${targetId}`;
|
|
819
|
+
entriesByKey.set(key, {
|
|
820
|
+
target: targetId,
|
|
821
|
+
referenceName: entry.referenceName,
|
|
822
|
+
location: entry.location
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
return [...entriesByKey.values()].sort((left, right) => compareReactUsageEntries(left, right, nodes));
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
//#endregion
|
|
829
|
+
//#region src/analyzers/react/queries.ts
|
|
830
|
+
function getReactUsageEntries(graph, filter = "all") {
|
|
831
|
+
return graph.entries.filter((entry) => {
|
|
832
|
+
const targetNode = graph.nodes.get(entry.target);
|
|
833
|
+
return targetNode !== void 0 && matchesReactFilter(targetNode, filter);
|
|
834
|
+
});
|
|
863
835
|
}
|
|
864
|
-
function
|
|
865
|
-
|
|
836
|
+
function getReactUsageRoots(graph, filter = "all") {
|
|
837
|
+
const entries = getReactUsageEntries(graph, filter);
|
|
838
|
+
if (entries.length > 0) return [...new Set(entries.map((entry) => entry.target))];
|
|
839
|
+
const filteredNodes = getFilteredReactUsageNodes(graph, filter);
|
|
840
|
+
const inboundCounts = /* @__PURE__ */ new Map();
|
|
841
|
+
filteredNodes.forEach((node) => {
|
|
842
|
+
inboundCounts.set(node.id, 0);
|
|
843
|
+
});
|
|
844
|
+
filteredNodes.forEach((node) => {
|
|
845
|
+
getFilteredUsages(node, graph, filter).forEach((usage) => {
|
|
846
|
+
inboundCounts.set(usage.target, (inboundCounts.get(usage.target) ?? 0) + 1);
|
|
847
|
+
});
|
|
848
|
+
});
|
|
849
|
+
const roots = filteredNodes.filter((node) => (inboundCounts.get(node.id) ?? 0) === 0).map((node) => node.id);
|
|
850
|
+
if (roots.length > 0) return roots.sort((left, right) => compareReactNodeIds(left, right, graph.nodes));
|
|
851
|
+
return filteredNodes.map((node) => node.id).sort((left, right) => compareReactNodeIds(left, right, graph.nodes));
|
|
866
852
|
}
|
|
867
|
-
function
|
|
868
|
-
return
|
|
853
|
+
function getFilteredUsages(node, graph, filter = "all") {
|
|
854
|
+
return node.usages.filter((usage) => {
|
|
855
|
+
const targetNode = graph.nodes.get(usage.target);
|
|
856
|
+
return targetNode !== void 0 && matchesReactFilter(targetNode, filter);
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
function getFilteredReactUsageNodes(graph, filter) {
|
|
860
|
+
return [...graph.nodes.values()].filter((node) => matchesReactFilter(node, filter)).sort((left, right) => {
|
|
861
|
+
return left.filePath.localeCompare(right.filePath) || left.name.localeCompare(right.name) || left.kind.localeCompare(right.kind);
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
function matchesReactFilter(node, filter) {
|
|
865
|
+
return filter === "all" || node.kind === filter;
|
|
869
866
|
}
|
|
870
867
|
//#endregion
|
|
871
868
|
//#region src/color.ts
|
|
872
869
|
const ANSI_RESET = "\x1B[0m";
|
|
873
870
|
const ANSI_COMPONENT = "\x1B[36m";
|
|
874
871
|
const ANSI_HOOK = "\x1B[35m";
|
|
872
|
+
const ANSI_MUTED = "\x1B[38;5;244m";
|
|
875
873
|
const ANSI_UNUSED = "\x1B[38;5;214m";
|
|
876
874
|
function resolveColorSupport(mode = "auto", options = {}) {
|
|
877
875
|
if (mode === true) return true;
|
|
@@ -888,10 +886,86 @@ function colorizeUnusedMarker(text, enabled) {
|
|
|
888
886
|
function formatReactSymbolLabel(name, kind, enabled) {
|
|
889
887
|
const label = `${name} [${kind}]`;
|
|
890
888
|
if (!enabled) return label;
|
|
891
|
-
return `${kind
|
|
889
|
+
return `${getReactSymbolColor(kind)}${label}${ANSI_RESET}`;
|
|
890
|
+
}
|
|
891
|
+
function colorizeReactLabel(text, kind, enabled) {
|
|
892
|
+
if (!enabled) return text;
|
|
893
|
+
return `${getReactSymbolColor(kind)}${text}${ANSI_RESET}`;
|
|
894
|
+
}
|
|
895
|
+
function colorizeMuted(text, enabled) {
|
|
896
|
+
if (!enabled) return text;
|
|
897
|
+
return `${ANSI_MUTED}${text}${ANSI_RESET}`;
|
|
898
|
+
}
|
|
899
|
+
function getReactSymbolColor(kind) {
|
|
900
|
+
return kind === "component" ? ANSI_COMPONENT : ANSI_HOOK;
|
|
901
|
+
}
|
|
902
|
+
//#endregion
|
|
903
|
+
//#region src/utils/to-display-path.ts
|
|
904
|
+
function toDisplayPath(filePath, cwd) {
|
|
905
|
+
const relativePath = path.relative(cwd, filePath);
|
|
906
|
+
if (relativePath === "") return ".";
|
|
907
|
+
const normalizedPath = relativePath.split(path.sep).join("/");
|
|
908
|
+
return normalizedPath.startsWith("..") ? filePath : normalizedPath;
|
|
909
|
+
}
|
|
910
|
+
//#endregion
|
|
911
|
+
//#region src/output/ascii/import.ts
|
|
912
|
+
function printDependencyTree(graph, options = {}) {
|
|
913
|
+
const cwd = options.cwd ?? graph.cwd;
|
|
914
|
+
const color = resolveColorSupport(options.color);
|
|
915
|
+
const includeExternals = options.includeExternals ?? false;
|
|
916
|
+
const omitUnused = options.omitUnused ?? false;
|
|
917
|
+
const rootLines = [toDisplayPath(graph.entryId, cwd)];
|
|
918
|
+
const visited = new Set([graph.entryId]);
|
|
919
|
+
const entryNode = graph.nodes.get(graph.entryId);
|
|
920
|
+
if (entryNode === void 0) return rootLines.join("\n");
|
|
921
|
+
const rootDependencies = filterDependencies(entryNode.dependencies, includeExternals, omitUnused);
|
|
922
|
+
rootDependencies.forEach((dependency, index) => {
|
|
923
|
+
rootLines.push(...renderDependency(dependency, graph, visited, "", index === rootDependencies.length - 1, includeExternals, omitUnused, color, cwd));
|
|
924
|
+
});
|
|
925
|
+
return rootLines.join("\n");
|
|
926
|
+
}
|
|
927
|
+
function renderDependency(dependency, graph, visited, prefix, isLast, includeExternals, omitUnused, color, cwd) {
|
|
928
|
+
const branch = `${prefix}${isLast ? "└─ " : "├─ "}`;
|
|
929
|
+
const label = formatDependencyLabel(dependency, cwd, color);
|
|
930
|
+
if (dependency.kind !== "source") return [`${branch}${label}`];
|
|
931
|
+
if (visited.has(dependency.target)) return [`${branch}${label} (circular)`];
|
|
932
|
+
const childNode = graph.nodes.get(dependency.target);
|
|
933
|
+
if (childNode === void 0) return [`${branch}${label}`];
|
|
934
|
+
const nextPrefix = `${prefix}${isLast ? " " : "│ "}`;
|
|
935
|
+
const nextVisited = new Set(visited);
|
|
936
|
+
nextVisited.add(dependency.target);
|
|
937
|
+
const childLines = [`${branch}${label}`];
|
|
938
|
+
const childDependencies = filterDependencies(childNode.dependencies, includeExternals, omitUnused);
|
|
939
|
+
childDependencies.forEach((childDependency, index) => {
|
|
940
|
+
childLines.push(...renderDependency(childDependency, graph, nextVisited, nextPrefix, index === childDependencies.length - 1, includeExternals, omitUnused, color, cwd));
|
|
941
|
+
});
|
|
942
|
+
return childLines;
|
|
943
|
+
}
|
|
944
|
+
function filterDependencies(dependencies, includeExternals, omitUnused) {
|
|
945
|
+
return dependencies.filter((dependency) => {
|
|
946
|
+
if (omitUnused && dependency.unused) return false;
|
|
947
|
+
if (dependency.kind === "source" || dependency.kind === "missing") return true;
|
|
948
|
+
return includeExternals;
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
function formatDependencyLabel(dependency, cwd, color) {
|
|
952
|
+
const prefixes = [];
|
|
953
|
+
if (dependency.isTypeOnly) prefixes.push("type");
|
|
954
|
+
if (dependency.referenceKind === "require") prefixes.push("require");
|
|
955
|
+
else if (dependency.referenceKind === "dynamic-import") prefixes.push("dynamic");
|
|
956
|
+
else if (dependency.referenceKind === "export") prefixes.push("re-export");
|
|
957
|
+
else if (dependency.referenceKind === "import-equals") prefixes.push("import=");
|
|
958
|
+
const annotation = prefixes.length > 0 ? `[${prefixes.join(", ")}] ` : "";
|
|
959
|
+
if (dependency.kind === "source") return colorizeUnusedMarker(withUnusedSuffix(`${annotation}${toDisplayPath(dependency.target, cwd)}`, dependency.unused), color);
|
|
960
|
+
if (dependency.kind === "missing") return colorizeUnusedMarker(withUnusedSuffix(`${annotation}${dependency.specifier} [missing]`, dependency.unused), color);
|
|
961
|
+
if (dependency.kind === "builtin") return colorizeUnusedMarker(withUnusedSuffix(`${annotation}${dependency.target} [builtin]`, dependency.unused), color);
|
|
962
|
+
return colorizeUnusedMarker(withUnusedSuffix(`${annotation}${dependency.target} [external]`, dependency.unused), color);
|
|
963
|
+
}
|
|
964
|
+
function withUnusedSuffix(label, unused) {
|
|
965
|
+
return unused ? `${label} (unused)` : label;
|
|
892
966
|
}
|
|
893
967
|
//#endregion
|
|
894
|
-
//#region src/react
|
|
968
|
+
//#region src/output/ascii/react.ts
|
|
895
969
|
function printReactUsageTree(graph, options = {}) {
|
|
896
970
|
const cwd = options.cwd ?? graph.cwd;
|
|
897
971
|
const color = resolveColorSupport(options.color);
|
|
@@ -919,7 +993,7 @@ function renderReactUsageEntries(graph, entries, cwd, filter, color) {
|
|
|
919
993
|
const root = graph.nodes.get(entry.target);
|
|
920
994
|
if (root === void 0) return;
|
|
921
995
|
lines.push(formatReactEntryLabel(entry, cwd));
|
|
922
|
-
lines.push(formatReactNodeLabel(root, cwd, color));
|
|
996
|
+
lines.push(formatReactNodeLabel(root, cwd, color, entry.referenceName));
|
|
923
997
|
const usages = getFilteredUsages(root, graph, filter);
|
|
924
998
|
usages.forEach((usage, usageIndex) => {
|
|
925
999
|
lines.push(...renderUsage(usage, graph, cwd, filter, color, new Set([root.id]), "", usageIndex === usages.length - 1));
|
|
@@ -932,8 +1006,8 @@ function renderUsage(usage, graph, cwd, filter, color, visited, prefix, isLast)
|
|
|
932
1006
|
const branch = `${prefix}${isLast ? "└─ " : "├─ "}`;
|
|
933
1007
|
const target = graph.nodes.get(usage.target);
|
|
934
1008
|
if (target === void 0) return [`${branch}${usage.target}`];
|
|
935
|
-
if (visited.has(target.id)) return [`${branch}${formatReactNodeLabel(target, cwd, color)} (circular)`];
|
|
936
|
-
const childLines = [`${branch}${formatReactNodeLabel(target, cwd, color)}`];
|
|
1009
|
+
if (visited.has(target.id)) return [`${branch}${formatReactNodeLabel(target, cwd, color, usage.referenceName)} (circular)`];
|
|
1010
|
+
const childLines = [`${branch}${formatReactNodeLabel(target, cwd, color, usage.referenceName)}`];
|
|
937
1011
|
const nextVisited = new Set(visited);
|
|
938
1012
|
nextVisited.add(target.id);
|
|
939
1013
|
const nextPrefix = `${prefix}${isLast ? " " : "│ "}`;
|
|
@@ -943,72 +1017,114 @@ function renderUsage(usage, graph, cwd, filter, color, visited, prefix, isLast)
|
|
|
943
1017
|
});
|
|
944
1018
|
return childLines;
|
|
945
1019
|
}
|
|
946
|
-
function formatReactNodeLabel(node, cwd, color) {
|
|
947
|
-
return `${formatReactSymbolLabel(node.name, node.kind, color)} (${toDisplayPath(node.filePath, cwd)})`;
|
|
1020
|
+
function formatReactNodeLabel(node, cwd, color, referenceName) {
|
|
1021
|
+
return `${referenceName !== void 0 && referenceName !== node.name ? `${colorizeReactLabel(node.name, node.kind, color)} ${colorizeMuted(`as ${referenceName}`, color)} ${colorizeReactLabel(`[${node.kind}]`, node.kind, color)}` : formatReactSymbolLabel(node.name, node.kind, color)} (${toDisplayPath(node.filePath, cwd)})`;
|
|
948
1022
|
}
|
|
949
1023
|
function formatReactEntryLabel(entry, cwd) {
|
|
950
1024
|
return `${toDisplayPath(entry.location.filePath, cwd)}:${entry.location.line}:${entry.location.column}`;
|
|
951
1025
|
}
|
|
952
1026
|
//#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");
|
|
1027
|
+
//#region src/output/json/import.ts
|
|
1028
|
+
function graphToSerializableTree(graph, options = {}) {
|
|
1029
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1030
|
+
return serializeNode(graph.entryId, graph, visited, options.omitUnused ?? false);
|
|
969
1031
|
}
|
|
970
|
-
function
|
|
971
|
-
const
|
|
972
|
-
const
|
|
973
|
-
if (
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1032
|
+
function serializeNode(filePath, graph, visited, omitUnused) {
|
|
1033
|
+
const node = graph.nodes.get(filePath);
|
|
1034
|
+
const displayPath = toDisplayPath(filePath, graph.cwd);
|
|
1035
|
+
if (node === void 0) return {
|
|
1036
|
+
path: displayPath,
|
|
1037
|
+
kind: "missing",
|
|
1038
|
+
dependencies: []
|
|
1039
|
+
};
|
|
1040
|
+
if (visited.has(filePath)) return {
|
|
1041
|
+
path: displayPath,
|
|
1042
|
+
kind: "circular",
|
|
1043
|
+
dependencies: []
|
|
1044
|
+
};
|
|
1045
|
+
visited.add(filePath);
|
|
1046
|
+
const dependencies = node.dependencies.filter((dependency) => !omitUnused || !dependency.unused).map((dependency) => {
|
|
1047
|
+
if (dependency.kind !== "source") return {
|
|
1048
|
+
specifier: dependency.specifier,
|
|
1049
|
+
referenceKind: dependency.referenceKind,
|
|
1050
|
+
isTypeOnly: dependency.isTypeOnly,
|
|
1051
|
+
unused: dependency.unused,
|
|
1052
|
+
kind: dependency.kind,
|
|
1053
|
+
target: dependency.kind === "missing" ? dependency.target : toDisplayPath(dependency.target, graph.cwd)
|
|
1054
|
+
};
|
|
1055
|
+
return {
|
|
1056
|
+
specifier: dependency.specifier,
|
|
1057
|
+
referenceKind: dependency.referenceKind,
|
|
1058
|
+
isTypeOnly: dependency.isTypeOnly,
|
|
1059
|
+
unused: dependency.unused,
|
|
1060
|
+
kind: dependency.kind,
|
|
1061
|
+
target: toDisplayPath(dependency.target, graph.cwd),
|
|
1062
|
+
node: serializeNode(dependency.target, graph, new Set(visited), omitUnused)
|
|
1063
|
+
};
|
|
985
1064
|
});
|
|
986
|
-
return
|
|
1065
|
+
return {
|
|
1066
|
+
path: displayPath,
|
|
1067
|
+
kind: filePath === graph.entryId ? "entry" : "source",
|
|
1068
|
+
dependencies
|
|
1069
|
+
};
|
|
987
1070
|
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1071
|
+
//#endregion
|
|
1072
|
+
//#region src/output/json/react.ts
|
|
1073
|
+
function graphToSerializableReactTree(graph, options = {}) {
|
|
1074
|
+
const filter = options.filter ?? "all";
|
|
1075
|
+
const entries = getReactUsageEntries(graph, filter);
|
|
1076
|
+
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()));
|
|
1077
|
+
return {
|
|
1078
|
+
kind: "react-usage",
|
|
1079
|
+
entries: entries.map((entry) => serializeReactUsageEntry(entry, graph, filter)),
|
|
1080
|
+
roots
|
|
1081
|
+
};
|
|
994
1082
|
}
|
|
995
|
-
function
|
|
996
|
-
const
|
|
997
|
-
if (
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
if (
|
|
1006
|
-
|
|
1083
|
+
function serializeReactUsageNode(nodeId, graph, filter, visited) {
|
|
1084
|
+
const node = graph.nodes.get(nodeId);
|
|
1085
|
+
if (node === void 0) return {
|
|
1086
|
+
id: nodeId,
|
|
1087
|
+
name: nodeId,
|
|
1088
|
+
symbolKind: "circular",
|
|
1089
|
+
filePath: "",
|
|
1090
|
+
exportNames: [],
|
|
1091
|
+
usages: []
|
|
1092
|
+
};
|
|
1093
|
+
if (visited.has(nodeId)) return {
|
|
1094
|
+
id: node.id,
|
|
1095
|
+
name: node.name,
|
|
1096
|
+
symbolKind: "circular",
|
|
1097
|
+
filePath: toDisplayPath(node.filePath, graph.cwd),
|
|
1098
|
+
exportNames: node.exportNames,
|
|
1099
|
+
usages: []
|
|
1100
|
+
};
|
|
1101
|
+
const nextVisited = new Set(visited);
|
|
1102
|
+
nextVisited.add(nodeId);
|
|
1103
|
+
return {
|
|
1104
|
+
id: node.id,
|
|
1105
|
+
name: node.name,
|
|
1106
|
+
symbolKind: node.kind,
|
|
1107
|
+
filePath: toDisplayPath(node.filePath, graph.cwd),
|
|
1108
|
+
exportNames: node.exportNames,
|
|
1109
|
+
usages: getFilteredUsages(node, graph, filter).map((usage) => ({
|
|
1110
|
+
kind: usage.kind,
|
|
1111
|
+
targetId: usage.target,
|
|
1112
|
+
referenceName: usage.referenceName,
|
|
1113
|
+
node: serializeReactUsageNode(usage.target, graph, filter, nextVisited)
|
|
1114
|
+
}))
|
|
1115
|
+
};
|
|
1007
1116
|
}
|
|
1008
|
-
function
|
|
1009
|
-
return
|
|
1117
|
+
function serializeReactUsageEntry(entry, graph, filter) {
|
|
1118
|
+
return {
|
|
1119
|
+
targetId: entry.target,
|
|
1120
|
+
referenceName: entry.referenceName,
|
|
1121
|
+
filePath: toDisplayPath(entry.location.filePath, graph.cwd),
|
|
1122
|
+
line: entry.location.line,
|
|
1123
|
+
column: entry.location.column,
|
|
1124
|
+
node: serializeReactUsageNode(entry.target, graph, filter, /* @__PURE__ */ new Set())
|
|
1125
|
+
};
|
|
1010
1126
|
}
|
|
1011
1127
|
//#endregion
|
|
1012
|
-
export {
|
|
1128
|
+
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
1129
|
|
|
1014
|
-
//# sourceMappingURL=
|
|
1130
|
+
//# sourceMappingURL=react-BHPy_fw5.mjs.map
|