@reactgraph/cli 0.1.1

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.
Files changed (178) hide show
  1. package/README.md +319 -0
  2. package/bun.lock +527 -0
  3. package/dist/cli/components/IndexProgress.d.ts +18 -0
  4. package/dist/cli/components/IndexProgress.d.ts.map +1 -0
  5. package/dist/cli/components/IndexProgress.js +26 -0
  6. package/dist/cli/components/IndexProgress.js.map +1 -0
  7. package/dist/cli/components/InitResult.d.ts +7 -0
  8. package/dist/cli/components/InitResult.d.ts.map +1 -0
  9. package/dist/cli/components/InitResult.js +6 -0
  10. package/dist/cli/components/InitResult.js.map +1 -0
  11. package/dist/cli/index-cmd.d.ts +7 -0
  12. package/dist/cli/index-cmd.d.ts.map +1 -0
  13. package/dist/cli/index-cmd.js +28 -0
  14. package/dist/cli/index-cmd.js.map +1 -0
  15. package/dist/cli/index.d.ts +3 -0
  16. package/dist/cli/index.d.ts.map +1 -0
  17. package/dist/cli/index.js +81 -0
  18. package/dist/cli/index.js.map +1 -0
  19. package/dist/cli/init.d.ts +8 -0
  20. package/dist/cli/init.d.ts.map +1 -0
  21. package/dist/cli/init.js +77 -0
  22. package/dist/cli/init.js.map +1 -0
  23. package/dist/cli/serve.d.ts +2 -0
  24. package/dist/cli/serve.d.ts.map +1 -0
  25. package/dist/cli/serve.js +28 -0
  26. package/dist/cli/serve.js.map +1 -0
  27. package/dist/cli/unused.d.ts +2 -0
  28. package/dist/cli/unused.d.ts.map +1 -0
  29. package/dist/cli/unused.js +56 -0
  30. package/dist/cli/unused.js.map +1 -0
  31. package/dist/graph/graph.d.ts +30 -0
  32. package/dist/graph/graph.d.ts.map +1 -0
  33. package/dist/graph/graph.js +166 -0
  34. package/dist/graph/graph.js.map +1 -0
  35. package/dist/graph/index.d.ts +5 -0
  36. package/dist/graph/index.d.ts.map +1 -0
  37. package/dist/graph/index.js +5 -0
  38. package/dist/graph/index.js.map +1 -0
  39. package/dist/graph/schema.d.ts +33 -0
  40. package/dist/graph/schema.d.ts.map +1 -0
  41. package/dist/graph/schema.js +3 -0
  42. package/dist/graph/schema.js.map +1 -0
  43. package/dist/graph/serialize.d.ts +7 -0
  44. package/dist/graph/serialize.d.ts.map +1 -0
  45. package/dist/graph/serialize.js +39 -0
  46. package/dist/graph/serialize.js.map +1 -0
  47. package/dist/graph/traverse.d.ts +14 -0
  48. package/dist/graph/traverse.d.ts.map +1 -0
  49. package/dist/graph/traverse.js +50 -0
  50. package/dist/graph/traverse.js.map +1 -0
  51. package/dist/mcp/formatter.d.ts +26 -0
  52. package/dist/mcp/formatter.d.ts.map +1 -0
  53. package/dist/mcp/formatter.js +691 -0
  54. package/dist/mcp/formatter.js.map +1 -0
  55. package/dist/mcp/server.d.ts +2 -0
  56. package/dist/mcp/server.d.ts.map +1 -0
  57. package/dist/mcp/server.js +45 -0
  58. package/dist/mcp/server.js.map +1 -0
  59. package/dist/mcp/tools.d.ts +9 -0
  60. package/dist/mcp/tools.d.ts.map +1 -0
  61. package/dist/mcp/tools.js +136 -0
  62. package/dist/mcp/tools.js.map +1 -0
  63. package/dist/output/ai-context.d.ts +7 -0
  64. package/dist/output/ai-context.d.ts.map +1 -0
  65. package/dist/output/ai-context.js +26 -0
  66. package/dist/output/ai-context.js.map +1 -0
  67. package/dist/parser/extractors/api-calls.d.ts +15 -0
  68. package/dist/parser/extractors/api-calls.d.ts.map +1 -0
  69. package/dist/parser/extractors/api-calls.js +168 -0
  70. package/dist/parser/extractors/api-calls.js.map +1 -0
  71. package/dist/parser/extractors/components.d.ts +5 -0
  72. package/dist/parser/extractors/components.d.ts.map +1 -0
  73. package/dist/parser/extractors/components.js +236 -0
  74. package/dist/parser/extractors/components.js.map +1 -0
  75. package/dist/parser/extractors/context.d.ts +14 -0
  76. package/dist/parser/extractors/context.d.ts.map +1 -0
  77. package/dist/parser/extractors/context.js +196 -0
  78. package/dist/parser/extractors/context.js.map +1 -0
  79. package/dist/parser/extractors/effects.d.ts +14 -0
  80. package/dist/parser/extractors/effects.d.ts.map +1 -0
  81. package/dist/parser/extractors/effects.js +175 -0
  82. package/dist/parser/extractors/effects.js.map +1 -0
  83. package/dist/parser/extractors/hooks.d.ts +5 -0
  84. package/dist/parser/extractors/hooks.d.ts.map +1 -0
  85. package/dist/parser/extractors/hooks.js +242 -0
  86. package/dist/parser/extractors/hooks.js.map +1 -0
  87. package/dist/parser/extractors/imports.d.ts +6 -0
  88. package/dist/parser/extractors/imports.d.ts.map +1 -0
  89. package/dist/parser/extractors/imports.js +148 -0
  90. package/dist/parser/extractors/imports.js.map +1 -0
  91. package/dist/parser/extractors/index.d.ts +12 -0
  92. package/dist/parser/extractors/index.d.ts.map +1 -0
  93. package/dist/parser/extractors/index.js +11 -0
  94. package/dist/parser/extractors/index.js.map +1 -0
  95. package/dist/parser/extractors/jsx-tree.d.ts +5 -0
  96. package/dist/parser/extractors/jsx-tree.d.ts.map +1 -0
  97. package/dist/parser/extractors/jsx-tree.js +226 -0
  98. package/dist/parser/extractors/jsx-tree.js.map +1 -0
  99. package/dist/parser/extractors/routes.d.ts +13 -0
  100. package/dist/parser/extractors/routes.d.ts.map +1 -0
  101. package/dist/parser/extractors/routes.js +275 -0
  102. package/dist/parser/extractors/routes.js.map +1 -0
  103. package/dist/parser/extractors/state.d.ts +14 -0
  104. package/dist/parser/extractors/state.d.ts.map +1 -0
  105. package/dist/parser/extractors/state.js +368 -0
  106. package/dist/parser/extractors/state.js.map +1 -0
  107. package/dist/parser/extractors/types.d.ts +22 -0
  108. package/dist/parser/extractors/types.d.ts.map +1 -0
  109. package/dist/parser/extractors/types.js +51 -0
  110. package/dist/parser/extractors/types.js.map +1 -0
  111. package/dist/parser/indexer.d.ts +14 -0
  112. package/dist/parser/indexer.d.ts.map +1 -0
  113. package/dist/parser/indexer.js +167 -0
  114. package/dist/parser/indexer.js.map +1 -0
  115. package/dist/parser/pipeline.d.ts +16 -0
  116. package/dist/parser/pipeline.d.ts.map +1 -0
  117. package/dist/parser/pipeline.js +63 -0
  118. package/dist/parser/pipeline.js.map +1 -0
  119. package/dist/parser/setup.d.ts +4 -0
  120. package/dist/parser/setup.d.ts.map +1 -0
  121. package/dist/parser/setup.js +29 -0
  122. package/dist/parser/setup.js.map +1 -0
  123. package/dist/parser/walker.d.ts +6 -0
  124. package/dist/parser/walker.d.ts.map +1 -0
  125. package/dist/parser/walker.js +45 -0
  126. package/dist/parser/walker.js.map +1 -0
  127. package/dist/watcher.d.ts +12 -0
  128. package/dist/watcher.d.ts.map +1 -0
  129. package/dist/watcher.js +72 -0
  130. package/dist/watcher.js.map +1 -0
  131. package/package.json +51 -0
  132. package/src/cli/components/IndexProgress.tsx +79 -0
  133. package/src/cli/components/InitResult.tsx +28 -0
  134. package/src/cli/index-cmd.ts +41 -0
  135. package/src/cli/index.ts +92 -0
  136. package/src/cli/init.ts +97 -0
  137. package/src/cli/serve.ts +29 -0
  138. package/src/cli/unused.ts +88 -0
  139. package/src/graph/graph.ts +179 -0
  140. package/src/graph/index.ts +4 -0
  141. package/src/graph/schema.ts +68 -0
  142. package/src/graph/serialize.ts +40 -0
  143. package/src/graph/traverse.ts +66 -0
  144. package/src/mcp/formatter.ts +757 -0
  145. package/src/mcp/server.ts +59 -0
  146. package/src/mcp/tools.ts +154 -0
  147. package/src/output/ai-context.ts +29 -0
  148. package/src/parser/extractors/api-calls.ts +192 -0
  149. package/src/parser/extractors/components.ts +273 -0
  150. package/src/parser/extractors/context.ts +216 -0
  151. package/src/parser/extractors/effects.ts +205 -0
  152. package/src/parser/extractors/hooks.ts +268 -0
  153. package/src/parser/extractors/imports.ts +192 -0
  154. package/src/parser/extractors/index.ts +11 -0
  155. package/src/parser/extractors/jsx-tree.ts +271 -0
  156. package/src/parser/extractors/routes.ts +331 -0
  157. package/src/parser/extractors/state.ts +392 -0
  158. package/src/parser/extractors/types.ts +71 -0
  159. package/src/parser/indexer.ts +197 -0
  160. package/src/parser/pipeline.ts +89 -0
  161. package/src/parser/setup.ts +33 -0
  162. package/src/parser/walker.ts +61 -0
  163. package/src/watcher.ts +91 -0
  164. package/templates/CLAUDE.md +7 -0
  165. package/tests/extractors.test.ts +164 -0
  166. package/tests/fixtures/basic/src/App.tsx +12 -0
  167. package/tests/fixtures/basic/src/components/Dashboard.tsx +24 -0
  168. package/tests/fixtures/basic/src/components/MetricsCard.tsx +15 -0
  169. package/tests/fixtures/basic/src/components/Sidebar.tsx +20 -0
  170. package/tests/fixtures/basic/src/contexts/ThemeContext.tsx +16 -0
  171. package/tests/fixtures/basic/src/hooks/useAuth.ts +25 -0
  172. package/tests/fixtures/basic/src/stores/authStore.ts +15 -0
  173. package/tests/fixtures/basic/src/utils.ts +7 -0
  174. package/tests/graph.test.ts +91 -0
  175. package/tests/phase2.test.ts +309 -0
  176. package/tests/smoke.test.ts +77 -0
  177. package/tsconfig.json +20 -0
  178. package/vitest.config.ts +8 -0
@@ -0,0 +1,192 @@
1
+ import { resolve, dirname, extname } from 'node:path';
2
+ import { existsSync } from 'node:fs';
3
+ import type Parser from 'tree-sitter';
4
+ import type { GraphNode, GraphEdge } from '../../graph/schema.js';
5
+ import { nodeId } from './types.js';
6
+ import type { ExtractionResult } from './types.js';
7
+
8
+ const TRY_EXTENSIONS = ['.tsx', '.ts', '.jsx', '.js', '/index.tsx', '/index.ts', '/index.jsx', '/index.js'];
9
+
10
+ export function extractImports(
11
+ tree: Parser.Tree,
12
+ filePath: string,
13
+ sourceCode: string,
14
+ _existingNodes: GraphNode[],
15
+ projectDir: string,
16
+ ): ExtractionResult {
17
+ const nodes: GraphNode[] = [];
18
+ const edges: GraphEdge[] = [];
19
+
20
+ // Create a Module node for this file
21
+ const moduleNode: GraphNode = {
22
+ id: nodeId(filePath, basename(filePath)),
23
+ kind: 'Module',
24
+ name: basename(filePath),
25
+ file: filePath,
26
+ line: 1,
27
+ exportType: 'none',
28
+ meta: {},
29
+ };
30
+ nodes.push(moduleNode);
31
+
32
+ const root = tree.rootNode;
33
+
34
+ for (let i = 0; i < root.childCount; i++) {
35
+ const child = root.child(i)!;
36
+
37
+ if (child.type === 'import_statement') {
38
+ processImport(child, filePath, projectDir, moduleNode.id, edges);
39
+ }
40
+
41
+ // Re-exports: export { X } from './Y'
42
+ if (child.type === 'export_statement') {
43
+ const source = child.childForFieldName('source');
44
+ if (source) {
45
+ processReexport(child, source, filePath, projectDir, moduleNode.id, edges);
46
+ }
47
+ }
48
+ }
49
+
50
+ return { nodes, edges };
51
+ }
52
+
53
+ function processImport(
54
+ node: Parser.SyntaxNode,
55
+ filePath: string,
56
+ projectDir: string,
57
+ moduleId: string,
58
+ edges: GraphEdge[],
59
+ ): void {
60
+ // Get the source string (the "from './path'" part)
61
+ const sourceNode = node.childForFieldName('source') ?? findChildByType(node, 'string');
62
+ if (!sourceNode) return;
63
+
64
+ const importPath = stripQuotes(sourceNode.text);
65
+
66
+ // Skip side-effect imports (import './styles.css')
67
+ const importClause = findChildByType(node, 'import_clause');
68
+ if (!importClause) return;
69
+
70
+ const resolvedPath = resolveImportPath(importPath, filePath, projectDir);
71
+ const names: string[] = [];
72
+ let isDefault = false;
73
+
74
+ for (let i = 0; i < importClause.childCount; i++) {
75
+ const child = importClause.child(i)!;
76
+
77
+ if (child.type === 'identifier') {
78
+ // Default import
79
+ names.push(child.text);
80
+ isDefault = true;
81
+ } else if (child.type === 'named_imports') {
82
+ // Named imports { A, B, C as D }
83
+ for (let j = 0; j < child.childCount; j++) {
84
+ const spec = child.child(j)!;
85
+ if (spec.type === 'import_specifier') {
86
+ const nameNode = spec.childForFieldName('name') ?? spec.child(0);
87
+ if (nameNode) names.push(nameNode.text);
88
+ }
89
+ }
90
+ } else if (child.type === 'namespace_import') {
91
+ // import * as X from ...
92
+ const id = findChildByType(child, 'identifier');
93
+ if (id) names.push(`* as ${id.text}`);
94
+ }
95
+ }
96
+
97
+ if (names.length > 0) {
98
+ edges.push({
99
+ source: moduleId,
100
+ target: resolvedPath,
101
+ kind: 'imports',
102
+ meta: { names, isDefault, importPath },
103
+ });
104
+ }
105
+ }
106
+
107
+ function processReexport(
108
+ node: Parser.SyntaxNode,
109
+ source: Parser.SyntaxNode,
110
+ filePath: string,
111
+ projectDir: string,
112
+ moduleId: string,
113
+ edges: GraphEdge[],
114
+ ): void {
115
+ const importPath = stripQuotes(source.text);
116
+ const resolvedPath = resolveImportPath(importPath, filePath, projectDir);
117
+ const names: string[] = [];
118
+
119
+ const exportClause = findChildByType(node, 'export_clause');
120
+ if (exportClause) {
121
+ for (let i = 0; i < exportClause.childCount; i++) {
122
+ const spec = exportClause.child(i)!;
123
+ if (spec.type === 'export_specifier') {
124
+ const nameNode = spec.childForFieldName('name') ?? spec.child(0);
125
+ if (nameNode) names.push(nameNode.text);
126
+ }
127
+ }
128
+ }
129
+
130
+ if (names.length > 0) {
131
+ edges.push({
132
+ source: moduleId,
133
+ target: resolvedPath,
134
+ kind: 'imports',
135
+ meta: { names, isReexport: true, importPath },
136
+ });
137
+ }
138
+ }
139
+
140
+ export function resolveImportPath(
141
+ importPath: string,
142
+ currentFile: string,
143
+ projectDir: string,
144
+ ): string {
145
+ // Relative import
146
+ if (importPath.startsWith('.')) {
147
+ const dir = dirname(resolve(projectDir, currentFile));
148
+ const base = resolve(dir, importPath);
149
+
150
+ // If it already has an extension and exists
151
+ if (extname(base) && existsSync(base)) {
152
+ return toRelative(base, projectDir);
153
+ }
154
+
155
+ // Try extensions
156
+ for (const ext of TRY_EXTENSIONS) {
157
+ const candidate = base + ext;
158
+ if (existsSync(candidate)) {
159
+ return toRelative(candidate, projectDir);
160
+ }
161
+ }
162
+
163
+ // Return best guess even if not found
164
+ return toRelative(base, projectDir);
165
+ }
166
+
167
+ // External package
168
+ return `external:${importPath}`;
169
+ }
170
+
171
+ function toRelative(absPath: string, projectDir: string): string {
172
+ const rel = absPath.startsWith(projectDir)
173
+ ? absPath.slice(projectDir.length + 1)
174
+ : absPath;
175
+ return rel.replace(/\\/g, '/');
176
+ }
177
+
178
+ function basename(filePath: string): string {
179
+ const parts = filePath.split('/');
180
+ return parts[parts.length - 1]!;
181
+ }
182
+
183
+ function stripQuotes(s: string): string {
184
+ return s.replace(/^['"`]|['"`]$/g, '');
185
+ }
186
+
187
+ function findChildByType(node: Parser.SyntaxNode, type: string): Parser.SyntaxNode | null {
188
+ for (let i = 0; i < node.childCount; i++) {
189
+ if (node.child(i)!.type === type) return node.child(i)!;
190
+ }
191
+ return null;
192
+ }
@@ -0,0 +1,11 @@
1
+ export { extractImports } from './imports.js';
2
+ export { extractComponents } from './components.js';
3
+ export { extractHooks } from './hooks.js';
4
+ export { extractJSXTree } from './jsx-tree.js';
5
+ export { extractContext } from './context.js';
6
+ export { extractState } from './state.js';
7
+ export { extractApiCalls } from './api-calls.js';
8
+ export { extractRoutes } from './routes.js';
9
+ export { extractEffects } from './effects.js';
10
+ export type { Extractor, ExtractionResult } from './types.js';
11
+ export { nodeId } from './types.js';
@@ -0,0 +1,271 @@
1
+ import type Parser from 'tree-sitter';
2
+ import type { GraphNode, GraphEdge } from '../../graph/schema.js';
3
+ import { isPascalCase, findAll } from './types.js';
4
+ import type { ExtractionResult } from './types.js';
5
+
6
+ export function extractJSXTree(
7
+ tree: Parser.Tree,
8
+ filePath: string,
9
+ sourceCode: string,
10
+ existingNodes: GraphNode[],
11
+ ): ExtractionResult {
12
+ const edges: GraphEdge[] = [];
13
+
14
+ // Get components defined in this file
15
+ const components = existingNodes.filter(n => n.kind === 'Component' && n.file === filePath);
16
+
17
+ // Build a map of imported names → their import source for resolving JSX tags
18
+ const importMap = buildImportMap(tree.rootNode);
19
+
20
+ for (const component of components) {
21
+ // Find the function body for this component
22
+ const funcNode = findComponentFunction(tree.rootNode, component.name);
23
+ if (!funcNode) continue;
24
+
25
+ const body = funcNode.childForFieldName('body');
26
+ if (!body) continue;
27
+
28
+ // Find all JSX elements in the body
29
+ walkJSXElements(body, component, filePath, importMap, existingNodes, edges);
30
+ }
31
+
32
+ return { nodes: [], edges };
33
+ }
34
+
35
+ function walkJSXElements(
36
+ node: Parser.SyntaxNode,
37
+ parentComponent: GraphNode,
38
+ filePath: string,
39
+ importMap: Map<string, string>,
40
+ existingNodes: GraphNode[],
41
+ edges: GraphEdge[],
42
+ ): void {
43
+ for (let i = 0; i < node.childCount; i++) {
44
+ const child = node.child(i)!;
45
+
46
+ if (child.type === 'jsx_element' || child.type === 'jsx_self_closing_element') {
47
+ const tagName = getJSXTagName(child);
48
+
49
+ if (tagName && isPascalCase(tagName)) {
50
+ const targetId = resolveComponentRef(tagName, filePath, importMap, existingNodes);
51
+
52
+ // Add renders edge (dedup)
53
+ const alreadyHas = edges.some(
54
+ e => e.source === parentComponent.id && e.target === targetId && e.kind === 'renders'
55
+ );
56
+ if (!alreadyHas) {
57
+ edges.push({
58
+ source: parentComponent.id,
59
+ target: targetId,
60
+ kind: 'renders',
61
+ meta: {},
62
+ });
63
+ }
64
+
65
+ // Extract props being passed
66
+ const propsEdges = extractPassedProps(child, parentComponent.id, targetId);
67
+ edges.push(...propsEdges);
68
+ }
69
+
70
+ // Recurse into children regardless
71
+ walkJSXElements(child, parentComponent, filePath, importMap, existingNodes, edges);
72
+ } else if (child.type === 'jsx_expression') {
73
+ // Handle {items.map(item => <Card />)} patterns
74
+ walkJSXElements(child, parentComponent, filePath, importMap, existingNodes, edges);
75
+ } else {
76
+ walkJSXElements(child, parentComponent, filePath, importMap, existingNodes, edges);
77
+ }
78
+ }
79
+ }
80
+
81
+ function getJSXTagName(node: Parser.SyntaxNode): string | null {
82
+ if (node.type === 'jsx_self_closing_element') {
83
+ const nameNode = node.childForFieldName('name') ?? node.child(1);
84
+ return nameNode?.text ?? null;
85
+ }
86
+
87
+ if (node.type === 'jsx_element') {
88
+ const opening = findDirectChild(node, 'jsx_opening_element');
89
+ if (!opening) return null;
90
+ const nameNode = opening.childForFieldName('name') ?? opening.child(1);
91
+ return nameNode?.text ?? null;
92
+ }
93
+
94
+ return null;
95
+ }
96
+
97
+ function extractPassedProps(
98
+ jsxNode: Parser.SyntaxNode,
99
+ sourceId: string,
100
+ targetId: string,
101
+ ): GraphEdge[] {
102
+ const edges: GraphEdge[] = [];
103
+
104
+ // Find all jsx_attribute nodes
105
+ const attrParent = jsxNode.type === 'jsx_self_closing_element'
106
+ ? jsxNode
107
+ : findDirectChild(jsxNode, 'jsx_opening_element');
108
+
109
+ if (!attrParent) return edges;
110
+
111
+ for (let i = 0; i < attrParent.childCount; i++) {
112
+ const child = attrParent.child(i)!;
113
+
114
+ if (child.type === 'jsx_attribute') {
115
+ const nameNode = findDirectChild(child, 'property_identifier');
116
+ if (!nameNode) continue;
117
+
118
+ const propName = nameNode.text;
119
+ if (propName === 'key') continue; // Skip React internal prop
120
+
121
+ const valueNode = child.child(child.childCount - 1);
122
+ const valueType = classifyPropValue(valueNode);
123
+
124
+ edges.push({
125
+ source: sourceId,
126
+ target: targetId,
127
+ kind: 'passes_prop',
128
+ meta: { prop: propName, valueType },
129
+ });
130
+ }
131
+ }
132
+
133
+ return edges;
134
+ }
135
+
136
+ function classifyPropValue(node: Parser.SyntaxNode | null): string {
137
+ if (!node) return 'true'; // boolean shorthand
138
+ if (node.type === 'string') return 'string';
139
+ if (node.type === 'jsx_expression') {
140
+ const inner = node.child(1); // skip {
141
+ if (!inner) return 'expression';
142
+ if (inner.type === 'arrow_function' || inner.type === 'function_expression') return 'function';
143
+ if (inner.type === 'number') return 'number';
144
+ if (inner.type === 'true' || inner.type === 'false') return 'boolean';
145
+ return 'expression';
146
+ }
147
+ return 'unknown';
148
+ }
149
+
150
+ function resolveComponentRef(
151
+ tagName: string,
152
+ filePath: string,
153
+ importMap: Map<string, string>,
154
+ existingNodes: GraphNode[],
155
+ ): string {
156
+ // Same file first
157
+ for (const n of existingNodes) {
158
+ if (n.kind === 'Component' && n.name === tagName && n.file === filePath) {
159
+ return n.id;
160
+ }
161
+ }
162
+
163
+ // Find by name across all known components (import resolution handled by pipeline)
164
+ for (const n of existingNodes) {
165
+ if (n.kind === 'Component' && n.name === tagName) {
166
+ return n.id;
167
+ }
168
+ }
169
+
170
+ return `unresolved:${tagName}`;
171
+ }
172
+
173
+ function buildImportMap(root: Parser.SyntaxNode): Map<string, string> {
174
+ const map = new Map<string, string>();
175
+
176
+ for (let i = 0; i < root.childCount; i++) {
177
+ const child = root.child(i)!;
178
+ if (child.type !== 'import_statement') continue;
179
+
180
+ const sourceNode = child.childForFieldName('source') ?? findDirectChild(child, 'string');
181
+ if (!sourceNode) continue;
182
+
183
+ const source = sourceNode.text.replace(/^['"`]|['"`]$/g, '');
184
+
185
+ const importClause = findDirectChild(child, 'import_clause');
186
+ if (!importClause) continue;
187
+
188
+ for (let j = 0; j < importClause.childCount; j++) {
189
+ const clause = importClause.child(j)!;
190
+
191
+ if (clause.type === 'identifier') {
192
+ // default import
193
+ map.set(clause.text, source);
194
+ } else if (clause.type === 'named_imports') {
195
+ for (let k = 0; k < clause.childCount; k++) {
196
+ const spec = clause.child(k)!;
197
+ if (spec.type === 'import_specifier') {
198
+ const alias = spec.childForFieldName('alias');
199
+ const name = spec.childForFieldName('name') ?? spec.child(0);
200
+ const localName = alias?.text ?? name?.text;
201
+ if (localName) map.set(localName, source);
202
+ }
203
+ }
204
+ }
205
+ }
206
+ }
207
+
208
+ return map;
209
+ }
210
+
211
+ function findComponentFunction(root: Parser.SyntaxNode, name: string): Parser.SyntaxNode | null {
212
+ for (let i = 0; i < root.childCount; i++) {
213
+ const child = root.child(i)!;
214
+
215
+ // function Foo() { ... }
216
+ if (child.type === 'function_declaration') {
217
+ if (child.childForFieldName('name')?.text === name) return child;
218
+ }
219
+
220
+ // export function Foo() { ... } or export default function Foo() { ... }
221
+ if (child.type === 'export_statement') {
222
+ const funcDecl = findDirectChild(child, 'function_declaration');
223
+ if (funcDecl?.childForFieldName('name')?.text === name) return funcDecl;
224
+
225
+ const lexDecl = findDirectChild(child, 'lexical_declaration');
226
+ if (lexDecl) {
227
+ const fn = extractArrowFromLexical(lexDecl, name);
228
+ if (fn) return fn;
229
+ }
230
+ }
231
+
232
+ // const Foo = () => ...
233
+ if (child.type === 'lexical_declaration') {
234
+ const fn = extractArrowFromLexical(child, name);
235
+ if (fn) return fn;
236
+ }
237
+ }
238
+
239
+ return null;
240
+ }
241
+
242
+ function extractArrowFromLexical(node: Parser.SyntaxNode, name: string): Parser.SyntaxNode | null {
243
+ const declarator = findDirectChild(node, 'variable_declarator');
244
+ if (!declarator) return null;
245
+ if (declarator.childForFieldName('name')?.text !== name) return null;
246
+
247
+ const value = declarator.childForFieldName('value');
248
+ if (!value) return null;
249
+
250
+ if (value.type === 'arrow_function' || value.type === 'function_expression') return value;
251
+
252
+ // memo/forwardRef wrapper
253
+ if (value.type === 'call_expression') {
254
+ const args = value.childForFieldName('arguments');
255
+ if (args) {
256
+ for (let i = 0; i < args.childCount; i++) {
257
+ const child = args.child(i)!;
258
+ if (child.type === 'arrow_function' || child.type === 'function_expression') return child;
259
+ }
260
+ }
261
+ }
262
+
263
+ return null;
264
+ }
265
+
266
+ function findDirectChild(node: Parser.SyntaxNode, type: string): Parser.SyntaxNode | null {
267
+ for (let i = 0; i < node.childCount; i++) {
268
+ if (node.child(i)!.type === type) return node.child(i)!;
269
+ }
270
+ return null;
271
+ }