project-graph-mcp 1.5.0 → 2.1.0
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 +171 -31
- package/docs/img/explorer-compact.jpg +0 -0
- package/docs/img/explorer-expanded.jpg +0 -0
- package/package.json +12 -8
- package/src/.project-graph-cache.json +1 -1
- package/src/analysis/analysis-cache.js +7 -0
- package/src/analysis/complexity.js +14 -0
- package/src/analysis/custom-rules.js +36 -0
- package/src/analysis/db-analysis.js +9 -0
- package/src/analysis/dead-code.js +19 -0
- package/src/analysis/full-analysis.js +18 -0
- package/src/analysis/jsdoc-checker.js +24 -0
- package/src/analysis/jsdoc-generator.js +10 -0
- package/src/analysis/large-files.js +11 -0
- package/src/analysis/outdated-patterns.js +12 -0
- package/src/analysis/similar-functions.js +16 -0
- package/src/analysis/test-annotations.js +21 -0
- package/src/analysis/type-checker.js +8 -0
- package/src/analysis/undocumented.js +14 -0
- package/src/cli/cli-handlers.js +4 -0
- package/src/cli/cli.js +5 -0
- package/src/compact/.project-graph-cache.json +1 -0
- package/src/compact/ai-context.js +7 -0
- package/src/compact/compact-migrate.js +17 -0
- package/src/compact/compact.js +18 -0
- package/src/compact/compress.js +14 -0
- package/src/compact/ctx-to-jsdoc.js +29 -0
- package/src/compact/doc-dialect.js +30 -0
- package/src/compact/expand.js +37 -0
- package/src/compact/framework-references.js +5 -0
- package/src/compact/instructions.js +3 -0
- package/src/compact/mode-config.js +8 -0
- package/src/compact/validate-pipeline.js +9 -0
- package/src/core/event-bus.js +9 -0
- package/src/core/filters.js +14 -0
- package/src/core/graph-builder.js +12 -0
- package/src/core/parser.js +31 -0
- package/src/core/workspace.js +8 -0
- package/src/lang/lang-go.js +17 -0
- package/src/lang/lang-python.js +12 -0
- package/src/lang/lang-sql.js +23 -0
- package/src/lang/lang-typescript.js +9 -0
- package/src/lang/lang-utils.js +4 -0
- package/src/mcp/mcp-server.js +17 -0
- package/src/mcp/tool-defs.js +3 -0
- package/src/mcp/tools.js +25 -0
- package/src/network/backend-lifecycle.js +19 -0
- package/src/network/backend.js +5 -0
- package/src/network/local-gateway.js +23 -0
- package/src/network/mdns.js +13 -0
- package/src/network/server.js +10 -0
- package/src/network/web-server.js +34 -0
- package/web/.project-graph-cache.json +1 -0
- package/web/app.js +17 -0
- package/web/components/code-block.js +3 -0
- package/web/components/quick-open.js +5 -0
- package/web/dashboard-state.js +3 -0
- package/web/dashboard.html +27 -0
- package/web/dashboard.js +8 -0
- package/web/highlight.js +13 -0
- package/web/index.html +35 -0
- package/web/panels/ActionBoard/ActionBoard.css.js +1 -0
- package/web/panels/ActionBoard/ActionBoard.js +4 -0
- package/web/panels/ActionBoard/ActionBoard.tpl.js +1 -0
- package/web/panels/EventItem/EventItem.css.js +1 -0
- package/web/panels/EventItem/EventItem.js +4 -0
- package/web/panels/EventItem/EventItem.tpl.js +1 -0
- package/web/panels/ProjectItem/ProjectItem.css.js +1 -0
- package/web/panels/ProjectItem/ProjectItem.js +5 -0
- package/web/panels/ProjectItem/ProjectItem.tpl.js +1 -0
- package/web/panels/ProjectList/ProjectList.css.js +1 -0
- package/web/panels/ProjectList/ProjectList.js +4 -0
- package/web/panels/ProjectList/ProjectList.tpl.js +1 -0
- package/web/panels/SettingsPanel/.project-graph-cache.json +1 -0
- package/web/panels/SettingsPanel/SettingsPanel.css.js +1 -0
- package/web/panels/SettingsPanel/SettingsPanel.js +7 -0
- package/web/panels/SettingsPanel/SettingsPanel.tpl.js +1 -0
- package/web/panels/code-viewer.js +5 -0
- package/web/panels/ctx-panel.js +4 -0
- package/web/panels/dep-graph.js +6 -0
- package/web/panels/file-tree.js +188 -0
- package/web/panels/health-panel.js +3 -0
- package/web/panels/live-monitor.js +3 -0
- package/web/state.js +17 -0
- package/web/style.css +157 -0
- package/references/symbiote-3x.md +0 -834
- package/src/ai-context.js +0 -113
- package/src/analysis-cache.js +0 -155
- package/src/cli-handlers.js +0 -271
- package/src/cli.js +0 -95
- package/src/compact.js +0 -207
- package/src/complexity.js +0 -237
- package/src/compress.js +0 -319
- package/src/ctx-to-jsdoc.js +0 -514
- package/src/custom-rules.js +0 -584
- package/src/db-analysis.js +0 -194
- package/src/dead-code.js +0 -468
- package/src/doc-dialect.js +0 -716
- package/src/filters.js +0 -227
- package/src/framework-references.js +0 -177
- package/src/full-analysis.js +0 -470
- package/src/graph-builder.js +0 -299
- package/src/instructions.js +0 -73
- package/src/jsdoc-checker.js +0 -351
- package/src/jsdoc-generator.js +0 -203
- package/src/lang-go.js +0 -285
- package/src/lang-python.js +0 -197
- package/src/lang-sql.js +0 -309
- package/src/lang-typescript.js +0 -190
- package/src/lang-utils.js +0 -124
- package/src/large-files.js +0 -163
- package/src/mcp-server.js +0 -675
- package/src/mode-config.js +0 -127
- package/src/outdated-patterns.js +0 -296
- package/src/parser.js +0 -662
- package/src/server.js +0 -28
- package/src/similar-functions.js +0 -279
- package/src/test-annotations.js +0 -323
- package/src/tool-defs.js +0 -793
- package/src/tools.js +0 -470
- package/src/type-checker.js +0 -188
- package/src/undocumented.js +0 -259
- package/src/workspace.js +0 -70
- /package/{AGENT_ROLE.md → docs/examples/AGENT_ROLE.md} +0 -0
- /package/{AGENT_ROLE_MINIMAL.md → docs/examples/AGENT_ROLE_MINIMAL.md} +0 -0
package/src/tools.js
DELETED
|
@@ -1,470 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MCP Tools for Project Graph
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { parseProject, parseFile, findJSFiles } from './parser.js';
|
|
6
|
-
import { buildGraph, createSkeleton } from './graph-builder.js';
|
|
7
|
-
import { readFileSync, statSync, writeFileSync, existsSync, unlinkSync } from 'fs';
|
|
8
|
-
import { execSync } from 'child_process';
|
|
9
|
-
import { join } from 'path';
|
|
10
|
-
|
|
11
|
-
/** @type {import('./graph-builder.js').Graph|null} */
|
|
12
|
-
let cachedGraph = null;
|
|
13
|
-
|
|
14
|
-
/** @type {string|null} */
|
|
15
|
-
let cachedPath = null;
|
|
16
|
-
|
|
17
|
-
/** @type {Map<string, number>} file path -> mtimeMs */
|
|
18
|
-
let cachedMtimes = new Map();
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Save cache to disk
|
|
22
|
-
* @param {string} path
|
|
23
|
-
* @param {import('./graph-builder.js').Graph} graph
|
|
24
|
-
*/
|
|
25
|
-
function saveDiskCache(path, graph) {
|
|
26
|
-
try {
|
|
27
|
-
const cachePath = join(path, '.project-graph-cache.json');
|
|
28
|
-
const cacheData = {
|
|
29
|
-
version: 1,
|
|
30
|
-
path: path,
|
|
31
|
-
mtimes: Object.fromEntries(cachedMtimes),
|
|
32
|
-
graph: graph
|
|
33
|
-
};
|
|
34
|
-
writeFileSync(cachePath, JSON.stringify(cacheData), 'utf-8');
|
|
35
|
-
} catch (e) {
|
|
36
|
-
// Ignore cache save errors
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Load cache from disk
|
|
42
|
-
* @param {string} path
|
|
43
|
-
* @returns {boolean} true if cache was successfully loaded and is valid
|
|
44
|
-
*/
|
|
45
|
-
function loadDiskCache(path) {
|
|
46
|
-
try {
|
|
47
|
-
const cachePath = join(path, '.project-graph-cache.json');
|
|
48
|
-
if (!existsSync(cachePath)) return false;
|
|
49
|
-
|
|
50
|
-
const content = readFileSync(cachePath, 'utf-8');
|
|
51
|
-
const data = JSON.parse(content);
|
|
52
|
-
|
|
53
|
-
if (data.version !== 1 || data.path !== path) return false;
|
|
54
|
-
|
|
55
|
-
cachedMtimes.clear();
|
|
56
|
-
for (const [file, mtime] of Object.entries(data.mtimes)) {
|
|
57
|
-
cachedMtimes.set(file, mtime);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
cachedGraph = data.graph;
|
|
61
|
-
cachedPath = path;
|
|
62
|
-
|
|
63
|
-
const changed = detectChanges(path);
|
|
64
|
-
if (changed) {
|
|
65
|
-
cachedGraph = null;
|
|
66
|
-
cachedPath = null;
|
|
67
|
-
cachedMtimes.clear();
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return true;
|
|
72
|
-
} catch (e) {
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Get or build graph with smart mtime-based caching.
|
|
79
|
-
* On first call: full parse + build.
|
|
80
|
-
* On subsequent calls: check file mtimes, rebuild only if changes detected.
|
|
81
|
-
* @param {string} path
|
|
82
|
-
* @returns {Promise<import('./graph-builder.js').Graph>}
|
|
83
|
-
*/
|
|
84
|
-
export async function getGraph(path) {
|
|
85
|
-
// Different path = full rebuild
|
|
86
|
-
if (cachedGraph && cachedPath === path) {
|
|
87
|
-
// Check for file changes via mtime
|
|
88
|
-
const changed = detectChanges(path);
|
|
89
|
-
if (!changed) {
|
|
90
|
-
return cachedGraph;
|
|
91
|
-
}
|
|
92
|
-
// Files changed - full rebuild (incremental would need graph-builder changes)
|
|
93
|
-
} else if (!cachedGraph) {
|
|
94
|
-
if (loadDiskCache(path)) {
|
|
95
|
-
return cachedGraph;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const parsed = await parseProject(path);
|
|
100
|
-
cachedGraph = buildGraph(parsed);
|
|
101
|
-
cachedPath = path;
|
|
102
|
-
|
|
103
|
-
// Snapshot mtimes for all parsed files
|
|
104
|
-
snapshotMtimes(path);
|
|
105
|
-
saveDiskCache(path, cachedGraph);
|
|
106
|
-
|
|
107
|
-
return cachedGraph;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Detect if any JS files changed since last snapshot.
|
|
112
|
-
* Checks: new files, deleted files, modified files (via mtimeMs).
|
|
113
|
-
* @param {string} path
|
|
114
|
-
* @returns {boolean} true if changes detected
|
|
115
|
-
*/
|
|
116
|
-
function detectChanges(path) {
|
|
117
|
-
if (cachedMtimes.size === 0) return true;
|
|
118
|
-
|
|
119
|
-
try {
|
|
120
|
-
const currentFiles = findJSFiles(path);
|
|
121
|
-
const currentSet = new Set(currentFiles);
|
|
122
|
-
const cachedSet = new Set(cachedMtimes.keys());
|
|
123
|
-
|
|
124
|
-
// New or deleted files
|
|
125
|
-
if (currentFiles.length !== cachedMtimes.size) return true;
|
|
126
|
-
for (const f of currentFiles) {
|
|
127
|
-
if (!cachedSet.has(f)) return true;
|
|
128
|
-
}
|
|
129
|
-
for (const f of cachedSet) {
|
|
130
|
-
if (!currentSet.has(f)) return true;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Check mtimes
|
|
134
|
-
for (const file of currentFiles) {
|
|
135
|
-
try {
|
|
136
|
-
const mtime = statSync(file).mtimeMs;
|
|
137
|
-
if (mtime !== cachedMtimes.get(file)) return true;
|
|
138
|
-
} catch {
|
|
139
|
-
return true; // File gone or unreadable
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return false;
|
|
144
|
-
} catch {
|
|
145
|
-
return true; // Safety: rebuild on error
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Snapshot current mtimes for all JS files in path.
|
|
151
|
-
* @param {string} path
|
|
152
|
-
*/
|
|
153
|
-
function snapshotMtimes(path) {
|
|
154
|
-
cachedMtimes.clear();
|
|
155
|
-
try {
|
|
156
|
-
const files = findJSFiles(path);
|
|
157
|
-
for (const file of files) {
|
|
158
|
-
try {
|
|
159
|
-
cachedMtimes.set(file, statSync(file).mtimeMs);
|
|
160
|
-
} catch {
|
|
161
|
-
// Skip unreadable files
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
} catch {
|
|
165
|
-
// Ignore errors
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Get compact project skeleton
|
|
171
|
-
* @param {string} path
|
|
172
|
-
* @returns {Promise<Object>}
|
|
173
|
-
*/
|
|
174
|
-
export async function getSkeleton(path) {
|
|
175
|
-
const graph = await getGraph(path);
|
|
176
|
-
return createSkeleton(graph);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Get enriched focus zone based on recent activity
|
|
181
|
-
* @param {Object} options
|
|
182
|
-
* @param {string[]} [options.recentFiles]
|
|
183
|
-
* @param {boolean} [options.useGitDiff]
|
|
184
|
-
* @returns {Promise<Object>}
|
|
185
|
-
*/
|
|
186
|
-
export async function getFocusZone(options = {}) {
|
|
187
|
-
const path = options.path || 'src/components';
|
|
188
|
-
const graph = await getGraph(path);
|
|
189
|
-
|
|
190
|
-
let focusFiles = options.recentFiles || [];
|
|
191
|
-
|
|
192
|
-
// Auto-detect from git diff
|
|
193
|
-
if (options.useGitDiff) {
|
|
194
|
-
try {
|
|
195
|
-
const diff = execSync('git diff --name-only HEAD~5', { encoding: 'utf-8' });
|
|
196
|
-
focusFiles = diff.split('\n').filter(f => f.endsWith('.js'));
|
|
197
|
-
} catch (e) {
|
|
198
|
-
// Git not available or not a repo
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const expanded = {};
|
|
203
|
-
|
|
204
|
-
for (const file of focusFiles) {
|
|
205
|
-
// Find classes in this file
|
|
206
|
-
const content = readFileSync(file, 'utf-8');
|
|
207
|
-
const parsed = await parseFile(content, file);
|
|
208
|
-
|
|
209
|
-
for (const cls of parsed.classes) {
|
|
210
|
-
const shortName = graph.legend[cls.name];
|
|
211
|
-
if (shortName && graph.nodes[shortName]) {
|
|
212
|
-
expanded[shortName] = {
|
|
213
|
-
...graph.nodes[shortName],
|
|
214
|
-
methods: cls.methods,
|
|
215
|
-
properties: cls.properties,
|
|
216
|
-
file: cls.file,
|
|
217
|
-
line: cls.line,
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
return {
|
|
224
|
-
focusFiles,
|
|
225
|
-
expanded,
|
|
226
|
-
expandable: Object.keys(graph.nodes).filter(k => !expanded[k]),
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Expand a symbol to full details
|
|
232
|
-
* @param {string} symbol - Minified symbol like 'SN' or 'SN.tP'
|
|
233
|
-
* @returns {Promise<Object>}
|
|
234
|
-
*/
|
|
235
|
-
export async function expand(symbol) {
|
|
236
|
-
const path = cachedPath || 'src/components';
|
|
237
|
-
const graph = await getGraph(path);
|
|
238
|
-
|
|
239
|
-
const [nodeKey, methodKey] = symbol.split('.');
|
|
240
|
-
const fullName = graph.reverseLegend[nodeKey];
|
|
241
|
-
|
|
242
|
-
if (!fullName) {
|
|
243
|
-
return { error: `Unknown symbol: ${symbol}. Run get_skeleton on your project first, then use symbols from the L (Legend) field.` };
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Find the source file
|
|
247
|
-
const parsed = await parseProject(path);
|
|
248
|
-
const cls = parsed.classes.find(c => c.name === fullName);
|
|
249
|
-
const fn = parsed.functions.find(f => f.name === fullName);
|
|
250
|
-
|
|
251
|
-
if (!cls && !fn) {
|
|
252
|
-
return { error: `Symbol not found: ${fullName}` };
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (fn && !methodKey) {
|
|
256
|
-
return {
|
|
257
|
-
symbol,
|
|
258
|
-
fullName,
|
|
259
|
-
type: 'function',
|
|
260
|
-
file: fn.file,
|
|
261
|
-
line: fn.line,
|
|
262
|
-
exported: fn.exported,
|
|
263
|
-
calls: fn.calls,
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// If method specified, extract method code
|
|
268
|
-
if (methodKey && cls) {
|
|
269
|
-
const methodName = graph.reverseLegend[methodKey] || methodKey;
|
|
270
|
-
const content = readFileSync(cls.file, 'utf-8');
|
|
271
|
-
const methodCode = extractMethod(content, methodName);
|
|
272
|
-
|
|
273
|
-
return {
|
|
274
|
-
symbol,
|
|
275
|
-
fullName: `${fullName}.${methodName}`,
|
|
276
|
-
file: cls.file,
|
|
277
|
-
line: cls.line, // TODO: get actual method line
|
|
278
|
-
code: methodCode,
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// Return full class info
|
|
283
|
-
return {
|
|
284
|
-
symbol,
|
|
285
|
-
fullName,
|
|
286
|
-
file: cls.file,
|
|
287
|
-
line: cls.line,
|
|
288
|
-
extends: cls.extends,
|
|
289
|
-
methods: cls.methods,
|
|
290
|
-
properties: cls.properties,
|
|
291
|
-
calls: cls.calls,
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Get dependency tree for a symbol
|
|
297
|
-
* @param {string} symbol
|
|
298
|
-
* @returns {Promise<Object>}
|
|
299
|
-
*/
|
|
300
|
-
export async function deps(symbol) {
|
|
301
|
-
const path = cachedPath || 'src/components';
|
|
302
|
-
const graph = await getGraph(path);
|
|
303
|
-
|
|
304
|
-
const node = graph.nodes[symbol];
|
|
305
|
-
if (!node) {
|
|
306
|
-
return { error: `Unknown symbol: ${symbol}. Run get_skeleton on your project first, then use symbols from the L (Legend) field.` };
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Find incoming edges (usedBy)
|
|
310
|
-
const usedBy = graph.edges
|
|
311
|
-
.filter(e => e[2].startsWith(symbol))
|
|
312
|
-
.map(e => e[0]);
|
|
313
|
-
|
|
314
|
-
// Find outgoing edges (calls)
|
|
315
|
-
const calls = graph.edges
|
|
316
|
-
.filter(e => e[0] === symbol)
|
|
317
|
-
.map(e => e[2]);
|
|
318
|
-
|
|
319
|
-
return {
|
|
320
|
-
symbol,
|
|
321
|
-
imports: node.i || [],
|
|
322
|
-
usedBy: [...new Set(usedBy)],
|
|
323
|
-
calls: [...new Set(calls)],
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Find all usages of a symbol
|
|
329
|
-
* @param {string} symbol
|
|
330
|
-
* @returns {Promise<Array>}
|
|
331
|
-
*/
|
|
332
|
-
export async function usages(symbol) {
|
|
333
|
-
const path = cachedPath || 'src/components';
|
|
334
|
-
const graph = await getGraph(path);
|
|
335
|
-
const parsed = await parseProject(path);
|
|
336
|
-
|
|
337
|
-
const fullName = graph.reverseLegend[symbol] || symbol;
|
|
338
|
-
const results = [];
|
|
339
|
-
|
|
340
|
-
for (const cls of parsed.classes) {
|
|
341
|
-
if (cls.calls?.includes(fullName) || cls.calls?.some(c => c.includes(fullName))) {
|
|
342
|
-
results.push({
|
|
343
|
-
file: cls.file,
|
|
344
|
-
line: cls.line,
|
|
345
|
-
context: `${cls.name} calls ${fullName}`,
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
return results;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* Extract method code from file content
|
|
355
|
-
* @param {string} content
|
|
356
|
-
* @param {string} methodName
|
|
357
|
-
* @returns {string}
|
|
358
|
-
*/
|
|
359
|
-
function extractMethod(content, methodName) {
|
|
360
|
-
const regex = new RegExp(`((?:\\/\\*\\*[\\s\\S]*?\\*\\/\\s*)?)(?:async\\s+)?${methodName}\\s*\\([^)]*\\)\\s*{`, 'g');
|
|
361
|
-
const match = regex.exec(content);
|
|
362
|
-
|
|
363
|
-
if (!match) return '';
|
|
364
|
-
|
|
365
|
-
const start = match.index;
|
|
366
|
-
let depth = 0;
|
|
367
|
-
let i = match.index + match[0].length - 1;
|
|
368
|
-
|
|
369
|
-
while (i < content.length) {
|
|
370
|
-
if (content[i] === '{') depth++;
|
|
371
|
-
else if (content[i] === '}') {
|
|
372
|
-
depth--;
|
|
373
|
-
if (depth === 0) {
|
|
374
|
-
return content.slice(start, i + 1);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
i++;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
return content.slice(start);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
/**
|
|
384
|
-
* Find call chain from one symbol to another
|
|
385
|
-
* @param {Object} options
|
|
386
|
-
* @param {string} options.from - Starting symbol (full or minified)
|
|
387
|
-
* @param {string} options.to - Target symbol (full or minified)
|
|
388
|
-
* @param {string} [options.path] - Project path
|
|
389
|
-
* @returns {Promise<string[]|Object>}
|
|
390
|
-
*/
|
|
391
|
-
export async function getCallChain(options = {}) {
|
|
392
|
-
const { from, to, path } = options;
|
|
393
|
-
if (!from || !to) {
|
|
394
|
-
return { error: 'Both "from" and "to" parameters are required' };
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
const projectPath = path || cachedPath || 'src/components';
|
|
398
|
-
const graph = await getGraph(projectPath);
|
|
399
|
-
|
|
400
|
-
const fromSym = graph.legend[from] || from;
|
|
401
|
-
const toSym = graph.legend[to] || to;
|
|
402
|
-
|
|
403
|
-
// Build adjacency list for fast lookup
|
|
404
|
-
const adj = {};
|
|
405
|
-
for (const [caller, _, target] of graph.edges) {
|
|
406
|
-
if (!adj[caller]) adj[caller] = [];
|
|
407
|
-
adj[caller].push(target);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// Queue stores { current: string, path: string[] }
|
|
411
|
-
const queue = [{ current: fromSym, path: [fromSym] }];
|
|
412
|
-
const visitedNodes = new Set();
|
|
413
|
-
const expandedBases = new Set();
|
|
414
|
-
visitedNodes.add(fromSym);
|
|
415
|
-
|
|
416
|
-
while (queue.length > 0) {
|
|
417
|
-
const { current, path: currentPath } = queue.shift();
|
|
418
|
-
|
|
419
|
-
const currentBase = current.split('.')[0];
|
|
420
|
-
const currentMethod = current.split('.')[1];
|
|
421
|
-
|
|
422
|
-
if (current === toSym || currentBase === toSym || currentMethod === toSym) {
|
|
423
|
-
const fullPath = currentPath.map(sym => {
|
|
424
|
-
const parts = sym.split('.');
|
|
425
|
-
const base = graph.reverseLegend[parts[0]] || parts[0];
|
|
426
|
-
if (parts.length === 2) {
|
|
427
|
-
const method = graph.reverseLegend[parts[1]] || parts[1];
|
|
428
|
-
return `${base}.${method}`;
|
|
429
|
-
}
|
|
430
|
-
return base;
|
|
431
|
-
});
|
|
432
|
-
return fullPath;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
if (expandedBases.has(currentBase)) {
|
|
436
|
-
continue;
|
|
437
|
-
}
|
|
438
|
-
expandedBases.add(currentBase);
|
|
439
|
-
|
|
440
|
-
const neighbors = adj[currentBase] || [];
|
|
441
|
-
for (const neighbor of neighbors) {
|
|
442
|
-
if (!visitedNodes.has(neighbor)) {
|
|
443
|
-
visitedNodes.add(neighbor);
|
|
444
|
-
queue.push({
|
|
445
|
-
current: neighbor,
|
|
446
|
-
path: [...currentPath, neighbor]
|
|
447
|
-
});
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
return { error: `No call path found from "${from}" to "${to}"` };
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
/**
|
|
456
|
-
* Invalidate cache
|
|
457
|
-
*/
|
|
458
|
-
export function invalidateCache() {
|
|
459
|
-
if (cachedPath) {
|
|
460
|
-
try {
|
|
461
|
-
const cachePath = join(cachedPath, '.project-graph-cache.json');
|
|
462
|
-
if (existsSync(cachePath)) {
|
|
463
|
-
unlinkSync(cachePath);
|
|
464
|
-
}
|
|
465
|
-
} catch (e) {}
|
|
466
|
-
}
|
|
467
|
-
cachedGraph = null;
|
|
468
|
-
cachedPath = null;
|
|
469
|
-
cachedMtimes.clear();
|
|
470
|
-
}
|
package/src/type-checker.js
DELETED
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Optional Type Checker (tsc wrapper)
|
|
3
|
-
* Provides JSDoc type validation via TypeScript compiler
|
|
4
|
-
*
|
|
5
|
-
* Requires `tsc` in PATH (npm i -g typescript)
|
|
6
|
-
* Graceful fallback if not available
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { execSync, spawn } from 'child_process';
|
|
10
|
-
import { existsSync } from 'fs';
|
|
11
|
-
import { resolve, join } from 'path';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* @typedef {Object} TypeDiagnostic
|
|
15
|
-
* @property {string} file
|
|
16
|
-
* @property {number} line
|
|
17
|
-
* @property {number} column
|
|
18
|
-
* @property {'error'|'warning'} severity
|
|
19
|
-
* @property {string} message
|
|
20
|
-
* @property {string} code - TS error code (e.g. "TS2345")
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Check if tsc is available
|
|
25
|
-
* @returns {{ available: boolean, version: string|null, path: string|null }}
|
|
26
|
-
*/
|
|
27
|
-
function detectTsc() {
|
|
28
|
-
try {
|
|
29
|
-
const version = execSync('tsc --version', { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
30
|
-
const tscPath = execSync('which tsc', { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
31
|
-
return { available: true, version, path: tscPath };
|
|
32
|
-
} catch (e) {
|
|
33
|
-
// Try npx
|
|
34
|
-
try {
|
|
35
|
-
const version = execSync('npx tsc --version', { encoding: 'utf-8', timeout: 15000 }).trim();
|
|
36
|
-
return { available: true, version, path: 'npx tsc' };
|
|
37
|
-
} catch (e2) {
|
|
38
|
-
return { available: false, version: null, path: null };
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Parse tsc output line into structured diagnostic
|
|
45
|
-
* @param {string} line
|
|
46
|
-
* @param {string} baseDir
|
|
47
|
-
* @returns {TypeDiagnostic|null}
|
|
48
|
-
*/
|
|
49
|
-
function parseDiagnosticLine(line, baseDir) {
|
|
50
|
-
// Format: file.js(line,col): error TS1234: message
|
|
51
|
-
const match = line.match(/^(.+?)\((\d+),(\d+)\):\s+(error|warning)\s+(TS\d+):\s+(.+)$/);
|
|
52
|
-
if (!match) return null;
|
|
53
|
-
|
|
54
|
-
return {
|
|
55
|
-
file: match[1],
|
|
56
|
-
line: parseInt(match[2]),
|
|
57
|
-
column: parseInt(match[3]),
|
|
58
|
-
severity: match[4],
|
|
59
|
-
message: match[6],
|
|
60
|
-
code: match[5],
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Build tsc arguments
|
|
66
|
-
* @param {string} dir
|
|
67
|
-
* @param {Object} options
|
|
68
|
-
* @returns {string[]}
|
|
69
|
-
*/
|
|
70
|
-
function buildArgs(dir, options = {}) {
|
|
71
|
-
const args = ['--noEmit'];
|
|
72
|
-
|
|
73
|
-
// Check for existing config
|
|
74
|
-
const tsconfig = join(dir, 'tsconfig.json');
|
|
75
|
-
const jsconfig = join(dir, 'jsconfig.json');
|
|
76
|
-
|
|
77
|
-
if (existsSync(tsconfig)) {
|
|
78
|
-
args.push('--project', tsconfig);
|
|
79
|
-
} else if (existsSync(jsconfig)) {
|
|
80
|
-
args.push('--project', jsconfig);
|
|
81
|
-
} else {
|
|
82
|
-
// No config — use sensible defaults for JS projects
|
|
83
|
-
args.push('--allowJs', '--checkJs');
|
|
84
|
-
args.push('--target', 'ESNext');
|
|
85
|
-
args.push('--module', 'NodeNext');
|
|
86
|
-
args.push('--moduleResolution', 'NodeNext');
|
|
87
|
-
args.push('--skipLibCheck');
|
|
88
|
-
|
|
89
|
-
// Include the directory
|
|
90
|
-
if (options.files?.length) {
|
|
91
|
-
args.push(...options.files);
|
|
92
|
-
} else {
|
|
93
|
-
args.push('--rootDir', dir);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return args;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Run type checking on a directory
|
|
102
|
-
* @param {string} dir - Directory to check
|
|
103
|
-
* @param {Object} [options]
|
|
104
|
-
* @param {string[]} [options.files] - Specific files to check
|
|
105
|
-
* @param {number} [options.maxDiagnostics=50] - Max diagnostics to return
|
|
106
|
-
* @returns {Promise<{ available: boolean, version: string|null, diagnostics: TypeDiagnostic[], summary: Object, hint: string|null }>}
|
|
107
|
-
*/
|
|
108
|
-
export async function checkTypes(dir, options = {}) {
|
|
109
|
-
const maxDiagnostics = options.maxDiagnostics || 50;
|
|
110
|
-
const resolvedDir = resolve(dir);
|
|
111
|
-
|
|
112
|
-
// Detect tsc
|
|
113
|
-
const tsc = detectTsc();
|
|
114
|
-
if (!tsc.available) {
|
|
115
|
-
return {
|
|
116
|
-
available: false,
|
|
117
|
-
version: null,
|
|
118
|
-
diagnostics: [],
|
|
119
|
-
summary: { total: 0, errors: 0, warnings: 0 },
|
|
120
|
-
hint: 'TypeScript not found. Install: npm i -g typescript',
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Build and run command
|
|
125
|
-
const args = buildArgs(resolvedDir, options);
|
|
126
|
-
const cmd = tsc.path.includes('npx') ? 'npx' : 'tsc';
|
|
127
|
-
const cmdArgs = tsc.path.includes('npx') ? ['tsc', ...args] : args;
|
|
128
|
-
|
|
129
|
-
const result = await new Promise((res) => {
|
|
130
|
-
const child = spawn(cmd, cmdArgs, {
|
|
131
|
-
cwd: resolvedDir,
|
|
132
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
let stdout = '';
|
|
136
|
-
let stderr = '';
|
|
137
|
-
child.stdout.on('data', (d) => { stdout += d; });
|
|
138
|
-
child.stderr.on('data', (d) => { stderr += d; });
|
|
139
|
-
|
|
140
|
-
const timer = setTimeout(() => {
|
|
141
|
-
child.kill('SIGTERM');
|
|
142
|
-
res({ stdout, stderr, killed: true });
|
|
143
|
-
}, 60000);
|
|
144
|
-
|
|
145
|
-
child.on('close', () => {
|
|
146
|
-
clearTimeout(timer);
|
|
147
|
-
res({ stdout, stderr, killed: false });
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
child.on('error', (e) => {
|
|
151
|
-
clearTimeout(timer);
|
|
152
|
-
res({ stdout: '', stderr: e.message, killed: false });
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
// Parse output (tsc exits with 1 on errors, stdout has diagnostics)
|
|
157
|
-
const output = (result.stdout || '') + (result.stderr || '');
|
|
158
|
-
const lines = output.split('\n').filter(l => l.trim());
|
|
159
|
-
|
|
160
|
-
const diagnostics = [];
|
|
161
|
-
for (const line of lines) {
|
|
162
|
-
const diag = parseDiagnosticLine(line, resolvedDir);
|
|
163
|
-
if (diag && diagnostics.length < maxDiagnostics) {
|
|
164
|
-
diagnostics.push(diag);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const errors = diagnostics.filter(d => d.severity === 'error').length;
|
|
169
|
-
const warnings = diagnostics.filter(d => d.severity === 'warning').length;
|
|
170
|
-
|
|
171
|
-
const byFile = {};
|
|
172
|
-
for (const d of diagnostics) {
|
|
173
|
-
byFile[d.file] = (byFile[d.file] || 0) + 1;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return {
|
|
177
|
-
available: true,
|
|
178
|
-
version: tsc.version,
|
|
179
|
-
diagnostics,
|
|
180
|
-
summary: {
|
|
181
|
-
total: diagnostics.length,
|
|
182
|
-
errors,
|
|
183
|
-
warnings,
|
|
184
|
-
byFile,
|
|
185
|
-
},
|
|
186
|
-
hint: null,
|
|
187
|
-
};
|
|
188
|
-
}
|