foresthouse 1.0.0-dev.7 → 1.0.0-dev.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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/path-utils.ts
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/analyzer.ts
76
- const BUILTIN_MODULES = new Set(builtinModules.flatMap((name) => [name, `node:${name}`]));
77
- function analyzeDependencies(entryFile, options = {}) {
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
- function createEdge(reference, kind, target) {
206
- return {
207
- specifier: reference.specifier,
208
- referenceKind: reference.referenceKind,
209
- isTypeOnly: reference.isTypeOnly,
210
- unused: reference.unused,
211
- kind,
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/react-analyzer.ts
315
- const FUNCTION_NODE_TYPES = new Set([
316
- "FunctionDeclaration",
317
- "FunctionExpression",
318
- "ArrowFunctionExpression",
319
- "TSDeclareFunction",
320
- "TSEmptyBodyFunctionExpression"
321
- ]);
322
- function analyzeReactUsage(entryFile, options = {}) {
323
- const dependencyGraph = analyzeDependencies(entryFile, options);
324
- const reachableFiles = new Set([dependencyGraph.entryId, ...dependencyGraph.nodes.keys()]);
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
- const node = nodes.get(symbol.id);
372
- if (node === void 0) continue;
373
- const sortedUsages = [...usages.values()].sort((left, right) => compareReactNodeIds(left.target, right.target, nodes));
374
- nodes.set(symbol.id, {
375
- ...node,
376
- usages: sortedUsages
208
+ return;
209
+ }
210
+ references.set(key, {
211
+ specifier,
212
+ referenceKind,
213
+ isTypeOnly,
214
+ unused
377
215
  });
378
216
  }
379
- const entriesByKey = /* @__PURE__ */ new Map();
380
- for (const fileAnalysis of fileAnalyses.values()) for (const entry of fileAnalysis.entryUsages) {
381
- const targetId = resolveReactReference(fileAnalysis, fileAnalyses, entry.referenceName, entry.kind);
382
- if (targetId === void 0) continue;
383
- const key = `${entry.location.filePath}:${entry.location.line}:${entry.location.column}:${targetId}`;
384
- entriesByKey.set(key, {
385
- target: targetId,
386
- location: entry.location
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
- const entries = [...entriesByKey.values()].sort((left, right) => compareReactUsageEntries(left, right, nodes));
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
- function getReactUsageRoots(graph, filter = "all") {
414
- const entries = getReactUsageEntries(graph, filter);
415
- if (entries.length > 0) return [...new Set(entries.map((entry) => entry.target))];
416
- const filteredNodes = getFilteredReactUsageNodes(graph, filter);
417
- const inboundCounts = /* @__PURE__ */ new Map();
418
- filteredNodes.forEach((node) => {
419
- inboundCounts.set(node.id, 0);
420
- });
421
- filteredNodes.forEach((node) => {
422
- getFilteredUsages(node, graph, filter).forEach((usage) => {
423
- inboundCounts.set(usage.target, (inboundCounts.get(usage.target) ?? 0) + 1);
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 getFilteredUsages(node, graph, filter = "all") {
431
- return node.usages.filter((usage) => {
432
- const targetNode = graph.nodes.get(usage.target);
433
- return targetNode !== void 0 && matchesReactFilter(targetNode, filter);
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 analyzeReactFile(program, filePath, sourceText, includeNestedRenderEntries, sourceDependencies) {
437
- const symbolsByName = /* @__PURE__ */ new Map();
438
- program.body.forEach((statement) => {
439
- collectTopLevelReactSymbols(statement, filePath, symbolsByName);
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
- const importsByLocalName = /* @__PURE__ */ new Map();
442
- const exportsByName = /* @__PURE__ */ new Map();
443
- const entryUsages = collectEntryUsages(program, filePath, sourceText, includeNestedRenderEntries);
444
- program.body.forEach((statement) => {
445
- collectImportsAndExports(statement, sourceDependencies, symbolsByName, importsByLocalName, exportsByName);
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
- symbolsByName.forEach((symbol) => {
448
- analyzeSymbolUsages(symbol);
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
- filePath,
452
- importsByLocalName,
453
- exportsByName,
454
- entryUsages,
455
- symbolsById: new Map([...symbolsByName.values()].map((symbol) => [symbol.id, symbol])),
456
- symbolsByName
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
- function collectImportsAndExports(statement, sourceDependencies, symbolsByName, importsByLocalName, exportsByName) {
589
- switch (statement.type) {
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
- function classifyReactSymbol(name, declaration) {
686
- if (isHookName(name)) return "hook";
687
- if (isComponentName(name) && returnsReactElement(declaration)) return "component";
688
- }
689
- function returnsReactElement(declaration) {
690
- if (declaration.type === "ArrowFunctionExpression" && declaration.expression) return containsReactElementLikeExpression(declaration.body);
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
- return found;
699
- }
700
- function containsReactElementLikeExpression(expression) {
701
- let found = false;
702
- walkNode(expression, (node) => {
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
- return found;
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
- function walkChild(value, visit, allowNestedFunctions) {
756
- if (Array.isArray(value)) {
757
- value.forEach((entry) => {
758
- walkChild(entry, visit, allowNestedFunctions);
759
- });
760
- return;
761
- }
762
- if (!isNode(value)) return;
763
- if (!allowNestedFunctions && FUNCTION_NODE_TYPES.has(value.type)) return;
764
- walkNode(value, visit, false);
765
- }
766
- function isNode(value) {
767
- return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
768
- }
683
+ //#endregion
684
+ //#region src/analyzers/react/references.ts
769
685
  function resolveReactReference(fileAnalysis, fileAnalyses, name, kind) {
770
686
  const localSymbol = fileAnalysis.symbolsByName.get(name);
771
687
  if (localSymbol !== void 0 && localSymbol.kind === kind) return localSymbol.id;
@@ -778,6 +694,14 @@ function resolveReactReference(fileAnalysis, fileAnalyses, name, kind) {
778
694
  if (targetId === void 0) return;
779
695
  return sourceFileAnalysis.symbolsById.get(targetId)?.kind === kind ? targetId : void 0;
780
696
  }
697
+ function addExternalHookNodes(fileAnalyses, nodes) {
698
+ for (const fileAnalysis of fileAnalyses.values()) fileAnalysis.importsByLocalName.forEach((binding, localName) => {
699
+ if (binding.sourcePath !== void 0) return;
700
+ if (!isHookName(localName) && !isHookName(binding.importedName)) return;
701
+ const externalNode = createExternalHookNode(binding, localName);
702
+ if (!nodes.has(externalNode.id)) nodes.set(externalNode.id, externalNode);
703
+ });
704
+ }
781
705
  function createExternalHookNode(binding, localName) {
782
706
  const name = getExternalHookName(binding, localName);
783
707
  return {
@@ -795,54 +719,6 @@ function getExternalHookNodeId(binding, localName) {
795
719
  function getExternalHookName(binding, localName) {
796
720
  return binding.importedName === "default" ? localName : binding.importedName;
797
721
  }
798
- function getFilteredReactUsageNodes(graph, filter) {
799
- return [...graph.nodes.values()].filter((node) => matchesReactFilter(node, filter)).sort((left, right) => compareReactNodes(left, right));
800
- }
801
- function matchesReactFilter(node, filter) {
802
- return filter === "all" || node.kind === filter;
803
- }
804
- function serializeReactUsageNode(nodeId, graph, filter, visited) {
805
- const node = graph.nodes.get(nodeId);
806
- if (node === void 0) return {
807
- id: nodeId,
808
- name: nodeId,
809
- symbolKind: "circular",
810
- filePath: "",
811
- exportNames: [],
812
- usages: []
813
- };
814
- if (visited.has(nodeId)) return {
815
- id: node.id,
816
- name: node.name,
817
- symbolKind: "circular",
818
- filePath: toDisplayPath(node.filePath, graph.cwd),
819
- exportNames: node.exportNames,
820
- usages: []
821
- };
822
- const nextVisited = new Set(visited);
823
- nextVisited.add(nodeId);
824
- return {
825
- id: node.id,
826
- name: node.name,
827
- symbolKind: node.kind,
828
- filePath: toDisplayPath(node.filePath, graph.cwd),
829
- exportNames: node.exportNames,
830
- usages: getFilteredUsages(node, graph, filter).map((usage) => ({
831
- kind: usage.kind,
832
- targetId: usage.target,
833
- node: serializeReactUsageNode(usage.target, graph, filter, nextVisited)
834
- }))
835
- };
836
- }
837
- function serializeReactUsageEntry(entry, graph, filter) {
838
- return {
839
- targetId: entry.target,
840
- filePath: toDisplayPath(entry.location.filePath, graph.cwd),
841
- line: entry.location.line,
842
- column: entry.location.column,
843
- node: serializeReactUsageNode(entry.target, graph, filter, /* @__PURE__ */ new Set())
844
- };
845
- }
846
722
  function compareReactNodeIds(leftId, rightId, nodes) {
847
723
  const left = nodes.get(leftId);
848
724
  const right = nodes.get(rightId);
@@ -852,20 +728,138 @@ function compareReactNodeIds(leftId, rightId, nodes) {
852
728
  function compareReactUsageEntries(left, right, nodes) {
853
729
  return left.location.filePath.localeCompare(right.location.filePath) || left.location.line - right.location.line || left.location.column - right.location.column || compareReactNodeIds(left.target, right.target, nodes);
854
730
  }
855
- function comparePendingReactUsageEntries(left, right) {
856
- return left.location.filePath.localeCompare(right.location.filePath) || left.location.line - right.location.line || left.location.column - right.location.column || left.kind.localeCompare(right.kind) || left.referenceName.localeCompare(right.referenceName);
857
- }
858
731
  function compareReactNodes(left, right) {
859
732
  return left.filePath.localeCompare(right.filePath) || left.name.localeCompare(right.name) || left.kind.localeCompare(right.kind);
860
733
  }
861
- function toModuleExportName(name) {
862
- return name.type === "Literal" ? name.value : name.name;
734
+ //#endregion
735
+ //#region src/analyzers/react/index.ts
736
+ function analyzeReactUsage(entryFile, options = {}) {
737
+ return new ReactAnalyzer(entryFile, options).analyze();
738
+ }
739
+ var ReactAnalyzer = class extends BaseAnalyzer {
740
+ doAnalyze() {
741
+ const dependencyGraph = analyzeDependencies(this.entryFile, this.options);
742
+ const fileAnalyses = this.collectFileAnalyses(dependencyGraph);
743
+ const nodes = this.createNodes(fileAnalyses);
744
+ this.attachUsages(fileAnalyses, nodes);
745
+ const entries = this.collectEntries(fileAnalyses, nodes);
746
+ return {
747
+ cwd: dependencyGraph.cwd,
748
+ entryId: dependencyGraph.entryId,
749
+ nodes,
750
+ entries
751
+ };
752
+ }
753
+ collectFileAnalyses(dependencyGraph) {
754
+ const reachableFiles = new Set([dependencyGraph.entryId, ...dependencyGraph.nodes.keys()]);
755
+ const fileAnalyses = /* @__PURE__ */ new Map();
756
+ for (const filePath of [...reachableFiles].sort()) {
757
+ if (!isSourceCodeFile(filePath) || filePath.endsWith(".d.ts")) continue;
758
+ const sourceText = fs.readFileSync(filePath, "utf8");
759
+ const parseResult = parseSync(filePath, sourceText, {
760
+ astType: "ts",
761
+ sourceType: "unambiguous"
762
+ });
763
+ const dependencyNode = dependencyGraph.nodes.get(filePath);
764
+ const sourceDependencies = /* @__PURE__ */ new Map();
765
+ dependencyNode?.dependencies.forEach((dependency) => {
766
+ if (dependency.kind === "source") sourceDependencies.set(dependency.specifier, dependency.target);
767
+ });
768
+ fileAnalyses.set(filePath, analyzeReactFile(parseResult.program, filePath, sourceText, filePath === dependencyGraph.entryId, sourceDependencies));
769
+ }
770
+ return fileAnalyses;
771
+ }
772
+ createNodes(fileAnalyses) {
773
+ const nodes = /* @__PURE__ */ new Map();
774
+ for (const fileAnalysis of fileAnalyses.values()) for (const symbol of fileAnalysis.symbolsById.values()) nodes.set(symbol.id, {
775
+ id: symbol.id,
776
+ name: symbol.name,
777
+ kind: symbol.kind,
778
+ filePath: symbol.filePath,
779
+ exportNames: [...symbol.exportNames].sort(),
780
+ usages: []
781
+ });
782
+ addExternalHookNodes(fileAnalyses, nodes);
783
+ return nodes;
784
+ }
785
+ attachUsages(fileAnalyses, nodes) {
786
+ for (const fileAnalysis of fileAnalyses.values()) for (const symbol of fileAnalysis.symbolsById.values()) {
787
+ const usages = /* @__PURE__ */ new Map();
788
+ symbol.componentReferences.forEach((referenceName) => {
789
+ const targetId = resolveReactReference(fileAnalysis, fileAnalyses, referenceName, "component");
790
+ if (targetId !== void 0 && targetId !== symbol.id) usages.set(`render:${targetId}`, {
791
+ kind: "render",
792
+ target: targetId
793
+ });
794
+ });
795
+ symbol.hookReferences.forEach((referenceName) => {
796
+ const targetId = resolveReactReference(fileAnalysis, fileAnalyses, referenceName, "hook");
797
+ if (targetId !== void 0 && targetId !== symbol.id) usages.set(`hook:${targetId}`, {
798
+ kind: "hook-call",
799
+ target: targetId
800
+ });
801
+ });
802
+ const node = nodes.get(symbol.id);
803
+ if (node === void 0) continue;
804
+ const sortedUsages = [...usages.values()].sort((left, right) => compareReactNodeIds(left.target, right.target, nodes));
805
+ nodes.set(symbol.id, {
806
+ ...node,
807
+ usages: sortedUsages
808
+ });
809
+ }
810
+ }
811
+ collectEntries(fileAnalyses, nodes) {
812
+ const entriesByKey = /* @__PURE__ */ new Map();
813
+ for (const fileAnalysis of fileAnalyses.values()) for (const entry of fileAnalysis.entryUsages) {
814
+ const targetId = resolveReactReference(fileAnalysis, fileAnalyses, entry.referenceName, entry.kind);
815
+ if (targetId === void 0) continue;
816
+ const key = `${entry.location.filePath}:${entry.location.line}:${entry.location.column}:${targetId}`;
817
+ entriesByKey.set(key, {
818
+ target: targetId,
819
+ location: entry.location
820
+ });
821
+ }
822
+ return [...entriesByKey.values()].sort((left, right) => compareReactUsageEntries(left, right, nodes));
823
+ }
824
+ };
825
+ //#endregion
826
+ //#region src/analyzers/react/queries.ts
827
+ function getReactUsageEntries(graph, filter = "all") {
828
+ return graph.entries.filter((entry) => {
829
+ const targetNode = graph.nodes.get(entry.target);
830
+ return targetNode !== void 0 && matchesReactFilter(targetNode, filter);
831
+ });
863
832
  }
864
- function isHookName(name) {
865
- return /^use[A-Z0-9]/.test(name);
833
+ function getReactUsageRoots(graph, filter = "all") {
834
+ const entries = getReactUsageEntries(graph, filter);
835
+ if (entries.length > 0) return [...new Set(entries.map((entry) => entry.target))];
836
+ const filteredNodes = getFilteredReactUsageNodes(graph, filter);
837
+ const inboundCounts = /* @__PURE__ */ new Map();
838
+ filteredNodes.forEach((node) => {
839
+ inboundCounts.set(node.id, 0);
840
+ });
841
+ filteredNodes.forEach((node) => {
842
+ getFilteredUsages(node, graph, filter).forEach((usage) => {
843
+ inboundCounts.set(usage.target, (inboundCounts.get(usage.target) ?? 0) + 1);
844
+ });
845
+ });
846
+ const roots = filteredNodes.filter((node) => (inboundCounts.get(node.id) ?? 0) === 0).map((node) => node.id);
847
+ if (roots.length > 0) return roots.sort((left, right) => compareReactNodeIds(left, right, graph.nodes));
848
+ return filteredNodes.map((node) => node.id).sort((left, right) => compareReactNodeIds(left, right, graph.nodes));
866
849
  }
867
- function isComponentName(name) {
868
- return /^[A-Z]/.test(name);
850
+ function getFilteredUsages(node, graph, filter = "all") {
851
+ return node.usages.filter((usage) => {
852
+ const targetNode = graph.nodes.get(usage.target);
853
+ return targetNode !== void 0 && matchesReactFilter(targetNode, filter);
854
+ });
855
+ }
856
+ function getFilteredReactUsageNodes(graph, filter) {
857
+ return [...graph.nodes.values()].filter((node) => matchesReactFilter(node, filter)).sort((left, right) => {
858
+ return left.filePath.localeCompare(right.filePath) || left.name.localeCompare(right.name) || left.kind.localeCompare(right.kind);
859
+ });
860
+ }
861
+ function matchesReactFilter(node, filter) {
862
+ return filter === "all" || node.kind === filter;
869
863
  }
870
864
  //#endregion
871
865
  //#region src/color.ts
@@ -891,7 +885,72 @@ function formatReactSymbolLabel(name, kind, enabled) {
891
885
  return `${kind === "component" ? ANSI_COMPONENT : ANSI_HOOK}${label}${ANSI_RESET}`;
892
886
  }
893
887
  //#endregion
894
- //#region src/react-tree.ts
888
+ //#region src/utils/to-display-path.ts
889
+ function toDisplayPath(filePath, cwd) {
890
+ const relativePath = path.relative(cwd, filePath);
891
+ if (relativePath === "") return ".";
892
+ const normalizedPath = relativePath.split(path.sep).join("/");
893
+ return normalizedPath.startsWith("..") ? filePath : normalizedPath;
894
+ }
895
+ //#endregion
896
+ //#region src/output/ascii/import.ts
897
+ function printDependencyTree(graph, options = {}) {
898
+ const cwd = options.cwd ?? graph.cwd;
899
+ const color = resolveColorSupport(options.color);
900
+ const includeExternals = options.includeExternals ?? false;
901
+ const omitUnused = options.omitUnused ?? false;
902
+ const rootLines = [toDisplayPath(graph.entryId, cwd)];
903
+ const visited = new Set([graph.entryId]);
904
+ const entryNode = graph.nodes.get(graph.entryId);
905
+ if (entryNode === void 0) return rootLines.join("\n");
906
+ const rootDependencies = filterDependencies(entryNode.dependencies, includeExternals, omitUnused);
907
+ rootDependencies.forEach((dependency, index) => {
908
+ rootLines.push(...renderDependency(dependency, graph, visited, "", index === rootDependencies.length - 1, includeExternals, omitUnused, color, cwd));
909
+ });
910
+ return rootLines.join("\n");
911
+ }
912
+ function renderDependency(dependency, graph, visited, prefix, isLast, includeExternals, omitUnused, color, cwd) {
913
+ const branch = `${prefix}${isLast ? "└─ " : "├─ "}`;
914
+ const label = formatDependencyLabel(dependency, cwd, color);
915
+ if (dependency.kind !== "source") return [`${branch}${label}`];
916
+ if (visited.has(dependency.target)) return [`${branch}${label} (circular)`];
917
+ const childNode = graph.nodes.get(dependency.target);
918
+ if (childNode === void 0) return [`${branch}${label}`];
919
+ const nextPrefix = `${prefix}${isLast ? " " : "│ "}`;
920
+ const nextVisited = new Set(visited);
921
+ nextVisited.add(dependency.target);
922
+ const childLines = [`${branch}${label}`];
923
+ const childDependencies = filterDependencies(childNode.dependencies, includeExternals, omitUnused);
924
+ childDependencies.forEach((childDependency, index) => {
925
+ childLines.push(...renderDependency(childDependency, graph, nextVisited, nextPrefix, index === childDependencies.length - 1, includeExternals, omitUnused, color, cwd));
926
+ });
927
+ return childLines;
928
+ }
929
+ function filterDependencies(dependencies, includeExternals, omitUnused) {
930
+ return dependencies.filter((dependency) => {
931
+ if (omitUnused && dependency.unused) return false;
932
+ if (dependency.kind === "source" || dependency.kind === "missing") return true;
933
+ return includeExternals;
934
+ });
935
+ }
936
+ function formatDependencyLabel(dependency, cwd, color) {
937
+ const prefixes = [];
938
+ if (dependency.isTypeOnly) prefixes.push("type");
939
+ if (dependency.referenceKind === "require") prefixes.push("require");
940
+ else if (dependency.referenceKind === "dynamic-import") prefixes.push("dynamic");
941
+ else if (dependency.referenceKind === "export") prefixes.push("re-export");
942
+ else if (dependency.referenceKind === "import-equals") prefixes.push("import=");
943
+ const annotation = prefixes.length > 0 ? `[${prefixes.join(", ")}] ` : "";
944
+ if (dependency.kind === "source") return colorizeUnusedMarker(withUnusedSuffix(`${annotation}${toDisplayPath(dependency.target, cwd)}`, dependency.unused), color);
945
+ if (dependency.kind === "missing") return colorizeUnusedMarker(withUnusedSuffix(`${annotation}${dependency.specifier} [missing]`, dependency.unused), color);
946
+ if (dependency.kind === "builtin") return colorizeUnusedMarker(withUnusedSuffix(`${annotation}${dependency.target} [builtin]`, dependency.unused), color);
947
+ return colorizeUnusedMarker(withUnusedSuffix(`${annotation}${dependency.target} [external]`, dependency.unused), color);
948
+ }
949
+ function withUnusedSuffix(label, unused) {
950
+ return unused ? `${label} (unused)` : label;
951
+ }
952
+ //#endregion
953
+ //#region src/output/ascii/react.ts
895
954
  function printReactUsageTree(graph, options = {}) {
896
955
  const cwd = options.cwd ?? graph.cwd;
897
956
  const color = resolveColorSupport(options.color);
@@ -950,65 +1009,105 @@ function formatReactEntryLabel(entry, cwd) {
950
1009
  return `${toDisplayPath(entry.location.filePath, cwd)}:${entry.location.line}:${entry.location.column}`;
951
1010
  }
952
1011
  //#endregion
953
- //#region src/tree.ts
954
- function printDependencyTree(graph, options = {}) {
955
- const cwd = options.cwd ?? graph.cwd;
956
- const color = resolveColorSupport(options.color);
957
- const includeExternals = options.includeExternals ?? false;
958
- const omitUnused = options.omitUnused ?? false;
959
- const rootLines = [toDisplayPath(graph.entryId, cwd)];
960
- const visited = new Set([graph.entryId]);
961
- const entryNode = graph.nodes.get(graph.entryId);
962
- if (entryNode === void 0) return rootLines.join("\n");
963
- const rootDependencies = filterDependencies(entryNode.dependencies, includeExternals, omitUnused);
964
- rootDependencies.forEach((dependency, index) => {
965
- const lines = renderDependency(dependency, graph, visited, "", index === rootDependencies.length - 1, includeExternals, omitUnused, color, cwd);
966
- rootLines.push(...lines);
967
- });
968
- return rootLines.join("\n");
1012
+ //#region src/output/json/import.ts
1013
+ function graphToSerializableTree(graph, options = {}) {
1014
+ const visited = /* @__PURE__ */ new Set();
1015
+ return serializeNode(graph.entryId, graph, visited, options.omitUnused ?? false);
969
1016
  }
970
- function renderDependency(dependency, graph, visited, prefix, isLast, includeExternals, omitUnused, color, cwd) {
971
- const branch = `${prefix}${isLast ? "└─ " : "├─ "}`;
972
- const label = formatDependencyLabel(dependency, cwd, color);
973
- if (dependency.kind !== "source") return [`${branch}${label}`];
974
- if (visited.has(dependency.target)) return [`${branch}${label} (circular)`];
975
- const childNode = graph.nodes.get(dependency.target);
976
- if (childNode === void 0) return [`${branch}${label}`];
977
- const nextPrefix = `${prefix}${isLast ? " " : "│ "}`;
978
- const nextVisited = new Set(visited);
979
- nextVisited.add(dependency.target);
980
- const childLines = [`${branch}${label}`];
981
- const childDependencies = filterDependencies(childNode.dependencies, includeExternals, omitUnused);
982
- childDependencies.forEach((childDependency, index) => {
983
- const isChildLast = index === childDependencies.length - 1;
984
- childLines.push(...renderDependency(childDependency, graph, nextVisited, nextPrefix, isChildLast, includeExternals, omitUnused, color, cwd));
1017
+ function serializeNode(filePath, graph, visited, omitUnused) {
1018
+ const node = graph.nodes.get(filePath);
1019
+ const displayPath = toDisplayPath(filePath, graph.cwd);
1020
+ if (node === void 0) return {
1021
+ path: displayPath,
1022
+ kind: "missing",
1023
+ dependencies: []
1024
+ };
1025
+ if (visited.has(filePath)) return {
1026
+ path: displayPath,
1027
+ kind: "circular",
1028
+ dependencies: []
1029
+ };
1030
+ visited.add(filePath);
1031
+ const dependencies = node.dependencies.filter((dependency) => !omitUnused || !dependency.unused).map((dependency) => {
1032
+ if (dependency.kind !== "source") return {
1033
+ specifier: dependency.specifier,
1034
+ referenceKind: dependency.referenceKind,
1035
+ isTypeOnly: dependency.isTypeOnly,
1036
+ unused: dependency.unused,
1037
+ kind: dependency.kind,
1038
+ target: dependency.kind === "missing" ? dependency.target : toDisplayPath(dependency.target, graph.cwd)
1039
+ };
1040
+ return {
1041
+ specifier: dependency.specifier,
1042
+ referenceKind: dependency.referenceKind,
1043
+ isTypeOnly: dependency.isTypeOnly,
1044
+ unused: dependency.unused,
1045
+ kind: dependency.kind,
1046
+ target: toDisplayPath(dependency.target, graph.cwd),
1047
+ node: serializeNode(dependency.target, graph, new Set(visited), omitUnused)
1048
+ };
985
1049
  });
986
- return childLines;
1050
+ return {
1051
+ path: displayPath,
1052
+ kind: filePath === graph.entryId ? "entry" : "source",
1053
+ dependencies
1054
+ };
987
1055
  }
988
- function filterDependencies(dependencies, includeExternals, omitUnused) {
989
- return dependencies.filter((dependency) => {
990
- if (omitUnused && dependency.unused) return false;
991
- if (dependency.kind === "source" || dependency.kind === "missing") return true;
992
- return includeExternals;
993
- });
1056
+ //#endregion
1057
+ //#region src/output/json/react.ts
1058
+ function graphToSerializableReactTree(graph, options = {}) {
1059
+ const filter = options.filter ?? "all";
1060
+ const entries = getReactUsageEntries(graph, filter);
1061
+ const roots = entries.length > 0 ? entries.map((entry) => serializeReactUsageNode(entry.target, graph, filter, /* @__PURE__ */ new Set())) : getReactUsageRoots(graph, filter).map((rootId) => serializeReactUsageNode(rootId, graph, filter, /* @__PURE__ */ new Set()));
1062
+ return {
1063
+ kind: "react-usage",
1064
+ entries: entries.map((entry) => serializeReactUsageEntry(entry, graph, filter)),
1065
+ roots
1066
+ };
994
1067
  }
995
- function formatDependencyLabel(dependency, cwd, color) {
996
- const prefixes = [];
997
- if (dependency.isTypeOnly) prefixes.push("type");
998
- if (dependency.referenceKind === "require") prefixes.push("require");
999
- else if (dependency.referenceKind === "dynamic-import") prefixes.push("dynamic");
1000
- else if (dependency.referenceKind === "export") prefixes.push("re-export");
1001
- else if (dependency.referenceKind === "import-equals") prefixes.push("import=");
1002
- const annotation = prefixes.length > 0 ? `[${prefixes.join(", ")}] ` : "";
1003
- if (dependency.kind === "source") return colorizeUnusedMarker(withUnusedSuffix(`${annotation}${toDisplayPath(dependency.target, cwd)}`, dependency.unused), color);
1004
- if (dependency.kind === "missing") return colorizeUnusedMarker(withUnusedSuffix(`${annotation}${dependency.specifier} [missing]`, dependency.unused), color);
1005
- if (dependency.kind === "builtin") return colorizeUnusedMarker(withUnusedSuffix(`${annotation}${dependency.target} [builtin]`, dependency.unused), color);
1006
- return colorizeUnusedMarker(withUnusedSuffix(`${annotation}${dependency.target} [external]`, dependency.unused), color);
1068
+ function serializeReactUsageNode(nodeId, graph, filter, visited) {
1069
+ const node = graph.nodes.get(nodeId);
1070
+ if (node === void 0) return {
1071
+ id: nodeId,
1072
+ name: nodeId,
1073
+ symbolKind: "circular",
1074
+ filePath: "",
1075
+ exportNames: [],
1076
+ usages: []
1077
+ };
1078
+ if (visited.has(nodeId)) return {
1079
+ id: node.id,
1080
+ name: node.name,
1081
+ symbolKind: "circular",
1082
+ filePath: toDisplayPath(node.filePath, graph.cwd),
1083
+ exportNames: node.exportNames,
1084
+ usages: []
1085
+ };
1086
+ const nextVisited = new Set(visited);
1087
+ nextVisited.add(nodeId);
1088
+ return {
1089
+ id: node.id,
1090
+ name: node.name,
1091
+ symbolKind: node.kind,
1092
+ filePath: toDisplayPath(node.filePath, graph.cwd),
1093
+ exportNames: node.exportNames,
1094
+ usages: getFilteredUsages(node, graph, filter).map((usage) => ({
1095
+ kind: usage.kind,
1096
+ targetId: usage.target,
1097
+ node: serializeReactUsageNode(usage.target, graph, filter, nextVisited)
1098
+ }))
1099
+ };
1007
1100
  }
1008
- function withUnusedSuffix(label, unused) {
1009
- return unused ? `${label} (unused)` : label;
1101
+ function serializeReactUsageEntry(entry, graph, filter) {
1102
+ return {
1103
+ targetId: entry.target,
1104
+ filePath: toDisplayPath(entry.location.filePath, graph.cwd),
1105
+ line: entry.location.line,
1106
+ column: entry.location.column,
1107
+ node: serializeReactUsageNode(entry.target, graph, filter, /* @__PURE__ */ new Set())
1108
+ };
1010
1109
  }
1011
1110
  //#endregion
1012
- export { graphToSerializableReactTree as a, getReactUsageRoots as i, printReactUsageTree as n, analyzeDependencies as o, analyzeReactUsage as r, graphToSerializableTree as s, printDependencyTree as t };
1111
+ export { getFilteredUsages as a, analyzeReactUsage as c, printDependencyTree as i, analyzeDependencies as l, graphToSerializableTree as n, getReactUsageEntries as o, printReactUsageTree as r, getReactUsageRoots as s, graphToSerializableReactTree as t };
1013
1112
 
1014
- //# sourceMappingURL=tree-BX1YFcqX.mjs.map
1113
+ //# sourceMappingURL=react-CWIt8V0Y.mjs.map