@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,309 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { join } from 'node:path';
3
+ import { indexProject } from '../src/parser/indexer.js';
4
+ import { parseFile } from '../src/parser/setup.js';
5
+ import { extractContext } from '../src/parser/extractors/context.js';
6
+ import { extractState } from '../src/parser/extractors/state.js';
7
+ import { extractApiCalls } from '../src/parser/extractors/api-calls.js';
8
+ import { extractEffects } from '../src/parser/extractors/effects.js';
9
+ import { extractRoutes } from '../src/parser/extractors/routes.js';
10
+ import { formatTraceFlow, formatImpact, formatFindNodes } from '../src/mcp/formatter.js';
11
+
12
+ const FIXTURE_DIR = join(import.meta.dirname, 'fixtures', 'basic');
13
+
14
+ describe('Context extractor', () => {
15
+ it('detects createContext and useContext', () => {
16
+ const code = `
17
+ import { createContext, useContext } from 'react';
18
+ const ThemeCtx = createContext('light');
19
+ function App() {
20
+ return <ThemeCtx.Provider value="dark"><div /></ThemeCtx.Provider>;
21
+ }
22
+ function Card() {
23
+ const theme = useContext(ThemeCtx);
24
+ return <div>{theme}</div>;
25
+ }
26
+ `;
27
+ const tree = parseFile('test.tsx', code)!;
28
+
29
+ // Need component nodes for edge sources
30
+ const existingNodes = [
31
+ { id: 'test.tsx:App', kind: 'Component' as const, name: 'App', file: 'test.tsx', line: 4, exportType: 'none' as const, meta: {} },
32
+ { id: 'test.tsx:Card', kind: 'Component' as const, name: 'Card', file: 'test.tsx', line: 8, exportType: 'none' as const, meta: {} },
33
+ ];
34
+
35
+ const { nodes, edges } = extractContext(tree, 'test.tsx', code, existingNodes);
36
+
37
+ // Should find the context
38
+ expect(nodes).toHaveLength(1);
39
+ expect(nodes[0]!.kind).toBe('Context');
40
+ expect(nodes[0]!.name).toBe('ThemeCtx');
41
+
42
+ // Should find provides edge (App provides ThemeCtx)
43
+ const provides = edges.filter(e => e.kind === 'provides');
44
+ expect(provides).toHaveLength(1);
45
+ expect(provides[0]!.source).toBe('test.tsx:App');
46
+
47
+ // Should find consumes edge (Card consumes ThemeCtx)
48
+ const consumes = edges.filter(e => e.kind === 'consumes');
49
+ expect(consumes).toHaveLength(1);
50
+ expect(consumes[0]!.source).toBe('test.tsx:Card');
51
+ });
52
+ });
53
+
54
+ describe('State extractor', () => {
55
+ it('detects zustand store creation', () => {
56
+ const code = `
57
+ import { create } from 'zustand';
58
+ const useStore = create((set) => ({
59
+ count: 0,
60
+ increment: () => set((s) => ({ count: s.count + 1 })),
61
+ }));
62
+ `;
63
+ const tree = parseFile('test.ts', code)!;
64
+ const { nodes } = extractState(tree, 'test.ts', code, []);
65
+
66
+ expect(nodes).toHaveLength(1);
67
+ expect(nodes[0]!.kind).toBe('Store');
68
+ expect(nodes[0]!.name).toBe('useStore');
69
+ expect(nodes[0]!.meta.library).toBe('zustand');
70
+ });
71
+
72
+ it('detects zustand store usage in component', () => {
73
+ const code = `
74
+ function Dashboard() {
75
+ const count = useStore((s) => s.count);
76
+ return <div>{count}</div>;
77
+ }
78
+ `;
79
+ const tree = parseFile('test.tsx', code)!;
80
+ const existingNodes = [
81
+ { id: 'test.tsx:Dashboard', kind: 'Component' as const, name: 'Dashboard', file: 'test.tsx', line: 2, exportType: 'none' as const, meta: {} },
82
+ { id: 'store.ts:useStore', kind: 'Store' as const, name: 'useStore', file: 'store.ts', line: 1, exportType: 'named' as const, meta: { library: 'zustand' } },
83
+ ];
84
+ const { edges } = extractState(tree, 'test.tsx', code, existingNodes);
85
+
86
+ const reads = edges.filter(e => e.kind === 'reads_store');
87
+ expect(reads).toHaveLength(1);
88
+ expect(reads[0]!.source).toBe('test.tsx:Dashboard');
89
+ expect(reads[0]!.target).toBe('store.ts:useStore');
90
+ });
91
+ });
92
+
93
+ describe('API calls extractor', () => {
94
+ it('detects fetch calls', () => {
95
+ const code = `
96
+ function useData() {
97
+ const data = fetch('/api/users');
98
+ return data;
99
+ }
100
+ `;
101
+ const tree = parseFile('test.ts', code)!;
102
+ const existingNodes = [
103
+ { id: 'test.ts:useData', kind: 'Hook' as const, name: 'useData', file: 'test.ts', line: 2, exportType: 'none' as const, meta: {} },
104
+ ];
105
+ const { edges } = extractApiCalls(tree, 'test.ts', code, existingNodes);
106
+
107
+ const fetches = edges.filter(e => e.kind === 'fetches');
108
+ expect(fetches).toHaveLength(1);
109
+ expect(fetches[0]!.meta.url).toBe('/api/users');
110
+ expect(fetches[0]!.meta.method).toBe('GET');
111
+ });
112
+
113
+ it('detects axios calls', () => {
114
+ const code = `
115
+ function useData() {
116
+ const data = axios.post('/api/users', { name: 'test' });
117
+ return data;
118
+ }
119
+ `;
120
+ const tree = parseFile('test.ts', code)!;
121
+ const existingNodes = [
122
+ { id: 'test.ts:useData', kind: 'Hook' as const, name: 'useData', file: 'test.ts', line: 2, exportType: 'none' as const, meta: {} },
123
+ ];
124
+ const { edges } = extractApiCalls(tree, 'test.ts', code, existingNodes);
125
+
126
+ const fetches = edges.filter(e => e.kind === 'fetches');
127
+ expect(fetches).toHaveLength(1);
128
+ expect(fetches[0]!.meta.method).toBe('POST');
129
+ expect(fetches[0]!.meta.url).toBe('/api/users');
130
+ });
131
+ });
132
+
133
+ describe('Effects extractor', () => {
134
+ it('detects useEffect with deps', () => {
135
+ const code = `
136
+ function Dashboard() {
137
+ useEffect(() => {
138
+ fetchData(userId);
139
+ }, [userId]);
140
+ return <div />;
141
+ }
142
+ `;
143
+ const tree = parseFile('test.tsx', code)!;
144
+ const node = {
145
+ id: 'test.tsx:Dashboard', kind: 'Component' as const, name: 'Dashboard',
146
+ file: 'test.tsx', line: 2, exportType: 'none' as const, meta: {} as Record<string, unknown>,
147
+ };
148
+ extractEffects(tree, 'test.tsx', code, [node]);
149
+
150
+ const effects = node.meta.effects as any[];
151
+ expect(effects).toHaveLength(1);
152
+ expect(effects[0].type).toBe('useEffect');
153
+ expect(effects[0].deps).toContain('userId');
154
+ expect(effects[0].triggers).toContain('fetchData');
155
+ });
156
+
157
+ it('detects mount-only effect', () => {
158
+ const code = `
159
+ function App() {
160
+ useEffect(() => {
161
+ init();
162
+ }, []);
163
+ return <div />;
164
+ }
165
+ `;
166
+ const tree = parseFile('test.tsx', code)!;
167
+ const node = {
168
+ id: 'test.tsx:App', kind: 'Component' as const, name: 'App',
169
+ file: 'test.tsx', line: 2, exportType: 'none' as const, meta: {} as Record<string, unknown>,
170
+ };
171
+ extractEffects(tree, 'test.tsx', code, [node]);
172
+
173
+ const effects = node.meta.effects as any[];
174
+ expect(effects).toHaveLength(1);
175
+ expect(effects[0].deps).toBe('mount-only');
176
+ });
177
+ });
178
+
179
+ describe('Routes extractor', () => {
180
+ it('detects React Router routes', () => {
181
+ const code = `
182
+ function AppRoutes() {
183
+ return (
184
+ <Routes>
185
+ <Route path="/" element={<Dashboard />} />
186
+ <Route path="/settings" element={<Settings />} />
187
+ </Routes>
188
+ );
189
+ }
190
+ `;
191
+ const tree = parseFile('test.tsx', code)!;
192
+ const existingNodes = [
193
+ { id: 'test.tsx:AppRoutes', kind: 'Component' as const, name: 'AppRoutes', file: 'test.tsx', line: 2, exportType: 'none' as const, meta: {} },
194
+ { id: 'dash.tsx:Dashboard', kind: 'Component' as const, name: 'Dashboard', file: 'dash.tsx', line: 1, exportType: 'default' as const, meta: {} },
195
+ { id: 'settings.tsx:Settings', kind: 'Component' as const, name: 'Settings', file: 'settings.tsx', line: 1, exportType: 'default' as const, meta: {} },
196
+ ];
197
+ const { nodes, edges } = extractRoutes(tree, 'test.tsx', code, existingNodes);
198
+
199
+ expect(nodes).toHaveLength(2);
200
+ expect(nodes[0]!.kind).toBe('Route');
201
+ expect(nodes[0]!.name).toBe('/');
202
+ expect(nodes[1]!.name).toBe('/settings');
203
+
204
+ // Should link routes to components
205
+ const renders = edges.filter(e => e.kind === 'renders');
206
+ expect(renders).toHaveLength(2);
207
+ });
208
+ });
209
+
210
+ describe('Full pipeline with Phase 2 extractors', () => {
211
+ it('indexes fixture project with context and stores', async () => {
212
+ const { graph } = await indexProject(FIXTURE_DIR);
213
+
214
+ // Should find context
215
+ const contexts = graph.getNodesByKind('Context');
216
+ expect(contexts.some(c => c.name === 'ThemeContext')).toBe(true);
217
+
218
+ // Should find zustand store
219
+ const stores = graph.getNodesByKind('Store');
220
+ expect(stores.some(s => s.name === 'useAuthStore')).toBe(true);
221
+
222
+ // Map should include new sections
223
+ const { formatMap } = await import('../src/mcp/formatter.js');
224
+ const map = formatMap(graph);
225
+ expect(map).toContain('REACTGRAPH MAP');
226
+ expect(map).toContain('COMPONENTS');
227
+ });
228
+
229
+ it('detects cross-file store usage (Dashboard reads useAuthStore)', async () => {
230
+ const { graph } = await indexProject(FIXTURE_DIR);
231
+
232
+ // Dashboard should have a reads_store edge to useAuthStore
233
+ const dashNode = graph.getNodesByKind('Component').find(c => c.name === 'Dashboard');
234
+ expect(dashNode).toBeDefined();
235
+
236
+ const storeReads = graph.getEdgesFrom(dashNode!.id, ['reads_store']);
237
+ expect(storeReads.length).toBeGreaterThan(0);
238
+
239
+ // The target should be the useAuthStore node
240
+ const storeNode = graph.getNodesByKind('Store').find(s => s.name === 'useAuthStore');
241
+ expect(storeNode).toBeDefined();
242
+ expect(storeReads.some(e => e.target === storeNode!.id)).toBe(true);
243
+ });
244
+
245
+ it('get_subgraph on store shows readers', async () => {
246
+ const { graph } = await indexProject(FIXTURE_DIR);
247
+ const { formatSubgraph } = await import('../src/mcp/formatter.js');
248
+ const result = formatSubgraph(graph, 'useAuthStore', 1);
249
+
250
+ expect(result).toContain('useAuthStore');
251
+ // Should show Dashboard reading the store
252
+ expect(result).toContain('Dashboard');
253
+ });
254
+ });
255
+
256
+ describe('New MCP tools', () => {
257
+ it('trace_flow finds path between nodes', async () => {
258
+ const { graph } = await indexProject(FIXTURE_DIR);
259
+ const result = formatTraceFlow(graph, 'App', 'MetricsCard');
260
+ expect(result).toContain('App');
261
+ expect(result).toContain('MetricsCard');
262
+ });
263
+
264
+ it('impact shows dependents', async () => {
265
+ const { graph } = await indexProject(FIXTURE_DIR);
266
+ const result = formatImpact(graph, 'useAuth');
267
+ expect(result).toContain('IMPACT ANALYSIS');
268
+ expect(result).toContain('useAuth');
269
+ });
270
+
271
+ it('find_nodes filters by kind', async () => {
272
+ const { graph } = await indexProject(FIXTURE_DIR);
273
+ const result = formatFindNodes(graph, { kind: 'Hook' });
274
+ expect(result).toContain('useAuth');
275
+ });
276
+
277
+ it('find_nodes finds orphan components', async () => {
278
+ const { graph } = await indexProject(FIXTURE_DIR);
279
+ const result = formatFindNodes(graph, { kind: 'Component', orphan: true });
280
+ // App is a root component — should be in orphans since nothing renders it
281
+ expect(result).toContain('App');
282
+ });
283
+
284
+ it('find_nodes query shows store + connections', async () => {
285
+ const { graph } = await indexProject(FIXTURE_DIR);
286
+ const result = formatFindNodes(graph, { query: 'useAuthStore' });
287
+
288
+ // Should show the store node
289
+ expect(result).toContain('useAuthStore');
290
+ expect(result).toContain('Store');
291
+
292
+ // Should show Dashboard as a reader
293
+ expect(result).toContain('reads_store');
294
+ expect(result).toContain('Dashboard');
295
+ });
296
+
297
+ it('find_nodes with no filters returns help, not all nodes', async () => {
298
+ const { graph } = await indexProject(FIXTURE_DIR);
299
+ const result = formatFindNodes(graph, {});
300
+ expect(result).toContain('No filters provided');
301
+ expect(result).not.toContain('[Component]');
302
+ });
303
+
304
+ it('find_nodes rejects unknown filters instead of dumping all', async () => {
305
+ const { graph } = await indexProject(FIXTURE_DIR);
306
+ const result = formatFindNodes(graph, { foo: 'bar' } as any);
307
+ expect(result).toContain('No filters provided');
308
+ });
309
+ });
@@ -0,0 +1,77 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { join } from 'node:path';
3
+ import { indexProject } from '../src/parser/indexer.js';
4
+ import { formatMap, formatSubgraph, formatFileContext } from '../src/mcp/formatter.js';
5
+
6
+ const FIXTURE_DIR = join(import.meta.dirname, 'fixtures', 'basic');
7
+
8
+ describe('ReactGraph smoke test', () => {
9
+ it('indexes the basic fixture project', async () => {
10
+ const { graph, stats } = await indexProject(FIXTURE_DIR);
11
+
12
+ // Should find files
13
+ expect(stats.totalFiles).toBeGreaterThan(0);
14
+ expect(stats.nodes).toBeGreaterThan(0);
15
+ expect(stats.edges).toBeGreaterThan(0);
16
+
17
+ // Should find components
18
+ const components = graph.getNodesByKind('Component');
19
+ const componentNames = components.map(c => c.name).sort();
20
+ expect(componentNames).toContain('App');
21
+ expect(componentNames).toContain('Dashboard');
22
+ expect(componentNames).toContain('Sidebar');
23
+ expect(componentNames).toContain('MetricsCard');
24
+
25
+ // Should find the hook
26
+ const hooks = graph.getNodesByKind('Hook');
27
+ const hookNames = hooks.map(h => h.name);
28
+ expect(hookNames).toContain('useAuth');
29
+
30
+ // Should have renders edges
31
+ const rendersEdges = graph.getEdgesByKind('renders');
32
+ expect(rendersEdges.length).toBeGreaterThan(0);
33
+
34
+ // App should render Dashboard and Sidebar
35
+ const appNode = components.find(c => c.name === 'App')!;
36
+ const appRenders = graph.getEdgesFrom(appNode.id, ['renders']);
37
+ const renderedNames = appRenders.map(e => {
38
+ const target = graph.getNode(e.target);
39
+ return target?.name ?? e.target;
40
+ });
41
+ expect(renderedNames).toContain('Dashboard');
42
+ expect(renderedNames).toContain('Sidebar');
43
+
44
+ // Dashboard should use useAuth
45
+ const dashNode = components.find(c => c.name === 'Dashboard')!;
46
+ const dashHooks = graph.getEdgesFrom(dashNode.id, ['uses_hook']);
47
+ const hookUsed = dashHooks.map(e => e.meta.hookName).filter(Boolean);
48
+ expect(hookUsed).toContain('useAuth');
49
+ });
50
+
51
+ it('generates a readable map', async () => {
52
+ const { graph } = await indexProject(FIXTURE_DIR);
53
+ const map = formatMap(graph);
54
+
55
+ expect(map).toContain('REACTGRAPH MAP');
56
+ expect(map).toContain('COMPONENTS');
57
+ expect(map).toContain('Dashboard');
58
+ expect(map).toContain('HOOKS');
59
+ expect(map).toContain('useAuth');
60
+ });
61
+
62
+ it('generates subgraph for a component', async () => {
63
+ const { graph } = await indexProject(FIXTURE_DIR);
64
+ const sub = formatSubgraph(graph, 'Dashboard');
65
+
66
+ expect(sub).toContain('Dashboard');
67
+ expect(sub).toContain('renders');
68
+ });
69
+
70
+ it('generates file context', async () => {
71
+ const { graph } = await indexProject(FIXTURE_DIR);
72
+ const ctx = formatFileContext(graph, 'src/components/Dashboard.tsx');
73
+
74
+ expect(ctx).toContain('Dashboard');
75
+ expect(ctx).toContain('IMPORTS');
76
+ });
77
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "declaration": true,
10
+ "declarationMap": true,
11
+ "sourceMap": true,
12
+ "esModuleInterop": true,
13
+ "skipLibCheck": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "resolveJsonModule": true,
16
+ "jsx": "react-jsx"
17
+ },
18
+ "include": ["src/**/*"],
19
+ "exclude": ["node_modules", "dist", "tests"]
20
+ }
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ testTimeout: 30000,
7
+ },
8
+ });