@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,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
+ }