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