foresthouse 1.0.0-dev.6 → 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-Qsy9Nmh9.mjs → react-CWIt8V0Y.mjs} +676 -575
- package/dist/react-CWIt8V0Y.mjs.map +1 -0
- package/package.json +2 -1
- package/dist/tree-Qsy9Nmh9.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,296 +192,132 @@ 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
|
-
if (filter === "hook") return [];
|
|
409
|
-
return graph.entries.filter((entry) => {
|
|
410
|
-
const targetNode = graph.nodes.get(entry.target);
|
|
411
|
-
return targetNode !== void 0 && matchesReactFilter(targetNode, filter);
|
|
412
|
-
});
|
|
235
|
+
visit(sourceFile);
|
|
236
|
+
return [...references.values()];
|
|
413
237
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
if (roots.length > 0) return roots.sort((left, right) => compareReactNodeIds(left, right, graph.nodes));
|
|
429
|
-
return filteredNodes.map((node) => node.id).sort((left, right) => compareReactNodeIds(left, right, graph.nodes));
|
|
430
|
-
}
|
|
431
|
-
function getFilteredUsages(node, graph, filter = "all") {
|
|
432
|
-
return node.usages.filter((usage) => {
|
|
433
|
-
const targetNode = graph.nodes.get(usage.target);
|
|
434
|
-
return targetNode !== void 0 && matchesReactFilter(targetNode, filter);
|
|
435
|
-
});
|
|
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);
|
|
436
252
|
}
|
|
437
|
-
function
|
|
438
|
-
const symbolsByName = /* @__PURE__ */ new Map();
|
|
439
|
-
program.body.forEach((statement) => {
|
|
440
|
-
collectTopLevelReactSymbols(statement, filePath, symbolsByName);
|
|
441
|
-
});
|
|
442
|
-
const importsByLocalName = /* @__PURE__ */ new Map();
|
|
443
|
-
const exportsByName = /* @__PURE__ */ new Map();
|
|
444
|
-
const renderEntries = collectRenderEntries(program, filePath, sourceText, includeNestedRenderEntries);
|
|
445
|
-
program.body.forEach((statement) => {
|
|
446
|
-
collectImportsAndExports(statement, sourceDependencies, symbolsByName, importsByLocalName, exportsByName);
|
|
447
|
-
});
|
|
448
|
-
symbolsByName.forEach((symbol) => {
|
|
449
|
-
analyzeSymbolUsages(symbol);
|
|
450
|
-
});
|
|
253
|
+
function createEdge(reference, kind, target) {
|
|
451
254
|
return {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
255
|
+
specifier: reference.specifier,
|
|
256
|
+
referenceKind: reference.referenceKind,
|
|
257
|
+
isTypeOnly: reference.isTypeOnly,
|
|
258
|
+
unused: reference.unused,
|
|
259
|
+
kind,
|
|
260
|
+
target
|
|
458
261
|
};
|
|
459
262
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
if (!hasComponentAncestor) addPendingRenderEntry(entries, referenceName, createReactUsageLocation(filePath, sourceText, node.start));
|
|
477
|
-
nextHasComponentAncestor = true;
|
|
478
|
-
}
|
|
479
|
-
} else if (node.type === "CallExpression") {
|
|
480
|
-
const referenceName = getCreateElementComponentReferenceName(node);
|
|
481
|
-
if (referenceName !== void 0) {
|
|
482
|
-
if (!hasComponentAncestor) addPendingRenderEntry(entries, referenceName, createReactUsageLocation(filePath, sourceText, node.start));
|
|
483
|
-
nextHasComponentAncestor = true;
|
|
484
|
-
}
|
|
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();
|
|
485
279
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
const value = node[key];
|
|
490
|
-
collectRenderEntryChild(value, filePath, sourceText, entries, nextHasComponentAncestor, includeNestedFunctions);
|
|
491
|
-
});
|
|
492
|
-
}
|
|
493
|
-
function collectRenderEntryChild(value, filePath, sourceText, entries, hasComponentAncestor, includeNestedFunctions) {
|
|
494
|
-
if (Array.isArray(value)) {
|
|
495
|
-
value.forEach((entry) => {
|
|
496
|
-
collectRenderEntryChild(entry, filePath, sourceText, entries, hasComponentAncestor, includeNestedFunctions);
|
|
497
|
-
});
|
|
498
|
-
return;
|
|
280
|
+
build() {
|
|
281
|
+
this.visitFile(this.entryPath);
|
|
282
|
+
return this.nodes;
|
|
499
283
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
}
|
|
510
|
-
function createReactUsageLocation(filePath, sourceText, offset) {
|
|
511
|
-
return {
|
|
512
|
-
filePath,
|
|
513
|
-
...offsetToLineAndColumn(sourceText, offset)
|
|
514
|
-
};
|
|
515
|
-
}
|
|
516
|
-
function offsetToLineAndColumn(sourceText, offset) {
|
|
517
|
-
let line = 1;
|
|
518
|
-
let column = 1;
|
|
519
|
-
for (let index = 0; index < offset && index < sourceText.length; index += 1) {
|
|
520
|
-
if (sourceText[index] === "\n") {
|
|
521
|
-
line += 1;
|
|
522
|
-
column = 1;
|
|
523
|
-
continue;
|
|
524
|
-
}
|
|
525
|
-
column += 1;
|
|
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);
|
|
526
293
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
});
|
|
541
|
-
return;
|
|
542
|
-
case "ExportNamedDeclaration":
|
|
543
|
-
if (statement.declaration !== null) collectTopLevelReactSymbols(statement.declaration, filePath, symbolsByName);
|
|
544
|
-
return;
|
|
545
|
-
case "ExportDefaultDeclaration":
|
|
546
|
-
addDefaultExportSymbol(statement, filePath, symbolsByName);
|
|
547
|
-
return;
|
|
548
|
-
default: return;
|
|
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);
|
|
549
307
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
if (declarator.id.type !== "Identifier" || declarator.init === null) return;
|
|
560
|
-
if (declarator.init.type !== "ArrowFunctionExpression" && declarator.init.type !== "FunctionExpression") return;
|
|
561
|
-
const name = declarator.id.name;
|
|
562
|
-
const kind = classifyReactSymbol(name, declarator.init);
|
|
563
|
-
if (kind === void 0) return;
|
|
564
|
-
symbolsByName.set(name, createPendingSymbol(filePath, name, kind, declarator.init));
|
|
565
|
-
}
|
|
566
|
-
function addDefaultExportSymbol(declaration, filePath, symbolsByName) {
|
|
567
|
-
if (declaration.declaration.type === "FunctionDeclaration" || declaration.declaration.type === "FunctionExpression") addFunctionSymbol(declaration.declaration, filePath, symbolsByName);
|
|
568
|
-
else if (declaration.declaration.type === "ArrowFunctionExpression") {
|
|
569
|
-
const name = "default";
|
|
570
|
-
const kind = declaration.declaration.body ? classifyReactSymbol(name, declaration.declaration) : void 0;
|
|
571
|
-
if (kind !== void 0) symbolsByName.set(name, createPendingSymbol(filePath, name, kind, declaration.declaration));
|
|
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
|
+
};
|
|
572
317
|
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
id: `${filePath}#${kind}:${name}`,
|
|
577
|
-
name,
|
|
578
|
-
kind,
|
|
579
|
-
filePath,
|
|
580
|
-
declaration,
|
|
581
|
-
exportNames: /* @__PURE__ */ new Set(),
|
|
582
|
-
componentReferences: /* @__PURE__ */ new Set(),
|
|
583
|
-
hookReferences: /* @__PURE__ */ new Set()
|
|
584
|
-
};
|
|
585
|
-
}
|
|
318
|
+
};
|
|
319
|
+
//#endregion
|
|
320
|
+
//#region src/analyzers/react/bindings.ts
|
|
586
321
|
function collectImportsAndExports(statement, sourceDependencies, symbolsByName, importsByLocalName, exportsByName) {
|
|
587
322
|
switch (statement.type) {
|
|
588
323
|
case "ImportDeclaration":
|
|
@@ -663,38 +398,48 @@ function addExportBinding(localName, exportedName, symbolsByName, exportsByName)
|
|
|
663
398
|
symbol.exportNames.add(exportedName);
|
|
664
399
|
exportsByName.set(exportedName, symbol.id);
|
|
665
400
|
}
|
|
666
|
-
function
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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);
|
|
681
423
|
});
|
|
682
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
|
+
}
|
|
683
439
|
function classifyReactSymbol(name, declaration) {
|
|
684
440
|
if (isHookName(name)) return "hook";
|
|
685
441
|
if (isComponentName(name) && returnsReactElement(declaration)) return "component";
|
|
686
442
|
}
|
|
687
|
-
function returnsReactElement(declaration) {
|
|
688
|
-
if (declaration.type === "ArrowFunctionExpression" && declaration.expression) return containsReactElementLikeExpression(declaration.body);
|
|
689
|
-
const body = declaration.body;
|
|
690
|
-
if (body === null) return false;
|
|
691
|
-
let found = false;
|
|
692
|
-
walkReactUsageTree(body, (node) => {
|
|
693
|
-
if (node.type !== "ReturnStatement" || node.argument === null) return;
|
|
694
|
-
if (containsReactElementLikeExpression(node.argument)) found = true;
|
|
695
|
-
});
|
|
696
|
-
return found;
|
|
697
|
-
}
|
|
698
443
|
function containsReactElementLikeExpression(expression) {
|
|
699
444
|
let found = false;
|
|
700
445
|
walkNode(expression, (node) => {
|
|
@@ -716,6 +461,23 @@ function getCreateElementComponentReferenceName(node) {
|
|
|
716
461
|
if (firstArgument === void 0 || firstArgument.type !== "Identifier") return;
|
|
717
462
|
return isComponentName(firstArgument.name) ? firstArgument.name : void 0;
|
|
718
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;
|
|
478
|
+
});
|
|
479
|
+
return found;
|
|
480
|
+
}
|
|
719
481
|
function isReactCreateElementCall(node) {
|
|
720
482
|
const callee = unwrapExpression(node.callee);
|
|
721
483
|
if (callee.type !== "MemberExpression" || callee.computed) return false;
|
|
@@ -738,32 +500,188 @@ function unwrapExpression(expression) {
|
|
|
738
500
|
return current;
|
|
739
501
|
}
|
|
740
502
|
}
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
503
|
+
//#endregion
|
|
504
|
+
//#region src/analyzers/react/entries.ts
|
|
505
|
+
function collectEntryUsages(program, filePath, sourceText, includeNestedFunctions) {
|
|
506
|
+
const entries = /* @__PURE__ */ new Map();
|
|
507
|
+
program.body.forEach((statement) => {
|
|
508
|
+
collectStatementEntryUsages(statement, filePath, sourceText, entries, includeNestedFunctions);
|
|
509
|
+
});
|
|
510
|
+
return [...entries.values()].sort(comparePendingReactUsageEntries);
|
|
511
|
+
}
|
|
512
|
+
function collectStatementEntryUsages(statement, filePath, sourceText, entries, includeNestedFunctions) {
|
|
513
|
+
collectNodeEntryUsages(statement, filePath, sourceText, entries, false, includeNestedFunctions);
|
|
514
|
+
}
|
|
515
|
+
function collectNodeEntryUsages(node, filePath, sourceText, entries, hasComponentAncestor, includeNestedFunctions) {
|
|
516
|
+
if (!includeNestedFunctions && FUNCTION_NODE_TYPES.has(node.type)) return;
|
|
517
|
+
let nextHasComponentAncestor = hasComponentAncestor;
|
|
518
|
+
if (node.type === "JSXElement") {
|
|
519
|
+
const referenceName = getComponentReferenceName(node);
|
|
520
|
+
if (referenceName !== void 0) {
|
|
521
|
+
if (!hasComponentAncestor) addPendingReactUsageEntry(entries, referenceName, "component", createReactUsageLocation(filePath, sourceText, node.start));
|
|
522
|
+
nextHasComponentAncestor = true;
|
|
523
|
+
}
|
|
524
|
+
} else if (node.type === "CallExpression") {
|
|
525
|
+
const hookReference = getHookReferenceName(node);
|
|
526
|
+
if (hookReference !== void 0) addPendingReactUsageEntry(entries, hookReference, "hook", createReactUsageLocation(filePath, sourceText, node.start));
|
|
527
|
+
const referenceName = getCreateElementComponentReferenceName(node);
|
|
528
|
+
if (referenceName !== void 0) {
|
|
529
|
+
if (!hasComponentAncestor) addPendingReactUsageEntry(entries, referenceName, "component", createReactUsageLocation(filePath, sourceText, node.start));
|
|
530
|
+
nextHasComponentAncestor = true;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
746
533
|
const keys = visitorKeys[node.type];
|
|
747
534
|
if (keys === void 0) return;
|
|
748
535
|
keys.forEach((key) => {
|
|
749
536
|
const value = node[key];
|
|
750
|
-
|
|
537
|
+
collectEntryUsageChild(value, filePath, sourceText, entries, nextHasComponentAncestor, includeNestedFunctions);
|
|
751
538
|
});
|
|
752
539
|
}
|
|
753
|
-
function
|
|
540
|
+
function collectEntryUsageChild(value, filePath, sourceText, entries, hasComponentAncestor, includeNestedFunctions) {
|
|
754
541
|
if (Array.isArray(value)) {
|
|
755
542
|
value.forEach((entry) => {
|
|
756
|
-
|
|
543
|
+
collectEntryUsageChild(entry, filePath, sourceText, entries, hasComponentAncestor, includeNestedFunctions);
|
|
757
544
|
});
|
|
758
545
|
return;
|
|
759
546
|
}
|
|
760
547
|
if (!isNode(value)) return;
|
|
761
|
-
|
|
762
|
-
walkNode(value, visit, false);
|
|
548
|
+
collectNodeEntryUsages(value, filePath, sourceText, entries, hasComponentAncestor, includeNestedFunctions);
|
|
763
549
|
}
|
|
764
|
-
function
|
|
765
|
-
|
|
550
|
+
function addPendingReactUsageEntry(entries, referenceName, kind, location) {
|
|
551
|
+
const key = `${location.filePath}:${location.line}:${location.column}:${kind}:${referenceName}`;
|
|
552
|
+
entries.set(key, {
|
|
553
|
+
referenceName,
|
|
554
|
+
kind,
|
|
555
|
+
location
|
|
556
|
+
});
|
|
766
557
|
}
|
|
558
|
+
function createReactUsageLocation(filePath, sourceText, offset) {
|
|
559
|
+
return {
|
|
560
|
+
filePath,
|
|
561
|
+
...offsetToLineAndColumn(sourceText, offset)
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
function offsetToLineAndColumn(sourceText, offset) {
|
|
565
|
+
let line = 1;
|
|
566
|
+
let column = 1;
|
|
567
|
+
for (let index = 0; index < offset && index < sourceText.length; index += 1) {
|
|
568
|
+
if (sourceText[index] === "\n") {
|
|
569
|
+
line += 1;
|
|
570
|
+
column = 1;
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
column += 1;
|
|
574
|
+
}
|
|
575
|
+
return {
|
|
576
|
+
line,
|
|
577
|
+
column
|
|
578
|
+
};
|
|
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
|
|
585
|
+
function collectTopLevelReactSymbols(statement, filePath, symbolsByName) {
|
|
586
|
+
switch (statement.type) {
|
|
587
|
+
case "FunctionDeclaration":
|
|
588
|
+
addFunctionSymbol(statement, filePath, symbolsByName);
|
|
589
|
+
return;
|
|
590
|
+
case "VariableDeclaration":
|
|
591
|
+
statement.declarations.forEach((declarator) => {
|
|
592
|
+
addVariableSymbol(declarator, filePath, symbolsByName);
|
|
593
|
+
});
|
|
594
|
+
return;
|
|
595
|
+
case "ExportNamedDeclaration":
|
|
596
|
+
if (statement.declaration !== null) collectTopLevelReactSymbols(statement.declaration, filePath, symbolsByName);
|
|
597
|
+
return;
|
|
598
|
+
case "ExportDefaultDeclaration":
|
|
599
|
+
addDefaultExportSymbol(statement, filePath, symbolsByName);
|
|
600
|
+
return;
|
|
601
|
+
default: return;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
function addFunctionSymbol(declaration, filePath, symbolsByName) {
|
|
605
|
+
const name = declaration.id?.name;
|
|
606
|
+
if (name === void 0) return;
|
|
607
|
+
const kind = classifyReactSymbol(name, declaration);
|
|
608
|
+
if (kind === void 0) return;
|
|
609
|
+
symbolsByName.set(name, createPendingSymbol(filePath, name, kind, declaration));
|
|
610
|
+
}
|
|
611
|
+
function addVariableSymbol(declarator, filePath, symbolsByName) {
|
|
612
|
+
if (declarator.id.type !== "Identifier" || declarator.init === null) return;
|
|
613
|
+
if (declarator.init.type !== "ArrowFunctionExpression" && declarator.init.type !== "FunctionExpression") return;
|
|
614
|
+
const name = declarator.id.name;
|
|
615
|
+
const kind = classifyReactSymbol(name, declarator.init);
|
|
616
|
+
if (kind === void 0) return;
|
|
617
|
+
symbolsByName.set(name, createPendingSymbol(filePath, name, kind, declarator.init));
|
|
618
|
+
}
|
|
619
|
+
function addDefaultExportSymbol(declaration, filePath, symbolsByName) {
|
|
620
|
+
if (declaration.declaration.type === "FunctionDeclaration" || declaration.declaration.type === "FunctionExpression") addFunctionSymbol(declaration.declaration, filePath, symbolsByName);
|
|
621
|
+
else if (declaration.declaration.type === "ArrowFunctionExpression") {
|
|
622
|
+
const name = "default";
|
|
623
|
+
const kind = declaration.declaration.body ? classifyReactSymbol(name, declaration.declaration) : void 0;
|
|
624
|
+
if (kind !== void 0) symbolsByName.set(name, createPendingSymbol(filePath, name, kind, declaration.declaration));
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
function createPendingSymbol(filePath, name, kind, declaration) {
|
|
628
|
+
return {
|
|
629
|
+
id: `${filePath}#${kind}:${name}`,
|
|
630
|
+
name,
|
|
631
|
+
kind,
|
|
632
|
+
filePath,
|
|
633
|
+
declaration,
|
|
634
|
+
exportNames: /* @__PURE__ */ new Set(),
|
|
635
|
+
componentReferences: /* @__PURE__ */ new Set(),
|
|
636
|
+
hookReferences: /* @__PURE__ */ new Set()
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
//#endregion
|
|
640
|
+
//#region src/analyzers/react/usage.ts
|
|
641
|
+
function analyzeSymbolUsages(symbol) {
|
|
642
|
+
const root = symbol.declaration.type === "ArrowFunctionExpression" ? symbol.declaration.body : symbol.declaration.body;
|
|
643
|
+
if (root === null) return;
|
|
644
|
+
walkReactUsageTree(root, (node) => {
|
|
645
|
+
if (node.type === "JSXElement") {
|
|
646
|
+
const name = getComponentReferenceName(node);
|
|
647
|
+
if (name !== void 0) symbol.componentReferences.add(name);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
if (node.type === "CallExpression") {
|
|
651
|
+
const hookReference = getHookReferenceName(node);
|
|
652
|
+
if (hookReference !== void 0) symbol.hookReferences.add(hookReference);
|
|
653
|
+
const componentReference = getCreateElementComponentReferenceName(node);
|
|
654
|
+
if (componentReference !== void 0) symbol.componentReferences.add(componentReference);
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
}
|
|
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);
|
|
664
|
+
});
|
|
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);
|
|
670
|
+
});
|
|
671
|
+
symbolsByName.forEach((symbol) => {
|
|
672
|
+
analyzeSymbolUsages(symbol);
|
|
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
|
+
};
|
|
682
|
+
}
|
|
683
|
+
//#endregion
|
|
684
|
+
//#region src/analyzers/react/references.ts
|
|
767
685
|
function resolveReactReference(fileAnalysis, fileAnalyses, name, kind) {
|
|
768
686
|
const localSymbol = fileAnalysis.symbolsByName.get(name);
|
|
769
687
|
if (localSymbol !== void 0 && localSymbol.kind === kind) return localSymbol.id;
|
|
@@ -776,6 +694,14 @@ function resolveReactReference(fileAnalysis, fileAnalyses, name, kind) {
|
|
|
776
694
|
if (targetId === void 0) return;
|
|
777
695
|
return sourceFileAnalysis.symbolsById.get(targetId)?.kind === kind ? targetId : void 0;
|
|
778
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
|
+
}
|
|
779
705
|
function createExternalHookNode(binding, localName) {
|
|
780
706
|
const name = getExternalHookName(binding, localName);
|
|
781
707
|
return {
|
|
@@ -793,54 +719,6 @@ function getExternalHookNodeId(binding, localName) {
|
|
|
793
719
|
function getExternalHookName(binding, localName) {
|
|
794
720
|
return binding.importedName === "default" ? localName : binding.importedName;
|
|
795
721
|
}
|
|
796
|
-
function getFilteredReactUsageNodes(graph, filter) {
|
|
797
|
-
return [...graph.nodes.values()].filter((node) => matchesReactFilter(node, filter)).sort((left, right) => compareReactNodes(left, right));
|
|
798
|
-
}
|
|
799
|
-
function matchesReactFilter(node, filter) {
|
|
800
|
-
return filter === "all" || node.kind === filter;
|
|
801
|
-
}
|
|
802
|
-
function serializeReactUsageNode(nodeId, graph, filter, visited) {
|
|
803
|
-
const node = graph.nodes.get(nodeId);
|
|
804
|
-
if (node === void 0) return {
|
|
805
|
-
id: nodeId,
|
|
806
|
-
name: nodeId,
|
|
807
|
-
symbolKind: "circular",
|
|
808
|
-
filePath: "",
|
|
809
|
-
exportNames: [],
|
|
810
|
-
usages: []
|
|
811
|
-
};
|
|
812
|
-
if (visited.has(nodeId)) return {
|
|
813
|
-
id: node.id,
|
|
814
|
-
name: node.name,
|
|
815
|
-
symbolKind: "circular",
|
|
816
|
-
filePath: toDisplayPath(node.filePath, graph.cwd),
|
|
817
|
-
exportNames: node.exportNames,
|
|
818
|
-
usages: []
|
|
819
|
-
};
|
|
820
|
-
const nextVisited = new Set(visited);
|
|
821
|
-
nextVisited.add(nodeId);
|
|
822
|
-
return {
|
|
823
|
-
id: node.id,
|
|
824
|
-
name: node.name,
|
|
825
|
-
symbolKind: node.kind,
|
|
826
|
-
filePath: toDisplayPath(node.filePath, graph.cwd),
|
|
827
|
-
exportNames: node.exportNames,
|
|
828
|
-
usages: getFilteredUsages(node, graph, filter).map((usage) => ({
|
|
829
|
-
kind: usage.kind,
|
|
830
|
-
targetId: usage.target,
|
|
831
|
-
node: serializeReactUsageNode(usage.target, graph, filter, nextVisited)
|
|
832
|
-
}))
|
|
833
|
-
};
|
|
834
|
-
}
|
|
835
|
-
function serializeReactUsageEntry(entry, graph, filter) {
|
|
836
|
-
return {
|
|
837
|
-
targetId: entry.target,
|
|
838
|
-
filePath: toDisplayPath(entry.location.filePath, graph.cwd),
|
|
839
|
-
line: entry.location.line,
|
|
840
|
-
column: entry.location.column,
|
|
841
|
-
node: serializeReactUsageNode(entry.target, graph, filter, /* @__PURE__ */ new Set())
|
|
842
|
-
};
|
|
843
|
-
}
|
|
844
722
|
function compareReactNodeIds(leftId, rightId, nodes) {
|
|
845
723
|
const left = nodes.get(leftId);
|
|
846
724
|
const right = nodes.get(rightId);
|
|
@@ -850,20 +728,138 @@ function compareReactNodeIds(leftId, rightId, nodes) {
|
|
|
850
728
|
function compareReactUsageEntries(left, right, nodes) {
|
|
851
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);
|
|
852
730
|
}
|
|
853
|
-
function comparePendingReactUsageEntries(left, right) {
|
|
854
|
-
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);
|
|
855
|
-
}
|
|
856
731
|
function compareReactNodes(left, right) {
|
|
857
732
|
return left.filePath.localeCompare(right.filePath) || left.name.localeCompare(right.name) || left.kind.localeCompare(right.kind);
|
|
858
733
|
}
|
|
859
|
-
|
|
860
|
-
|
|
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
|
+
});
|
|
861
832
|
}
|
|
862
|
-
function
|
|
863
|
-
|
|
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));
|
|
864
849
|
}
|
|
865
|
-
function
|
|
866
|
-
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;
|
|
867
863
|
}
|
|
868
864
|
//#endregion
|
|
869
865
|
//#region src/color.ts
|
|
@@ -889,7 +885,72 @@ function formatReactSymbolLabel(name, kind, enabled) {
|
|
|
889
885
|
return `${kind === "component" ? ANSI_COMPONENT : ANSI_HOOK}${label}${ANSI_RESET}`;
|
|
890
886
|
}
|
|
891
887
|
//#endregion
|
|
892
|
-
//#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
|
|
893
954
|
function printReactUsageTree(graph, options = {}) {
|
|
894
955
|
const cwd = options.cwd ?? graph.cwd;
|
|
895
956
|
const color = resolveColorSupport(options.color);
|
|
@@ -948,65 +1009,105 @@ function formatReactEntryLabel(entry, cwd) {
|
|
|
948
1009
|
return `${toDisplayPath(entry.location.filePath, cwd)}:${entry.location.line}:${entry.location.column}`;
|
|
949
1010
|
}
|
|
950
1011
|
//#endregion
|
|
951
|
-
//#region src/
|
|
952
|
-
function
|
|
953
|
-
const
|
|
954
|
-
|
|
955
|
-
const includeExternals = options.includeExternals ?? false;
|
|
956
|
-
const omitUnused = options.omitUnused ?? false;
|
|
957
|
-
const rootLines = [toDisplayPath(graph.entryId, cwd)];
|
|
958
|
-
const visited = new Set([graph.entryId]);
|
|
959
|
-
const entryNode = graph.nodes.get(graph.entryId);
|
|
960
|
-
if (entryNode === void 0) return rootLines.join("\n");
|
|
961
|
-
const rootDependencies = filterDependencies(entryNode.dependencies, includeExternals, omitUnused);
|
|
962
|
-
rootDependencies.forEach((dependency, index) => {
|
|
963
|
-
const lines = renderDependency(dependency, graph, visited, "", index === rootDependencies.length - 1, includeExternals, omitUnused, color, cwd);
|
|
964
|
-
rootLines.push(...lines);
|
|
965
|
-
});
|
|
966
|
-
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);
|
|
967
1016
|
}
|
|
968
|
-
function
|
|
969
|
-
const
|
|
970
|
-
const
|
|
971
|
-
if (
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
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
|
+
};
|
|
983
1049
|
});
|
|
984
|
-
return
|
|
1050
|
+
return {
|
|
1051
|
+
path: displayPath,
|
|
1052
|
+
kind: filePath === graph.entryId ? "entry" : "source",
|
|
1053
|
+
dependencies
|
|
1054
|
+
};
|
|
985
1055
|
}
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
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
|
+
};
|
|
992
1067
|
}
|
|
993
|
-
function
|
|
994
|
-
const
|
|
995
|
-
if (
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
if (
|
|
1004
|
-
|
|
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
|
+
};
|
|
1005
1100
|
}
|
|
1006
|
-
function
|
|
1007
|
-
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
|
+
};
|
|
1008
1109
|
}
|
|
1009
1110
|
//#endregion
|
|
1010
|
-
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 };
|
|
1011
1112
|
|
|
1012
|
-
//# sourceMappingURL=
|
|
1113
|
+
//# sourceMappingURL=react-CWIt8V0Y.mjs.map
|