@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,331 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join, relative, dirname, basename } from 'node:path';
|
|
3
|
+
import type Parser from 'tree-sitter';
|
|
4
|
+
import type { GraphNode, GraphEdge } from '../../graph/schema.js';
|
|
5
|
+
import { nodeId, findAll } from './types.js';
|
|
6
|
+
import type { ExtractionResult } from './types.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Route extractor — detects React Router and Next.js routing.
|
|
10
|
+
*
|
|
11
|
+
* React Router: <Route path="/" element={<Dashboard />} />
|
|
12
|
+
* Next.js App Router: src/app/dashboard/page.tsx → /dashboard
|
|
13
|
+
* Next.js Pages Router: pages/dashboard.tsx → /dashboard
|
|
14
|
+
* Next.js API Routes: src/app/api/auth/route.ts → /api/auth
|
|
15
|
+
*/
|
|
16
|
+
export function extractRoutes(
|
|
17
|
+
tree: Parser.Tree,
|
|
18
|
+
filePath: string,
|
|
19
|
+
sourceCode: string,
|
|
20
|
+
existingNodes: GraphNode[],
|
|
21
|
+
): ExtractionResult {
|
|
22
|
+
const nodes: GraphNode[] = [];
|
|
23
|
+
const edges: GraphEdge[] = [];
|
|
24
|
+
|
|
25
|
+
// Detect React Router <Route> components in JSX
|
|
26
|
+
extractReactRouterRoutes(tree.rootNode, filePath, existingNodes, nodes, edges);
|
|
27
|
+
|
|
28
|
+
// Detect Next.js file-based routing
|
|
29
|
+
extractNextjsRoute(filePath, sourceCode, tree.rootNode, existingNodes, nodes, edges);
|
|
30
|
+
|
|
31
|
+
return { nodes, edges };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Detect file-based routes for Next.js.
|
|
36
|
+
* Runs on every file, but only creates Route nodes for page.tsx, route.ts, etc.
|
|
37
|
+
*/
|
|
38
|
+
function extractNextjsRoute(
|
|
39
|
+
filePath: string,
|
|
40
|
+
sourceCode: string,
|
|
41
|
+
root: Parser.SyntaxNode,
|
|
42
|
+
existingNodes: GraphNode[],
|
|
43
|
+
nodes: GraphNode[],
|
|
44
|
+
edges: GraphEdge[],
|
|
45
|
+
): void {
|
|
46
|
+
const fileName = basename(filePath);
|
|
47
|
+
const dir = dirname(filePath);
|
|
48
|
+
|
|
49
|
+
// App Router: page.tsx → page route
|
|
50
|
+
if (/^page\.(tsx?|jsx?)$/.test(fileName)) {
|
|
51
|
+
const routePath = appDirToRoute(dir);
|
|
52
|
+
if (!routePath) return;
|
|
53
|
+
|
|
54
|
+
const routeNode: GraphNode = {
|
|
55
|
+
id: nodeId(filePath, `route:${routePath}`),
|
|
56
|
+
kind: 'Route',
|
|
57
|
+
name: routePath,
|
|
58
|
+
file: filePath,
|
|
59
|
+
line: 1,
|
|
60
|
+
exportType: 'none',
|
|
61
|
+
meta: { framework: 'next-app' },
|
|
62
|
+
};
|
|
63
|
+
nodes.push(routeNode);
|
|
64
|
+
|
|
65
|
+
// Link to the default exported component
|
|
66
|
+
const component = existingNodes.find(
|
|
67
|
+
n => n.kind === 'Component' && n.file === filePath && n.exportType === 'default'
|
|
68
|
+
);
|
|
69
|
+
if (component) {
|
|
70
|
+
edges.push({
|
|
71
|
+
source: routeNode.id,
|
|
72
|
+
target: component.id,
|
|
73
|
+
kind: 'renders',
|
|
74
|
+
meta: { role: 'page' },
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// App Router: layout.tsx → layout
|
|
81
|
+
if (/^layout\.(tsx?|jsx?)$/.test(fileName)) {
|
|
82
|
+
const routePath = appDirToRoute(dir);
|
|
83
|
+
if (!routePath) return;
|
|
84
|
+
|
|
85
|
+
const layoutNode: GraphNode = {
|
|
86
|
+
id: nodeId(filePath, `layout:${routePath}`),
|
|
87
|
+
kind: 'Route',
|
|
88
|
+
name: `layout:${routePath}`,
|
|
89
|
+
file: filePath,
|
|
90
|
+
line: 1,
|
|
91
|
+
exportType: 'none',
|
|
92
|
+
meta: { framework: 'next-app', role: 'layout' },
|
|
93
|
+
};
|
|
94
|
+
nodes.push(layoutNode);
|
|
95
|
+
|
|
96
|
+
const component = existingNodes.find(
|
|
97
|
+
n => n.kind === 'Component' && n.file === filePath && n.exportType === 'default'
|
|
98
|
+
);
|
|
99
|
+
if (component) {
|
|
100
|
+
edges.push({
|
|
101
|
+
source: layoutNode.id,
|
|
102
|
+
target: component.id,
|
|
103
|
+
kind: 'renders',
|
|
104
|
+
meta: { role: 'layout' },
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// App Router: route.ts → API endpoint
|
|
111
|
+
if (/^route\.(ts|js)$/.test(fileName)) {
|
|
112
|
+
const routePath = appDirToRoute(dir);
|
|
113
|
+
if (!routePath) return;
|
|
114
|
+
|
|
115
|
+
// Find exported handler functions (GET, POST, PUT, DELETE, PATCH)
|
|
116
|
+
const httpMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];
|
|
117
|
+
for (let i = 0; i < root.childCount; i++) {
|
|
118
|
+
const child = root.child(i)!;
|
|
119
|
+
if (child.type === 'export_statement') {
|
|
120
|
+
const funcDecl = findDirectChild(child, 'function_declaration');
|
|
121
|
+
const lexDecl = findDirectChild(child, 'lexical_declaration');
|
|
122
|
+
|
|
123
|
+
const funcName = funcDecl?.childForFieldName('name')?.text
|
|
124
|
+
?? lexDecl?.child(0)?.childForFieldName?.('name')?.text;
|
|
125
|
+
|
|
126
|
+
if (funcName && httpMethods.includes(funcName)) {
|
|
127
|
+
const endpointNode: GraphNode = {
|
|
128
|
+
id: nodeId(filePath, `${funcName}:${routePath}`),
|
|
129
|
+
kind: 'ApiEndpoint',
|
|
130
|
+
name: `${funcName} ${routePath}`,
|
|
131
|
+
file: filePath,
|
|
132
|
+
line: child.startPosition.row + 1,
|
|
133
|
+
exportType: 'named',
|
|
134
|
+
meta: { method: funcName, path: routePath, framework: 'next-app' },
|
|
135
|
+
};
|
|
136
|
+
nodes.push(endpointNode);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Pages Router: pages/dashboard.tsx → /dashboard
|
|
144
|
+
if (isNextPagesFile(filePath)) {
|
|
145
|
+
const routePath = pagesToRoute(filePath);
|
|
146
|
+
if (!routePath) return;
|
|
147
|
+
|
|
148
|
+
const routeNode: GraphNode = {
|
|
149
|
+
id: nodeId(filePath, `route:${routePath}`),
|
|
150
|
+
kind: 'Route',
|
|
151
|
+
name: routePath,
|
|
152
|
+
file: filePath,
|
|
153
|
+
line: 1,
|
|
154
|
+
exportType: 'none',
|
|
155
|
+
meta: { framework: 'next-pages' },
|
|
156
|
+
};
|
|
157
|
+
nodes.push(routeNode);
|
|
158
|
+
|
|
159
|
+
const component = existingNodes.find(
|
|
160
|
+
n => n.kind === 'Component' && n.file === filePath && n.exportType === 'default'
|
|
161
|
+
);
|
|
162
|
+
if (component) {
|
|
163
|
+
edges.push({
|
|
164
|
+
source: routeNode.id,
|
|
165
|
+
target: component.id,
|
|
166
|
+
kind: 'renders',
|
|
167
|
+
meta: { role: 'page' },
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Detect React Router <Route> elements in JSX.
|
|
175
|
+
*/
|
|
176
|
+
function extractReactRouterRoutes(
|
|
177
|
+
root: Parser.SyntaxNode,
|
|
178
|
+
filePath: string,
|
|
179
|
+
existingNodes: GraphNode[],
|
|
180
|
+
nodes: GraphNode[],
|
|
181
|
+
edges: GraphEdge[],
|
|
182
|
+
): void {
|
|
183
|
+
const jsxElements = [
|
|
184
|
+
...findAll(root, 'jsx_element'),
|
|
185
|
+
...findAll(root, 'jsx_self_closing_element'),
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
for (const jsx of jsxElements) {
|
|
189
|
+
const tagName = getJSXTagName(jsx);
|
|
190
|
+
if (tagName !== 'Route') continue;
|
|
191
|
+
|
|
192
|
+
// Extract path and element attributes
|
|
193
|
+
const attrs = getJSXAttributes(jsx);
|
|
194
|
+
const path = attrs.get('path');
|
|
195
|
+
if (!path) continue;
|
|
196
|
+
|
|
197
|
+
const routeNode: GraphNode = {
|
|
198
|
+
id: nodeId(filePath, `route:${path}`),
|
|
199
|
+
kind: 'Route',
|
|
200
|
+
name: path,
|
|
201
|
+
file: filePath,
|
|
202
|
+
line: jsx.startPosition.row + 1,
|
|
203
|
+
exportType: 'none',
|
|
204
|
+
meta: { framework: 'react-router' },
|
|
205
|
+
};
|
|
206
|
+
nodes.push(routeNode);
|
|
207
|
+
|
|
208
|
+
// Link to the rendered component via element prop
|
|
209
|
+
const elementAttr = attrs.get('element');
|
|
210
|
+
if (elementAttr) {
|
|
211
|
+
// element={<Dashboard />} — extract component name
|
|
212
|
+
const match = elementAttr.match(/<(\w+)/);
|
|
213
|
+
if (match) {
|
|
214
|
+
const componentName = match[1]!;
|
|
215
|
+
const component = existingNodes.find(
|
|
216
|
+
n => n.kind === 'Component' && n.name === componentName
|
|
217
|
+
);
|
|
218
|
+
if (component) {
|
|
219
|
+
edges.push({
|
|
220
|
+
source: routeNode.id,
|
|
221
|
+
target: component.id,
|
|
222
|
+
kind: 'renders',
|
|
223
|
+
meta: { role: 'page' },
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Link to the rendered component via component prop (react-router v5)
|
|
230
|
+
const componentProp = attrs.get('component');
|
|
231
|
+
if (componentProp) {
|
|
232
|
+
const component = existingNodes.find(
|
|
233
|
+
n => n.kind === 'Component' && n.name === componentProp
|
|
234
|
+
);
|
|
235
|
+
if (component) {
|
|
236
|
+
edges.push({
|
|
237
|
+
source: routeNode.id,
|
|
238
|
+
target: component.id,
|
|
239
|
+
kind: 'renders',
|
|
240
|
+
meta: { role: 'page' },
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ---- Helpers ----
|
|
248
|
+
|
|
249
|
+
function appDirToRoute(dir: string): string | null {
|
|
250
|
+
// Convert src/app/dashboard/settings → /dashboard/settings
|
|
251
|
+
const appMatch = dir.match(/(?:src\/)?app(\/.*)?$/);
|
|
252
|
+
if (!appMatch) return null;
|
|
253
|
+
|
|
254
|
+
const routePart = appMatch[1] ?? '';
|
|
255
|
+
// Convert [param] to :param, [...slug] to *slug, (group) segments removed
|
|
256
|
+
const route = routePart
|
|
257
|
+
.split('/')
|
|
258
|
+
.filter(s => !s.startsWith('(') || !s.endsWith(')')) // remove route groups
|
|
259
|
+
.map(s => {
|
|
260
|
+
if (s.startsWith('[...') && s.endsWith(']')) return `*${s.slice(4, -1)}`;
|
|
261
|
+
if (s.startsWith('[') && s.endsWith(']')) return `:${s.slice(1, -1)}`;
|
|
262
|
+
return s;
|
|
263
|
+
})
|
|
264
|
+
.join('/');
|
|
265
|
+
|
|
266
|
+
return route || '/';
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function isNextPagesFile(filePath: string): boolean {
|
|
270
|
+
return filePath.startsWith('pages/') || filePath.includes('/pages/');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function pagesToRoute(filePath: string): string | null {
|
|
274
|
+
const match = filePath.match(/pages(\/.*)\.(tsx?|jsx?)$/);
|
|
275
|
+
if (!match) return null;
|
|
276
|
+
|
|
277
|
+
let route = match[1]!;
|
|
278
|
+
// Remove /index suffix
|
|
279
|
+
route = route.replace(/\/index$/, '') || '/';
|
|
280
|
+
// Convert [param] to :param
|
|
281
|
+
route = route.replace(/\[\.\.\.(\w+)\]/g, '*$1');
|
|
282
|
+
route = route.replace(/\[(\w+)\]/g, ':$1');
|
|
283
|
+
|
|
284
|
+
return route;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function getJSXTagName(node: Parser.SyntaxNode): string | null {
|
|
288
|
+
if (node.type === 'jsx_self_closing_element') {
|
|
289
|
+
const nameNode = node.childForFieldName('name') ?? node.child(1);
|
|
290
|
+
return nameNode?.text ?? null;
|
|
291
|
+
}
|
|
292
|
+
if (node.type === 'jsx_element') {
|
|
293
|
+
const opening = findDirectChild(node, 'jsx_opening_element');
|
|
294
|
+
if (!opening) return null;
|
|
295
|
+
const nameNode = opening.childForFieldName('name') ?? opening.child(1);
|
|
296
|
+
return nameNode?.text ?? null;
|
|
297
|
+
}
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function getJSXAttributes(node: Parser.SyntaxNode): Map<string, string> {
|
|
302
|
+
const attrs = new Map<string, string>();
|
|
303
|
+
const attrParent = node.type === 'jsx_self_closing_element'
|
|
304
|
+
? node
|
|
305
|
+
: findDirectChild(node, 'jsx_opening_element');
|
|
306
|
+
|
|
307
|
+
if (!attrParent) return attrs;
|
|
308
|
+
|
|
309
|
+
for (let i = 0; i < attrParent.childCount; i++) {
|
|
310
|
+
const child = attrParent.child(i)!;
|
|
311
|
+
if (child.type === 'jsx_attribute') {
|
|
312
|
+
const nameNode = findDirectChild(child, 'property_identifier');
|
|
313
|
+
if (!nameNode) continue;
|
|
314
|
+
|
|
315
|
+
const valueNode = child.child(child.childCount - 1);
|
|
316
|
+
if (valueNode) {
|
|
317
|
+
const value = valueNode.text.replace(/^['"{]|['"}]$/g, '');
|
|
318
|
+
attrs.set(nameNode.text, value);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return attrs;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function findDirectChild(node: Parser.SyntaxNode, type: string): Parser.SyntaxNode | null {
|
|
327
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
328
|
+
if (node.child(i)!.type === type) return node.child(i)!;
|
|
329
|
+
}
|
|
330
|
+
return null;
|
|
331
|
+
}
|