@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.
- package/README.md +319 -0
- package/bun.lock +527 -0
- package/dist/cli/components/IndexProgress.d.ts +18 -0
- package/dist/cli/components/IndexProgress.d.ts.map +1 -0
- package/dist/cli/components/IndexProgress.js +26 -0
- package/dist/cli/components/IndexProgress.js.map +1 -0
- package/dist/cli/components/InitResult.d.ts +7 -0
- package/dist/cli/components/InitResult.d.ts.map +1 -0
- package/dist/cli/components/InitResult.js +6 -0
- package/dist/cli/components/InitResult.js.map +1 -0
- package/dist/cli/index-cmd.d.ts +7 -0
- package/dist/cli/index-cmd.d.ts.map +1 -0
- package/dist/cli/index-cmd.js +28 -0
- package/dist/cli/index-cmd.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +81 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +8 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +77 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/serve.d.ts +2 -0
- package/dist/cli/serve.d.ts.map +1 -0
- package/dist/cli/serve.js +28 -0
- package/dist/cli/serve.js.map +1 -0
- package/dist/cli/unused.d.ts +2 -0
- package/dist/cli/unused.d.ts.map +1 -0
- package/dist/cli/unused.js +56 -0
- package/dist/cli/unused.js.map +1 -0
- package/dist/graph/graph.d.ts +30 -0
- package/dist/graph/graph.d.ts.map +1 -0
- package/dist/graph/graph.js +166 -0
- package/dist/graph/graph.js.map +1 -0
- package/dist/graph/index.d.ts +5 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +5 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/graph/schema.d.ts +33 -0
- package/dist/graph/schema.d.ts.map +1 -0
- package/dist/graph/schema.js +3 -0
- package/dist/graph/schema.js.map +1 -0
- package/dist/graph/serialize.d.ts +7 -0
- package/dist/graph/serialize.d.ts.map +1 -0
- package/dist/graph/serialize.js +39 -0
- package/dist/graph/serialize.js.map +1 -0
- package/dist/graph/traverse.d.ts +14 -0
- package/dist/graph/traverse.d.ts.map +1 -0
- package/dist/graph/traverse.js +50 -0
- package/dist/graph/traverse.js.map +1 -0
- package/dist/mcp/formatter.d.ts +26 -0
- package/dist/mcp/formatter.d.ts.map +1 -0
- package/dist/mcp/formatter.js +691 -0
- package/dist/mcp/formatter.js.map +1 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +45 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +9 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +136 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/output/ai-context.d.ts +7 -0
- package/dist/output/ai-context.d.ts.map +1 -0
- package/dist/output/ai-context.js +26 -0
- package/dist/output/ai-context.js.map +1 -0
- package/dist/parser/extractors/api-calls.d.ts +15 -0
- package/dist/parser/extractors/api-calls.d.ts.map +1 -0
- package/dist/parser/extractors/api-calls.js +168 -0
- package/dist/parser/extractors/api-calls.js.map +1 -0
- package/dist/parser/extractors/components.d.ts +5 -0
- package/dist/parser/extractors/components.d.ts.map +1 -0
- package/dist/parser/extractors/components.js +236 -0
- package/dist/parser/extractors/components.js.map +1 -0
- package/dist/parser/extractors/context.d.ts +14 -0
- package/dist/parser/extractors/context.d.ts.map +1 -0
- package/dist/parser/extractors/context.js +196 -0
- package/dist/parser/extractors/context.js.map +1 -0
- package/dist/parser/extractors/effects.d.ts +14 -0
- package/dist/parser/extractors/effects.d.ts.map +1 -0
- package/dist/parser/extractors/effects.js +175 -0
- package/dist/parser/extractors/effects.js.map +1 -0
- package/dist/parser/extractors/hooks.d.ts +5 -0
- package/dist/parser/extractors/hooks.d.ts.map +1 -0
- package/dist/parser/extractors/hooks.js +242 -0
- package/dist/parser/extractors/hooks.js.map +1 -0
- package/dist/parser/extractors/imports.d.ts +6 -0
- package/dist/parser/extractors/imports.d.ts.map +1 -0
- package/dist/parser/extractors/imports.js +148 -0
- package/dist/parser/extractors/imports.js.map +1 -0
- package/dist/parser/extractors/index.d.ts +12 -0
- package/dist/parser/extractors/index.d.ts.map +1 -0
- package/dist/parser/extractors/index.js +11 -0
- package/dist/parser/extractors/index.js.map +1 -0
- package/dist/parser/extractors/jsx-tree.d.ts +5 -0
- package/dist/parser/extractors/jsx-tree.d.ts.map +1 -0
- package/dist/parser/extractors/jsx-tree.js +226 -0
- package/dist/parser/extractors/jsx-tree.js.map +1 -0
- package/dist/parser/extractors/routes.d.ts +13 -0
- package/dist/parser/extractors/routes.d.ts.map +1 -0
- package/dist/parser/extractors/routes.js +275 -0
- package/dist/parser/extractors/routes.js.map +1 -0
- package/dist/parser/extractors/state.d.ts +14 -0
- package/dist/parser/extractors/state.d.ts.map +1 -0
- package/dist/parser/extractors/state.js +368 -0
- package/dist/parser/extractors/state.js.map +1 -0
- package/dist/parser/extractors/types.d.ts +22 -0
- package/dist/parser/extractors/types.d.ts.map +1 -0
- package/dist/parser/extractors/types.js +51 -0
- package/dist/parser/extractors/types.js.map +1 -0
- package/dist/parser/indexer.d.ts +14 -0
- package/dist/parser/indexer.d.ts.map +1 -0
- package/dist/parser/indexer.js +167 -0
- package/dist/parser/indexer.js.map +1 -0
- package/dist/parser/pipeline.d.ts +16 -0
- package/dist/parser/pipeline.d.ts.map +1 -0
- package/dist/parser/pipeline.js +63 -0
- package/dist/parser/pipeline.js.map +1 -0
- package/dist/parser/setup.d.ts +4 -0
- package/dist/parser/setup.d.ts.map +1 -0
- package/dist/parser/setup.js +29 -0
- package/dist/parser/setup.js.map +1 -0
- package/dist/parser/walker.d.ts +6 -0
- package/dist/parser/walker.d.ts.map +1 -0
- package/dist/parser/walker.js +45 -0
- package/dist/parser/walker.js.map +1 -0
- package/dist/watcher.d.ts +12 -0
- package/dist/watcher.d.ts.map +1 -0
- package/dist/watcher.js +72 -0
- package/dist/watcher.js.map +1 -0
- package/package.json +51 -0
- package/src/cli/components/IndexProgress.tsx +79 -0
- package/src/cli/components/InitResult.tsx +28 -0
- package/src/cli/index-cmd.ts +41 -0
- package/src/cli/index.ts +92 -0
- package/src/cli/init.ts +97 -0
- package/src/cli/serve.ts +29 -0
- package/src/cli/unused.ts +88 -0
- package/src/graph/graph.ts +179 -0
- package/src/graph/index.ts +4 -0
- package/src/graph/schema.ts +68 -0
- package/src/graph/serialize.ts +40 -0
- package/src/graph/traverse.ts +66 -0
- package/src/mcp/formatter.ts +757 -0
- package/src/mcp/server.ts +59 -0
- package/src/mcp/tools.ts +154 -0
- package/src/output/ai-context.ts +29 -0
- package/src/parser/extractors/api-calls.ts +192 -0
- package/src/parser/extractors/components.ts +273 -0
- package/src/parser/extractors/context.ts +216 -0
- package/src/parser/extractors/effects.ts +205 -0
- package/src/parser/extractors/hooks.ts +268 -0
- package/src/parser/extractors/imports.ts +192 -0
- package/src/parser/extractors/index.ts +11 -0
- package/src/parser/extractors/jsx-tree.ts +271 -0
- package/src/parser/extractors/routes.ts +331 -0
- package/src/parser/extractors/state.ts +392 -0
- package/src/parser/extractors/types.ts +71 -0
- package/src/parser/indexer.ts +197 -0
- package/src/parser/pipeline.ts +89 -0
- package/src/parser/setup.ts +33 -0
- package/src/parser/walker.ts +61 -0
- package/src/watcher.ts +91 -0
- package/templates/CLAUDE.md +7 -0
- package/tests/extractors.test.ts +164 -0
- package/tests/fixtures/basic/src/App.tsx +12 -0
- package/tests/fixtures/basic/src/components/Dashboard.tsx +24 -0
- package/tests/fixtures/basic/src/components/MetricsCard.tsx +15 -0
- package/tests/fixtures/basic/src/components/Sidebar.tsx +20 -0
- package/tests/fixtures/basic/src/contexts/ThemeContext.tsx +16 -0
- package/tests/fixtures/basic/src/hooks/useAuth.ts +25 -0
- package/tests/fixtures/basic/src/stores/authStore.ts +15 -0
- package/tests/fixtures/basic/src/utils.ts +7 -0
- package/tests/graph.test.ts +91 -0
- package/tests/phase2.test.ts +309 -0
- package/tests/smoke.test.ts +77 -0
- package/tsconfig.json +20 -0
- 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
|
+
}
|