@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,691 @@
|
|
|
1
|
+
import { getSubgraph } from '../graph/traverse.js';
|
|
2
|
+
/**
|
|
3
|
+
* Generate the full project map — token-optimized structured text.
|
|
4
|
+
*/
|
|
5
|
+
export function formatMap(graph, maxComponents = 30) {
|
|
6
|
+
const stats = graph.stats();
|
|
7
|
+
const lines = [];
|
|
8
|
+
const routeCount = stats.Route ?? 0;
|
|
9
|
+
const storeCount = stats.Store ?? 0;
|
|
10
|
+
const endpointCount = stats.ApiEndpoint ?? 0;
|
|
11
|
+
// Header
|
|
12
|
+
lines.push(`# REACTGRAPH MAP | ${stats.Component ?? 0} components, ${stats.Hook ?? 0} hooks, ${storeCount} stores, ${routeCount} routes`);
|
|
13
|
+
lines.push(`# Indexed: ${new Date().toISOString()} | ${stats.files} files`);
|
|
14
|
+
lines.push('');
|
|
15
|
+
// Routes section
|
|
16
|
+
const routes = graph.getNodesByKind('Route');
|
|
17
|
+
const endpoints = graph.getNodesByKind('ApiEndpoint');
|
|
18
|
+
if (routes.length > 0 || endpoints.length > 0) {
|
|
19
|
+
lines.push('## ROUTES');
|
|
20
|
+
for (const route of routes) {
|
|
21
|
+
const rendersEdges = graph.getEdgesFrom(route.id, ['renders']);
|
|
22
|
+
const component = rendersEdges.length > 0 ? resolveNodeName(graph, rendersEdges[0].target) : '';
|
|
23
|
+
const role = route.meta.role ? ` [${route.meta.role}]` : '';
|
|
24
|
+
lines.push(`${route.name.padEnd(25)} → ${component}${role} [${route.file}]`);
|
|
25
|
+
}
|
|
26
|
+
for (const ep of endpoints) {
|
|
27
|
+
lines.push(`${ep.name.padEnd(25)} [${ep.file}]`);
|
|
28
|
+
}
|
|
29
|
+
lines.push('');
|
|
30
|
+
}
|
|
31
|
+
// Components section
|
|
32
|
+
const components = graph.getNodesByKind('Component')
|
|
33
|
+
.map(c => ({ node: c, conn: graph.connectivity(c.id) }))
|
|
34
|
+
.sort((a, b) => b.conn - a.conn)
|
|
35
|
+
.slice(0, maxComponents);
|
|
36
|
+
if (components.length > 0) {
|
|
37
|
+
lines.push(`## COMPONENTS (top ${Math.min(maxComponents, components.length)} by connectivity)`);
|
|
38
|
+
for (const { node } of components) {
|
|
39
|
+
lines.push(`${node.name} [${node.file}:${node.line}]`);
|
|
40
|
+
// What it renders
|
|
41
|
+
const rendersEdges = graph.getEdgesFrom(node.id, ['renders']);
|
|
42
|
+
if (rendersEdges.length > 0) {
|
|
43
|
+
const rendered = rendersEdges
|
|
44
|
+
.map(e => resolveNodeName(graph, e.target))
|
|
45
|
+
.filter(Boolean);
|
|
46
|
+
lines.push(` renders: ${rendered.join(', ')}`);
|
|
47
|
+
}
|
|
48
|
+
// Hooks used
|
|
49
|
+
const hookEdges = graph.getEdgesFrom(node.id, ['uses_hook']);
|
|
50
|
+
if (hookEdges.length > 0) {
|
|
51
|
+
const hooks = hookEdges
|
|
52
|
+
.map(e => e.meta.hookName ?? resolveNodeName(graph, e.target))
|
|
53
|
+
.filter(h => !h.startsWith('builtin:'));
|
|
54
|
+
if (hooks.length > 0) {
|
|
55
|
+
lines.push(` hooks: ${hooks.join(', ')}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Context
|
|
59
|
+
const provides = graph.getEdgesFrom(node.id, ['provides']);
|
|
60
|
+
const consumes = graph.getEdgesFrom(node.id, ['consumes']);
|
|
61
|
+
if (provides.length > 0 || consumes.length > 0) {
|
|
62
|
+
const parts = [];
|
|
63
|
+
for (const e of provides)
|
|
64
|
+
parts.push(`${resolveNodeName(graph, e.target)}(provides)`);
|
|
65
|
+
for (const e of consumes)
|
|
66
|
+
parts.push(`${resolveNodeName(graph, e.target)}(read)`);
|
|
67
|
+
lines.push(` context: ${parts.join(', ')}`);
|
|
68
|
+
}
|
|
69
|
+
// Store reads/writes
|
|
70
|
+
const reads = graph.getEdgesFrom(node.id, ['reads_store']);
|
|
71
|
+
const writes = graph.getEdgesFrom(node.id, ['writes_store']);
|
|
72
|
+
if (reads.length > 0 || writes.length > 0) {
|
|
73
|
+
const parts = [];
|
|
74
|
+
const seen = new Set();
|
|
75
|
+
for (const e of reads) {
|
|
76
|
+
const name = resolveNodeName(graph, e.target);
|
|
77
|
+
if (!seen.has(name)) {
|
|
78
|
+
parts.push(`${name}(read)`);
|
|
79
|
+
seen.add(name);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
for (const e of writes) {
|
|
83
|
+
const name = resolveNodeName(graph, e.target);
|
|
84
|
+
if (!seen.has(name)) {
|
|
85
|
+
parts.push(`${name}(write)`);
|
|
86
|
+
seen.add(name);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
// Upgrade to read/write
|
|
90
|
+
const idx = parts.findIndex(p => p.startsWith(name));
|
|
91
|
+
if (idx >= 0)
|
|
92
|
+
parts[idx] = `${name}(read/write)`;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
lines.push(` state: ${parts.join(', ')}`);
|
|
96
|
+
}
|
|
97
|
+
// API calls
|
|
98
|
+
const fetches = graph.getEdgesFrom(node.id, ['fetches']);
|
|
99
|
+
if (fetches.length > 0) {
|
|
100
|
+
const apis = fetches.map(e => `${e.meta.method} ${e.meta.url}`);
|
|
101
|
+
lines.push(` fetches: ${apis.join(', ')}`);
|
|
102
|
+
}
|
|
103
|
+
// Props
|
|
104
|
+
if (node.props && node.props.length > 0) {
|
|
105
|
+
lines.push(` props: ${node.props.join(', ')}`);
|
|
106
|
+
}
|
|
107
|
+
else if (node.exportType === 'default') {
|
|
108
|
+
lines.push(` props: none (page component)`);
|
|
109
|
+
}
|
|
110
|
+
lines.push('');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Hooks section
|
|
114
|
+
const hooks = graph.getNodesByKind('Hook');
|
|
115
|
+
if (hooks.length > 0) {
|
|
116
|
+
lines.push('## HOOKS');
|
|
117
|
+
for (const hook of hooks) {
|
|
118
|
+
lines.push(`${hook.name} [${hook.file}:${hook.line}]`);
|
|
119
|
+
if (hook.returns)
|
|
120
|
+
lines.push(` returns: ${hook.returns}`);
|
|
121
|
+
// Store reads/writes
|
|
122
|
+
const reads = graph.getEdgesFrom(hook.id, ['reads_store']);
|
|
123
|
+
const writes = graph.getEdgesFrom(hook.id, ['writes_store']);
|
|
124
|
+
if (reads.length > 0 || writes.length > 0) {
|
|
125
|
+
const parts = [];
|
|
126
|
+
for (const e of reads)
|
|
127
|
+
parts.push(`${resolveNodeName(graph, e.target)}(read)`);
|
|
128
|
+
for (const e of writes)
|
|
129
|
+
parts.push(`${resolveNodeName(graph, e.target)}(write)`);
|
|
130
|
+
lines.push(` state: ${parts.join(', ')}`);
|
|
131
|
+
}
|
|
132
|
+
// API calls
|
|
133
|
+
const fetches = graph.getEdgesFrom(hook.id, ['fetches']);
|
|
134
|
+
if (fetches.length > 0) {
|
|
135
|
+
const apis = fetches.map(e => `${e.meta.method} ${e.meta.url}`);
|
|
136
|
+
lines.push(` fetches: ${apis.join(', ')}`);
|
|
137
|
+
}
|
|
138
|
+
// Who uses this hook
|
|
139
|
+
const usedBy = graph.getEdgesTo(hook.id, ['uses_hook']);
|
|
140
|
+
if (usedBy.length > 0) {
|
|
141
|
+
const users = usedBy.map(e => resolveNodeName(graph, e.source)).filter(Boolean);
|
|
142
|
+
const display = users.length > 5
|
|
143
|
+
? `${users.slice(0, 5).join(', ')} (${users.length} total)`
|
|
144
|
+
: users.join(', ');
|
|
145
|
+
lines.push(` used-by: ${display}`);
|
|
146
|
+
}
|
|
147
|
+
// Hook deps
|
|
148
|
+
const deps = graph.getEdgesFrom(hook.id, ['uses_hook']);
|
|
149
|
+
const customDeps = deps.filter(e => !e.target.startsWith('builtin:'));
|
|
150
|
+
if (customDeps.length > 0) {
|
|
151
|
+
const depNames = customDeps
|
|
152
|
+
.map(e => e.meta.hookName ?? resolveNodeName(graph, e.target))
|
|
153
|
+
.filter(Boolean);
|
|
154
|
+
lines.push(` deps: ${depNames.join(', ')}`);
|
|
155
|
+
}
|
|
156
|
+
lines.push('');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// State stores section
|
|
160
|
+
const stores = graph.getNodesByKind('Store');
|
|
161
|
+
if (stores.length > 0) {
|
|
162
|
+
lines.push('## STATE STORES');
|
|
163
|
+
for (const store of stores) {
|
|
164
|
+
const lib = store.meta.library ? `[${store.meta.library}]` : '';
|
|
165
|
+
lines.push(`${store.name} ${lib} [${store.file}]`);
|
|
166
|
+
const shape = store.meta.shape;
|
|
167
|
+
if (shape && shape.length > 0) {
|
|
168
|
+
lines.push(` shape: { ${shape.join(', ')} }`);
|
|
169
|
+
}
|
|
170
|
+
const writers = graph.getEdgesTo(store.id, ['writes_store']);
|
|
171
|
+
if (writers.length > 0) {
|
|
172
|
+
lines.push(` writers: ${writers.map(e => resolveNodeName(graph, e.source)).join(', ')}`);
|
|
173
|
+
}
|
|
174
|
+
const readers = graph.getEdgesTo(store.id, ['reads_store']);
|
|
175
|
+
if (readers.length > 0) {
|
|
176
|
+
lines.push(` readers: ${readers.map(e => resolveNodeName(graph, e.source)).join(', ')}`);
|
|
177
|
+
}
|
|
178
|
+
lines.push('');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Context providers section
|
|
182
|
+
const contexts = graph.getNodesByKind('Context');
|
|
183
|
+
if (contexts.length > 0) {
|
|
184
|
+
lines.push('## CONTEXT PROVIDERS');
|
|
185
|
+
for (const ctx of contexts) {
|
|
186
|
+
const providers = graph.getEdgesTo(ctx.id, ['provides']);
|
|
187
|
+
const providedAt = providers.length > 0
|
|
188
|
+
? ` provided-at: ${providers.map(e => resolveNodeName(graph, e.source)).join(', ')}`
|
|
189
|
+
: '';
|
|
190
|
+
lines.push(`${ctx.name} [${ctx.file}]${providedAt}`);
|
|
191
|
+
const consumers = graph.getEdgesTo(ctx.id, ['consumes']);
|
|
192
|
+
if (consumers.length > 0) {
|
|
193
|
+
const names = consumers.map(e => resolveNodeName(graph, e.source));
|
|
194
|
+
const display = names.length > 5
|
|
195
|
+
? `${names.slice(0, 5).join(', ')} (${names.length} total)`
|
|
196
|
+
: names.join(', ');
|
|
197
|
+
lines.push(` consumers: ${display}`);
|
|
198
|
+
}
|
|
199
|
+
lines.push('');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Shared modules
|
|
203
|
+
const modules = graph.getNodesByKind('Module');
|
|
204
|
+
const highFanOut = modules
|
|
205
|
+
.map(m => ({ node: m, importers: graph.getEdgesTo(m.id, ['imports']).length }))
|
|
206
|
+
.filter(m => m.importers >= 5)
|
|
207
|
+
.sort((a, b) => b.importers - a.importers);
|
|
208
|
+
if (highFanOut.length > 0) {
|
|
209
|
+
lines.push('## SHARED MODULES (high fan-out)');
|
|
210
|
+
for (const { node, importers } of highFanOut) {
|
|
211
|
+
lines.push(`${node.name} [${node.file}] → ${importers} importers`);
|
|
212
|
+
}
|
|
213
|
+
lines.push('');
|
|
214
|
+
}
|
|
215
|
+
return lines.join('\n');
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Format a subgraph around a specific node — tree-style rendering.
|
|
219
|
+
*/
|
|
220
|
+
export function formatSubgraph(graph, target, depth = 2) {
|
|
221
|
+
const node = graph.findNode(target) ?? graph.getNode(target);
|
|
222
|
+
if (!node)
|
|
223
|
+
return `Node not found: ${target}`;
|
|
224
|
+
const sub = getSubgraph(graph, node.id, depth);
|
|
225
|
+
if (!sub)
|
|
226
|
+
return `Could not build subgraph for: ${target}`;
|
|
227
|
+
const lines = [];
|
|
228
|
+
lines.push(`${node.name} [${node.file}:${node.line}]`);
|
|
229
|
+
const outgoing = graph.getEdgesFrom(node.id);
|
|
230
|
+
const byKind = groupBy(outgoing, e => e.kind);
|
|
231
|
+
for (const [kind, edges] of byKind) {
|
|
232
|
+
for (const edge of edges) {
|
|
233
|
+
const targetNode = graph.getNode(edge.target);
|
|
234
|
+
const targetName = targetNode ? `${targetNode.name} [${targetNode.file}]` : edge.target;
|
|
235
|
+
const metaStr = formatEdgeMeta(edge);
|
|
236
|
+
lines.push(` ├─ ${kind} → ${targetName}${metaStr}`);
|
|
237
|
+
if (targetNode && depth > 1) {
|
|
238
|
+
const deeper = graph.getEdgesFrom(targetNode.id);
|
|
239
|
+
for (const d of deeper.slice(0, 5)) {
|
|
240
|
+
const dTarget = graph.getNode(d.target);
|
|
241
|
+
const dName = dTarget ? dTarget.name : d.target;
|
|
242
|
+
lines.push(` │ └─ ${d.kind} → ${dName}`);
|
|
243
|
+
}
|
|
244
|
+
if (deeper.length > 5) {
|
|
245
|
+
lines.push(` │ └─ ... (${deeper.length - 5} more)`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const incoming = graph.getEdgesTo(node.id);
|
|
251
|
+
if (incoming.length > 0) {
|
|
252
|
+
lines.push(` used-by (${incoming.length}):`);
|
|
253
|
+
const grouped = groupBy(incoming, e => e.kind);
|
|
254
|
+
for (const [kind, edges] of grouped) {
|
|
255
|
+
for (const edge of edges.slice(0, 10)) {
|
|
256
|
+
const sourceNode = graph.getNode(edge.source);
|
|
257
|
+
const sourceName = sourceNode ? `${sourceNode.name} [${sourceNode.file}]` : edge.source;
|
|
258
|
+
lines.push(` │ ← ${kind} ← ${sourceName}`);
|
|
259
|
+
}
|
|
260
|
+
if (edges.length > 10) {
|
|
261
|
+
lines.push(` │ ... (${edges.length - 10} more)`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return lines.join('\n');
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Format everything about a specific file.
|
|
269
|
+
*/
|
|
270
|
+
export function formatFileContext(graph, filePath) {
|
|
271
|
+
const nodes = graph.getNodesByFile(filePath);
|
|
272
|
+
if (nodes.length === 0)
|
|
273
|
+
return `No nodes found for file: ${filePath}`;
|
|
274
|
+
const lines = [];
|
|
275
|
+
lines.push(`FILE: ${filePath}`);
|
|
276
|
+
// Exports
|
|
277
|
+
const exports = nodes.filter(n => n.exportType !== 'none');
|
|
278
|
+
if (exports.length > 0) {
|
|
279
|
+
lines.push(`EXPORTS: ${exports.map(n => `${n.name} (${n.exportType})`).join(', ')}`);
|
|
280
|
+
}
|
|
281
|
+
// Imports
|
|
282
|
+
const moduleNode = nodes.find(n => n.kind === 'Module');
|
|
283
|
+
if (moduleNode) {
|
|
284
|
+
const importEdges = graph.getEdgesFrom(moduleNode.id, ['imports']);
|
|
285
|
+
if (importEdges.length > 0) {
|
|
286
|
+
lines.push('IMPORTS:');
|
|
287
|
+
for (const edge of importEdges) {
|
|
288
|
+
const names = edge.meta.names ?? [];
|
|
289
|
+
lines.push(` ${names.join(', ')} ← ${edge.target}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
// Components
|
|
294
|
+
for (const comp of nodes.filter(n => n.kind === 'Component')) {
|
|
295
|
+
lines.push('');
|
|
296
|
+
lines.push(`COMPONENT: ${comp.name} (line ${comp.line})`);
|
|
297
|
+
const renders = graph.getEdgesFrom(comp.id, ['renders']);
|
|
298
|
+
if (renders.length > 0) {
|
|
299
|
+
lines.push(` RENDERS: ${renders.map(e => resolveNodeName(graph, e.target)).join(', ')}`);
|
|
300
|
+
}
|
|
301
|
+
const hookEdges = graph.getEdgesFrom(comp.id, ['uses_hook']);
|
|
302
|
+
const customHooks = hookEdges.filter(e => !e.target.startsWith('builtin:'));
|
|
303
|
+
if (customHooks.length > 0) {
|
|
304
|
+
const hookDetails = customHooks.map(e => {
|
|
305
|
+
const name = e.meta.hookName ?? resolveNodeName(graph, e.target);
|
|
306
|
+
const destructured = e.meta.destructured;
|
|
307
|
+
return destructured ? `${name} → { ${destructured.join(', ')} }` : name;
|
|
308
|
+
});
|
|
309
|
+
lines.push(` HOOKS: ${hookDetails.join(', ')}`);
|
|
310
|
+
}
|
|
311
|
+
const provides = graph.getEdgesFrom(comp.id, ['provides']);
|
|
312
|
+
const consumes = graph.getEdgesFrom(comp.id, ['consumes']);
|
|
313
|
+
if (provides.length > 0 || consumes.length > 0) {
|
|
314
|
+
const parts = [];
|
|
315
|
+
for (const e of provides)
|
|
316
|
+
parts.push(`${resolveNodeName(graph, e.target)}(provides)`);
|
|
317
|
+
for (const e of consumes)
|
|
318
|
+
parts.push(`${resolveNodeName(graph, e.target)}(read)`);
|
|
319
|
+
lines.push(` CONTEXT: ${parts.join(', ')}`);
|
|
320
|
+
}
|
|
321
|
+
const storeReads = graph.getEdgesFrom(comp.id, ['reads_store']);
|
|
322
|
+
const storeWrites = graph.getEdgesFrom(comp.id, ['writes_store']);
|
|
323
|
+
if (storeReads.length > 0 || storeWrites.length > 0) {
|
|
324
|
+
const parts = [];
|
|
325
|
+
for (const e of storeReads)
|
|
326
|
+
parts.push(`${resolveNodeName(graph, e.target)}(read)`);
|
|
327
|
+
for (const e of storeWrites)
|
|
328
|
+
parts.push(`${resolveNodeName(graph, e.target)}(write)`);
|
|
329
|
+
lines.push(` STATE: ${parts.join(', ')}`);
|
|
330
|
+
}
|
|
331
|
+
if (comp.props && comp.props.length > 0) {
|
|
332
|
+
lines.push(` PROPS: ${comp.props.join(', ')}`);
|
|
333
|
+
}
|
|
334
|
+
const renderedBy = graph.getEdgesTo(comp.id, ['renders']);
|
|
335
|
+
if (renderedBy.length > 0) {
|
|
336
|
+
lines.push(` RENDERED BY: ${renderedBy.map(e => resolveNodeName(graph, e.source)).join(', ')}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// Hooks
|
|
340
|
+
for (const hook of nodes.filter(n => n.kind === 'Hook')) {
|
|
341
|
+
lines.push('');
|
|
342
|
+
lines.push(`HOOK: ${hook.name} (line ${hook.line})`);
|
|
343
|
+
if (hook.returns)
|
|
344
|
+
lines.push(` RETURNS: ${hook.returns}`);
|
|
345
|
+
const usedBy = graph.getEdgesTo(hook.id, ['uses_hook']);
|
|
346
|
+
if (usedBy.length > 0) {
|
|
347
|
+
lines.push(` USED BY: ${usedBy.map(e => resolveNodeName(graph, e.source)).join(', ')}`);
|
|
348
|
+
}
|
|
349
|
+
const effects = hook.meta.effects;
|
|
350
|
+
if (effects?.length) {
|
|
351
|
+
for (const eff of effects) {
|
|
352
|
+
const depsStr = Array.isArray(eff.deps) ? `[${eff.deps.join(', ')}]` : eff.deps;
|
|
353
|
+
lines.push(` EFFECT: ${eff.type} deps=${depsStr}${eff.hasCleanup ? ' +cleanup' : ''}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
// Stores
|
|
358
|
+
for (const store of nodes.filter(n => n.kind === 'Store')) {
|
|
359
|
+
lines.push('');
|
|
360
|
+
lines.push(`STORE: ${store.name} [${store.meta.library}]`);
|
|
361
|
+
const shape = store.meta.shape;
|
|
362
|
+
if (shape?.length)
|
|
363
|
+
lines.push(` SHAPE: { ${shape.join(', ')} }`);
|
|
364
|
+
}
|
|
365
|
+
// Contexts
|
|
366
|
+
for (const ctx of nodes.filter(n => n.kind === 'Context')) {
|
|
367
|
+
lines.push('');
|
|
368
|
+
lines.push(`CONTEXT: ${ctx.name}`);
|
|
369
|
+
}
|
|
370
|
+
// Routes
|
|
371
|
+
for (const route of nodes.filter(n => n.kind === 'Route')) {
|
|
372
|
+
lines.push('');
|
|
373
|
+
lines.push(`ROUTE: ${route.name}`);
|
|
374
|
+
}
|
|
375
|
+
// API Endpoints
|
|
376
|
+
for (const ep of nodes.filter(n => n.kind === 'ApiEndpoint')) {
|
|
377
|
+
lines.push('');
|
|
378
|
+
lines.push(`ENDPOINT: ${ep.name}`);
|
|
379
|
+
}
|
|
380
|
+
return lines.join('\n');
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Trace the data flow path from node A to node B.
|
|
384
|
+
*/
|
|
385
|
+
export function formatTraceFlow(graph, from, to) {
|
|
386
|
+
const fromNode = graph.findNode(from) ?? graph.getNode(from);
|
|
387
|
+
const toNode = graph.findNode(to) ?? graph.getNode(to);
|
|
388
|
+
if (!fromNode)
|
|
389
|
+
return `Source not found: ${from}`;
|
|
390
|
+
if (!toNode)
|
|
391
|
+
return `Target not found: ${to}`;
|
|
392
|
+
// BFS to find shortest path
|
|
393
|
+
const path = findPath(graph, fromNode.id, toNode.id);
|
|
394
|
+
if (!path)
|
|
395
|
+
return `No path found from ${fromNode.name} to ${toNode.name}`;
|
|
396
|
+
const lines = [];
|
|
397
|
+
lines.push(`${fromNode.name} → ${toNode.name}`);
|
|
398
|
+
lines.push('');
|
|
399
|
+
for (let i = 0; i < path.length; i++) {
|
|
400
|
+
const step = path[i];
|
|
401
|
+
const node = graph.getNode(step.nodeId);
|
|
402
|
+
const indent = ' '.repeat(i);
|
|
403
|
+
const arrow = i === 0 ? '' : `└─ ${step.edgeKind} → `;
|
|
404
|
+
const name = node ? `${node.name} [${node.file}]` : step.nodeId;
|
|
405
|
+
lines.push(`${indent}${arrow}${name}`);
|
|
406
|
+
}
|
|
407
|
+
return lines.join('\n');
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Show impact analysis — what breaks if target changes.
|
|
411
|
+
*/
|
|
412
|
+
export function formatImpact(graph, target) {
|
|
413
|
+
const node = graph.findNode(target) ?? graph.getNode(target);
|
|
414
|
+
if (!node)
|
|
415
|
+
return `Node not found: ${target}`;
|
|
416
|
+
const lines = [];
|
|
417
|
+
lines.push(`IMPACT ANALYSIS: ${node.name} [${node.file}]`);
|
|
418
|
+
lines.push('');
|
|
419
|
+
// Direct dependents (things that import/use this node)
|
|
420
|
+
const directEdges = graph.getEdgesTo(node.id);
|
|
421
|
+
const directByKind = groupBy(directEdges, e => e.kind);
|
|
422
|
+
if (directEdges.length > 0) {
|
|
423
|
+
lines.push(`DIRECT (${directEdges.length} dependents):`);
|
|
424
|
+
for (const [kind, edges] of directByKind) {
|
|
425
|
+
for (const edge of edges) {
|
|
426
|
+
const source = graph.getNode(edge.source);
|
|
427
|
+
const name = source ? `${source.name} [${source.file}]` : edge.source;
|
|
428
|
+
lines.push(` ${kind} ← ${name}`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
lines.push('');
|
|
432
|
+
}
|
|
433
|
+
// Indirect: if this is a hook, find all components that use it,
|
|
434
|
+
// then find what renders those components
|
|
435
|
+
if (node.kind === 'Hook') {
|
|
436
|
+
const users = graph.getEdgesTo(node.id, ['uses_hook']);
|
|
437
|
+
const indirect = new Set();
|
|
438
|
+
for (const edge of users) {
|
|
439
|
+
const userNode = graph.getNode(edge.source);
|
|
440
|
+
if (!userNode)
|
|
441
|
+
continue;
|
|
442
|
+
// Who renders this component?
|
|
443
|
+
const renderers = graph.getEdgesTo(userNode.id, ['renders']);
|
|
444
|
+
for (const r of renderers) {
|
|
445
|
+
indirect.add(r.source);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (indirect.size > 0) {
|
|
449
|
+
lines.push(`INDIRECT (via components using ${node.name}):`);
|
|
450
|
+
for (const id of indirect) {
|
|
451
|
+
const n = graph.getNode(id);
|
|
452
|
+
lines.push(` ${n ? `${n.name} [${n.file}]` : id}`);
|
|
453
|
+
}
|
|
454
|
+
lines.push('');
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
// If this is a store, find all readers and writers
|
|
458
|
+
if (node.kind === 'Store') {
|
|
459
|
+
const readers = graph.getEdgesTo(node.id, ['reads_store']);
|
|
460
|
+
const writers = graph.getEdgesTo(node.id, ['writes_store']);
|
|
461
|
+
if (readers.length > 0) {
|
|
462
|
+
lines.push('READERS (will see changed state shape):');
|
|
463
|
+
for (const e of readers) {
|
|
464
|
+
const n = graph.getNode(e.source);
|
|
465
|
+
lines.push(` ${n ? `${n.name} [${n.file}]` : e.source}`);
|
|
466
|
+
}
|
|
467
|
+
lines.push('');
|
|
468
|
+
}
|
|
469
|
+
if (writers.length > 0) {
|
|
470
|
+
lines.push('WRITERS (may need signature update):');
|
|
471
|
+
for (const e of writers) {
|
|
472
|
+
const n = graph.getNode(e.source);
|
|
473
|
+
lines.push(` ${n ? `${n.name} [${n.file}]` : e.source}`);
|
|
474
|
+
}
|
|
475
|
+
lines.push('');
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
// Safe to change analysis
|
|
479
|
+
if (node.kind === 'Component') {
|
|
480
|
+
const renders = graph.getEdgesFrom(node.id, ['renders']);
|
|
481
|
+
const renderedBy = graph.getEdgesTo(node.id, ['renders']);
|
|
482
|
+
lines.push('SAFE TO CHANGE:');
|
|
483
|
+
lines.push(' Internal implementation (same props + same renders = no breakage)');
|
|
484
|
+
if (node.props && node.props.length > 0) {
|
|
485
|
+
lines.push(`RISKY:`);
|
|
486
|
+
lines.push(` Props shape — ${renderedBy.length} parents pass: ${node.props.join(', ')}`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return lines.join('\n');
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Query the graph with filters.
|
|
493
|
+
*/
|
|
494
|
+
export function formatFindNodes(graph, filters) {
|
|
495
|
+
// If query is provided, find a node by name and show it + all connections.
|
|
496
|
+
// This is the most common usage: "find_nodes({ query: 'useUIStore' })"
|
|
497
|
+
if (filters.query) {
|
|
498
|
+
return formatNodeQuery(graph, filters.query);
|
|
499
|
+
}
|
|
500
|
+
let results = graph.getAllNodes();
|
|
501
|
+
let hasFilter = false;
|
|
502
|
+
// Filter by kind
|
|
503
|
+
if (filters.kind) {
|
|
504
|
+
hasFilter = true;
|
|
505
|
+
const kind = filters.kind;
|
|
506
|
+
results = results.filter(n => n.kind === kind);
|
|
507
|
+
}
|
|
508
|
+
// Filter by name (substring match)
|
|
509
|
+
if (filters.name) {
|
|
510
|
+
hasFilter = true;
|
|
511
|
+
const name = filters.name.toLowerCase();
|
|
512
|
+
results = results.filter(n => n.name.toLowerCase().includes(name));
|
|
513
|
+
}
|
|
514
|
+
// Filter by hook usage
|
|
515
|
+
if (filters.using_hook) {
|
|
516
|
+
hasFilter = true;
|
|
517
|
+
const hookName = filters.using_hook;
|
|
518
|
+
results = results.filter(n => {
|
|
519
|
+
const hookEdges = graph.getEdgesFrom(n.id, ['uses_hook']);
|
|
520
|
+
return hookEdges.some(e => {
|
|
521
|
+
const target = graph.getNode(e.target);
|
|
522
|
+
return target?.name === hookName || e.meta.hookName === hookName;
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
// Filter by fetches (components/hooks that make API calls)
|
|
527
|
+
if (filters.fetches) {
|
|
528
|
+
hasFilter = true;
|
|
529
|
+
results = results.filter(n => {
|
|
530
|
+
return graph.getEdgesFrom(n.id, ['fetches']).length > 0;
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
// Filter orphans (components rendered by nothing)
|
|
534
|
+
if (filters.orphan) {
|
|
535
|
+
hasFilter = true;
|
|
536
|
+
results = results.filter(n => {
|
|
537
|
+
if (n.kind !== 'Component')
|
|
538
|
+
return false;
|
|
539
|
+
const renderedBy = graph.getEdgesTo(n.id, ['renders']);
|
|
540
|
+
return renderedBy.length === 0;
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
// Filter by store usage
|
|
544
|
+
if (filters.using_store) {
|
|
545
|
+
hasFilter = true;
|
|
546
|
+
const storeName = filters.using_store;
|
|
547
|
+
results = results.filter(n => {
|
|
548
|
+
const reads = graph.getEdgesFrom(n.id, ['reads_store']);
|
|
549
|
+
const writes = graph.getEdgesFrom(n.id, ['writes_store']);
|
|
550
|
+
return [...reads, ...writes].some(e => {
|
|
551
|
+
const target = graph.getNode(e.target);
|
|
552
|
+
return target?.name === storeName;
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
// Safety: if no recognized filter was applied, don't dump the entire graph
|
|
557
|
+
if (!hasFilter) {
|
|
558
|
+
return 'No filters provided. Use: query, name, kind, using_hook, using_store, fetches, or orphan.';
|
|
559
|
+
}
|
|
560
|
+
if (results.length === 0)
|
|
561
|
+
return 'No matching nodes found.';
|
|
562
|
+
const lines = [];
|
|
563
|
+
lines.push(`Found ${results.length} nodes:`);
|
|
564
|
+
for (const node of results) {
|
|
565
|
+
lines.push(` ${node.name} [${node.kind}] [${node.file}:${node.line}]`);
|
|
566
|
+
}
|
|
567
|
+
return lines.join('\n');
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Smart query: find a node by name and return it + all connected nodes,
|
|
571
|
+
* grouped by relationship type. This is what Claude actually needs when
|
|
572
|
+
* asking "who uses useUIStore?"
|
|
573
|
+
*/
|
|
574
|
+
function formatNodeQuery(graph, query) {
|
|
575
|
+
const node = graph.findNode(query) ?? graph.getNode(query);
|
|
576
|
+
if (!node) {
|
|
577
|
+
// Try substring match
|
|
578
|
+
const all = graph.getAllNodes();
|
|
579
|
+
const matches = all.filter(n => n.name.toLowerCase().includes(query.toLowerCase()));
|
|
580
|
+
if (matches.length === 0)
|
|
581
|
+
return `No nodes matching "${query}".`;
|
|
582
|
+
if (matches.length > 20) {
|
|
583
|
+
return `${matches.length} nodes match "${query}". Be more specific, or use kind filter.\n` +
|
|
584
|
+
matches.slice(0, 20).map(n => ` ${n.name} [${n.kind}] [${n.file}]`).join('\n');
|
|
585
|
+
}
|
|
586
|
+
const lines = [`Found ${matches.length} nodes matching "${query}":`];
|
|
587
|
+
for (const m of matches)
|
|
588
|
+
lines.push(` ${m.name} [${m.kind}] [${m.file}:${m.line}]`);
|
|
589
|
+
return lines.join('\n');
|
|
590
|
+
}
|
|
591
|
+
const lines = [];
|
|
592
|
+
lines.push(`${node.name} [${node.kind}] [${node.file}:${node.line}]`);
|
|
593
|
+
lines.push('');
|
|
594
|
+
// Outgoing connections
|
|
595
|
+
const outgoing = graph.getEdgesFrom(node.id);
|
|
596
|
+
if (outgoing.length > 0) {
|
|
597
|
+
const byKind = groupBy(outgoing, e => e.kind);
|
|
598
|
+
for (const [kind, edges] of byKind) {
|
|
599
|
+
const targets = edges.map(e => {
|
|
600
|
+
const t = graph.getNode(e.target);
|
|
601
|
+
const meta = formatEdgeMeta(e);
|
|
602
|
+
return t ? `${t.name} [${t.file}]${meta}` : `${e.target}${meta}`;
|
|
603
|
+
});
|
|
604
|
+
lines.push(`${kind} → ${targets.join(', ')}`);
|
|
605
|
+
}
|
|
606
|
+
lines.push('');
|
|
607
|
+
}
|
|
608
|
+
// Incoming connections (who depends on this node)
|
|
609
|
+
const incoming = graph.getEdgesTo(node.id);
|
|
610
|
+
if (incoming.length > 0) {
|
|
611
|
+
const byKind = groupBy(incoming, e => e.kind);
|
|
612
|
+
for (const [kind, edges] of byKind) {
|
|
613
|
+
const sources = edges.map(e => {
|
|
614
|
+
const s = graph.getNode(e.source);
|
|
615
|
+
return s ? `${s.name} [${s.file}]` : e.source;
|
|
616
|
+
});
|
|
617
|
+
lines.push(`${kind} ← ${sources.join(', ')}`);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (outgoing.length === 0 && incoming.length === 0) {
|
|
621
|
+
lines.push('No connections found (isolated node).');
|
|
622
|
+
}
|
|
623
|
+
return lines.join('\n');
|
|
624
|
+
}
|
|
625
|
+
function findPath(graph, fromId, toId) {
|
|
626
|
+
const visited = new Set();
|
|
627
|
+
const queue = [
|
|
628
|
+
{ nodeId: fromId, path: [{ nodeId: fromId, edgeKind: '' }] },
|
|
629
|
+
];
|
|
630
|
+
while (queue.length > 0) {
|
|
631
|
+
const { nodeId, path } = queue.shift();
|
|
632
|
+
if (nodeId === toId)
|
|
633
|
+
return path;
|
|
634
|
+
if (visited.has(nodeId))
|
|
635
|
+
continue;
|
|
636
|
+
visited.add(nodeId);
|
|
637
|
+
// Follow outgoing edges
|
|
638
|
+
for (const edge of graph.getEdgesFrom(nodeId)) {
|
|
639
|
+
if (!visited.has(edge.target)) {
|
|
640
|
+
queue.push({
|
|
641
|
+
nodeId: edge.target,
|
|
642
|
+
path: [...path, { nodeId: edge.target, edgeKind: edge.kind }],
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return null;
|
|
648
|
+
}
|
|
649
|
+
// ---- Helpers ----
|
|
650
|
+
function resolveNodeName(graph, id) {
|
|
651
|
+
const node = graph.getNode(id);
|
|
652
|
+
if (node)
|
|
653
|
+
return node.name;
|
|
654
|
+
if (id.startsWith('builtin:'))
|
|
655
|
+
return id.slice(8);
|
|
656
|
+
if (id.startsWith('external:'))
|
|
657
|
+
return id.slice(9);
|
|
658
|
+
if (id.startsWith('unresolved:'))
|
|
659
|
+
return id.slice(11);
|
|
660
|
+
if (id.startsWith('endpoint:'))
|
|
661
|
+
return id.slice(9);
|
|
662
|
+
const colonIdx = id.lastIndexOf(':');
|
|
663
|
+
if (colonIdx > 0)
|
|
664
|
+
return id.slice(colonIdx + 1);
|
|
665
|
+
return id;
|
|
666
|
+
}
|
|
667
|
+
function formatEdgeMeta(edge) {
|
|
668
|
+
const parts = [];
|
|
669
|
+
if (edge.meta.prop)
|
|
670
|
+
parts.push(`prop=${edge.meta.prop}`);
|
|
671
|
+
if (edge.meta.hookName)
|
|
672
|
+
parts.push(edge.meta.hookName);
|
|
673
|
+
if (edge.meta.method && edge.meta.url)
|
|
674
|
+
parts.push(`${edge.meta.method} ${edge.meta.url}`);
|
|
675
|
+
if (edge.meta.library)
|
|
676
|
+
parts.push(edge.meta.library);
|
|
677
|
+
if (parts.length === 0)
|
|
678
|
+
return '';
|
|
679
|
+
return ` (${parts.join(', ')})`;
|
|
680
|
+
}
|
|
681
|
+
function groupBy(items, keyFn) {
|
|
682
|
+
const map = new Map();
|
|
683
|
+
for (const item of items) {
|
|
684
|
+
const key = keyFn(item);
|
|
685
|
+
if (!map.has(key))
|
|
686
|
+
map.set(key, []);
|
|
687
|
+
map.get(key).push(item);
|
|
688
|
+
}
|
|
689
|
+
return map;
|
|
690
|
+
}
|
|
691
|
+
//# sourceMappingURL=formatter.js.map
|