typegraph-mcp 0.9.3 → 0.9.5
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/.claude-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/check.ts +7 -15
- package/cli.ts +14 -2
- package/dist/check.js +402 -52
- package/dist/cli.js +517 -511
- package/dist/smoke-test.js +0 -7
- package/package.json +1 -1
- package/smoke-test.ts +0 -14
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "typegraph",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.5",
|
|
4
4
|
"description": "Type-aware TypeScript navigation — 14 MCP tools for go-to-definition, find-references, dependency graphs, cycle detection, and impact analysis",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Owen Jones"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "typegraph",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.5",
|
|
4
4
|
"description": "Type-aware TypeScript navigation — 14 MCP tools for go-to-definition, find-references, dependency graphs, cycle detection, and impact analysis",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Owen Jones"
|
package/check.ts
CHANGED
|
@@ -335,7 +335,13 @@ export async function main(configOverride?: TypegraphConfig): Promise<CheckResul
|
|
|
335
335
|
|
|
336
336
|
// 10. Module graph build test
|
|
337
337
|
try {
|
|
338
|
-
|
|
338
|
+
let buildGraph: (root: string, tsconfig: string) => Promise<{ graph: { files: Set<string>; forward: Map<string, unknown[]> } }>;
|
|
339
|
+
try {
|
|
340
|
+
({ buildGraph } = await import(path.resolve(toolDir, "module-graph.js")));
|
|
341
|
+
} catch {
|
|
342
|
+
// Fallback: plugin dir has .ts files only (no tsx at runtime), use the co-bundled version
|
|
343
|
+
({ buildGraph } = await import("./module-graph.js"));
|
|
344
|
+
}
|
|
339
345
|
const start = performance.now();
|
|
340
346
|
const { graph } = await buildGraph(projectRoot, tsconfigPath);
|
|
341
347
|
const elapsed = (performance.now() - start).toFixed(0);
|
|
@@ -443,17 +449,3 @@ export async function main(configOverride?: TypegraphConfig): Promise<CheckResul
|
|
|
443
449
|
return { passed, failed, warned };
|
|
444
450
|
}
|
|
445
451
|
|
|
446
|
-
// ─── Self-run guard ──────────────────────────────────────────────────────────
|
|
447
|
-
|
|
448
|
-
const isDirectRun =
|
|
449
|
-
process.argv[1] &&
|
|
450
|
-
fs.realpathSync(process.argv[1]) === fs.realpathSync(new URL(import.meta.url).pathname);
|
|
451
|
-
|
|
452
|
-
if (isDirectRun) {
|
|
453
|
-
main()
|
|
454
|
-
.then((result) => process.exit(result.failed > 0 ? 1 : 0))
|
|
455
|
-
.catch((err) => {
|
|
456
|
-
console.error("Fatal error:", err);
|
|
457
|
-
process.exit(1);
|
|
458
|
-
});
|
|
459
|
-
}
|
package/cli.ts
CHANGED
|
@@ -709,17 +709,29 @@ async function remove(yes: boolean): Promise<void> {
|
|
|
709
709
|
|
|
710
710
|
// ─── Check Command ───────────────────────────────────────────────────────────
|
|
711
711
|
|
|
712
|
+
function resolvePluginDir(): string {
|
|
713
|
+
// Prefer the installed plugin in the user's project over the npx cache
|
|
714
|
+
const installed = path.resolve(process.cwd(), PLUGIN_DIR_NAME);
|
|
715
|
+
if (fs.existsSync(installed)) return installed;
|
|
716
|
+
// Fall back to the source directory (running from the repo itself)
|
|
717
|
+
return path.basename(import.meta.dirname) === "dist"
|
|
718
|
+
? path.resolve(import.meta.dirname, "..")
|
|
719
|
+
: import.meta.dirname;
|
|
720
|
+
}
|
|
721
|
+
|
|
712
722
|
async function check(): Promise<void> {
|
|
723
|
+
const config = resolveConfig(resolvePluginDir());
|
|
713
724
|
const { main: checkMain } = await import("./check.js");
|
|
714
|
-
const result = await checkMain();
|
|
725
|
+
const result = await checkMain(config);
|
|
715
726
|
process.exit(result.failed > 0 ? 1 : 0);
|
|
716
727
|
}
|
|
717
728
|
|
|
718
729
|
// ─── Test Command ────────────────────────────────────────────────────────────
|
|
719
730
|
|
|
720
731
|
async function test(): Promise<void> {
|
|
732
|
+
const config = resolveConfig(resolvePluginDir());
|
|
721
733
|
const { main: testMain } = await import("./smoke-test.js");
|
|
722
|
-
const result = await testMain();
|
|
734
|
+
const result = await testMain(config);
|
|
723
735
|
process.exit(result.failed > 0 ? 1 : 0);
|
|
724
736
|
}
|
|
725
737
|
|
package/dist/check.js
CHANGED
|
@@ -1,7 +1,359 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
1
10
|
|
|
2
|
-
//
|
|
11
|
+
// module-graph.ts
|
|
12
|
+
var module_graph_exports = {};
|
|
13
|
+
__export(module_graph_exports, {
|
|
14
|
+
buildGraph: () => buildGraph,
|
|
15
|
+
createResolver: () => createResolver,
|
|
16
|
+
discoverFiles: () => discoverFiles,
|
|
17
|
+
removeFile: () => removeFile,
|
|
18
|
+
startWatcher: () => startWatcher,
|
|
19
|
+
updateFile: () => updateFile
|
|
20
|
+
});
|
|
21
|
+
import { parseSync } from "oxc-parser";
|
|
22
|
+
import { ResolverFactory } from "oxc-resolver";
|
|
3
23
|
import * as fs from "fs";
|
|
4
24
|
import * as path2 from "path";
|
|
25
|
+
function discoverFiles(rootDir) {
|
|
26
|
+
const files = [];
|
|
27
|
+
function walk(dir) {
|
|
28
|
+
let entries;
|
|
29
|
+
try {
|
|
30
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
31
|
+
} catch {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
if (entry.isDirectory()) {
|
|
36
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
37
|
+
if (entry.name.startsWith(".") && dir !== rootDir) continue;
|
|
38
|
+
walk(path2.join(dir, entry.name));
|
|
39
|
+
} else if (entry.isFile()) {
|
|
40
|
+
const name = entry.name;
|
|
41
|
+
if (SKIP_FILES.has(name)) continue;
|
|
42
|
+
if (name.endsWith(".d.ts") || name.endsWith(".d.mts") || name.endsWith(".d.cts")) continue;
|
|
43
|
+
const ext = path2.extname(name);
|
|
44
|
+
if (TS_EXTENSIONS.has(ext)) {
|
|
45
|
+
files.push(path2.join(dir, name));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
walk(rootDir);
|
|
51
|
+
return files;
|
|
52
|
+
}
|
|
53
|
+
function parseFileImports(filePath, source) {
|
|
54
|
+
const result = parseSync(filePath, source);
|
|
55
|
+
const imports = [];
|
|
56
|
+
for (const imp of result.module.staticImports) {
|
|
57
|
+
const specifier = imp.moduleRequest.value;
|
|
58
|
+
const names = [];
|
|
59
|
+
let allTypeOnly = true;
|
|
60
|
+
for (const entry of imp.entries) {
|
|
61
|
+
const kind = entry.importName.kind;
|
|
62
|
+
const name = kind === "Default" ? "default" : kind === "All" || kind === "AllButDefault" || kind === "NamespaceObject" ? "*" : entry.importName.name ?? entry.localName.value;
|
|
63
|
+
names.push(name);
|
|
64
|
+
if (!entry.isType) allTypeOnly = false;
|
|
65
|
+
}
|
|
66
|
+
if (names.length === 0) {
|
|
67
|
+
imports.push({ specifier, names: ["*"], isTypeOnly: false, isDynamic: false });
|
|
68
|
+
} else {
|
|
69
|
+
imports.push({ specifier, names, isTypeOnly: allTypeOnly, isDynamic: false });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
for (const exp of result.module.staticExports) {
|
|
73
|
+
for (const entry of exp.entries) {
|
|
74
|
+
const moduleRequest = entry.moduleRequest;
|
|
75
|
+
if (!moduleRequest) continue;
|
|
76
|
+
const specifier = moduleRequest.value;
|
|
77
|
+
const entryKind = entry.importName.kind;
|
|
78
|
+
const name = entryKind === "AllButDefault" || entryKind === "All" || entryKind === "NamespaceObject" ? "*" : entry.importName.name ?? "*";
|
|
79
|
+
const existing = imports.find((i) => i.specifier === specifier && !i.isDynamic);
|
|
80
|
+
if (existing) {
|
|
81
|
+
if (!existing.names.includes(name)) existing.names.push(name);
|
|
82
|
+
} else {
|
|
83
|
+
imports.push({ specifier, names: [name], isTypeOnly: false, isDynamic: false });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
for (const di of result.module.dynamicImports) {
|
|
88
|
+
if (di.moduleRequest) {
|
|
89
|
+
const sliced = source.slice(di.moduleRequest.start, di.moduleRequest.end);
|
|
90
|
+
if (sliced.startsWith("'") || sliced.startsWith('"')) {
|
|
91
|
+
const specifier = sliced.slice(1, -1);
|
|
92
|
+
imports.push({ specifier, names: ["*"], isTypeOnly: false, isDynamic: true });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return imports;
|
|
97
|
+
}
|
|
98
|
+
function distToSource(resolvedPath, projectRoot) {
|
|
99
|
+
if (!resolvedPath.startsWith(projectRoot)) return resolvedPath;
|
|
100
|
+
const rel = path2.relative(projectRoot, resolvedPath);
|
|
101
|
+
const distIdx = rel.indexOf("dist" + path2.sep);
|
|
102
|
+
if (distIdx === -1) return resolvedPath;
|
|
103
|
+
const prefix = rel.slice(0, distIdx);
|
|
104
|
+
const afterDist = rel.slice(distIdx + 5);
|
|
105
|
+
const withoutExt = afterDist.replace(/\.(m?j|c)s$/, "");
|
|
106
|
+
for (const ext of SOURCE_EXTS) {
|
|
107
|
+
const candidate = path2.resolve(projectRoot, prefix, "src", withoutExt + ext);
|
|
108
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
109
|
+
}
|
|
110
|
+
for (const ext of SOURCE_EXTS) {
|
|
111
|
+
const candidate = path2.resolve(projectRoot, prefix, withoutExt + ext);
|
|
112
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
113
|
+
}
|
|
114
|
+
if (withoutExt.endsWith("/index")) {
|
|
115
|
+
const dirPath = withoutExt.slice(0, -6);
|
|
116
|
+
for (const ext of SOURCE_EXTS) {
|
|
117
|
+
const candidate = path2.resolve(projectRoot, prefix, "src", dirPath + ext);
|
|
118
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return resolvedPath;
|
|
122
|
+
}
|
|
123
|
+
function resolveImport(resolver, fromDir, specifier, projectRoot) {
|
|
124
|
+
try {
|
|
125
|
+
const result = resolver.sync(fromDir, specifier);
|
|
126
|
+
if (result.path && !result.path.includes("node_modules")) {
|
|
127
|
+
const mapped = distToSource(result.path, projectRoot);
|
|
128
|
+
const ext = path2.extname(mapped);
|
|
129
|
+
if (!TS_EXTENSIONS.has(ext)) return null;
|
|
130
|
+
if (SKIP_FILES.has(path2.basename(mapped))) return null;
|
|
131
|
+
return mapped;
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
function createResolver(projectRoot, tsconfigPath) {
|
|
138
|
+
return new ResolverFactory({
|
|
139
|
+
tsconfig: {
|
|
140
|
+
configFile: path2.resolve(projectRoot, tsconfigPath),
|
|
141
|
+
references: "auto"
|
|
142
|
+
},
|
|
143
|
+
extensions: [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"],
|
|
144
|
+
extensionAlias: {
|
|
145
|
+
".js": [".ts", ".tsx", ".js"],
|
|
146
|
+
".jsx": [".tsx", ".jsx"],
|
|
147
|
+
".mjs": [".mts", ".mjs"],
|
|
148
|
+
".cjs": [".cts", ".cjs"]
|
|
149
|
+
},
|
|
150
|
+
conditionNames: ["import", "require"],
|
|
151
|
+
mainFields: ["module", "main"]
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
function buildForwardEdges(files, resolver, projectRoot) {
|
|
155
|
+
const forward = /* @__PURE__ */ new Map();
|
|
156
|
+
const parseFailures = [];
|
|
157
|
+
for (const filePath of files) {
|
|
158
|
+
let source;
|
|
159
|
+
try {
|
|
160
|
+
source = fs.readFileSync(filePath, "utf-8");
|
|
161
|
+
} catch {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
let rawImports;
|
|
165
|
+
try {
|
|
166
|
+
rawImports = parseFileImports(filePath, source);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
parseFailures.push(filePath);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const edges = [];
|
|
172
|
+
const fromDir = path2.dirname(filePath);
|
|
173
|
+
for (const raw of rawImports) {
|
|
174
|
+
const target = resolveImport(resolver, fromDir, raw.specifier, projectRoot);
|
|
175
|
+
if (target) {
|
|
176
|
+
edges.push({
|
|
177
|
+
target,
|
|
178
|
+
specifiers: raw.names,
|
|
179
|
+
isTypeOnly: raw.isTypeOnly,
|
|
180
|
+
isDynamic: raw.isDynamic
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
forward.set(filePath, edges);
|
|
185
|
+
}
|
|
186
|
+
return { forward, parseFailures };
|
|
187
|
+
}
|
|
188
|
+
function buildReverseMap(forward) {
|
|
189
|
+
const reverse = /* @__PURE__ */ new Map();
|
|
190
|
+
for (const [source, edges] of forward) {
|
|
191
|
+
for (const edge of edges) {
|
|
192
|
+
let revEdges = reverse.get(edge.target);
|
|
193
|
+
if (!revEdges) {
|
|
194
|
+
revEdges = [];
|
|
195
|
+
reverse.set(edge.target, revEdges);
|
|
196
|
+
}
|
|
197
|
+
revEdges.push({
|
|
198
|
+
target: source,
|
|
199
|
+
// reverse: the "target" is the file that imports
|
|
200
|
+
specifiers: edge.specifiers,
|
|
201
|
+
isTypeOnly: edge.isTypeOnly,
|
|
202
|
+
isDynamic: edge.isDynamic
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return reverse;
|
|
207
|
+
}
|
|
208
|
+
async function buildGraph(projectRoot, tsconfigPath) {
|
|
209
|
+
const startTime = performance.now();
|
|
210
|
+
const resolver = createResolver(projectRoot, tsconfigPath);
|
|
211
|
+
const fileList = discoverFiles(projectRoot);
|
|
212
|
+
log(`Discovered ${fileList.length} source files`);
|
|
213
|
+
const { forward, parseFailures } = buildForwardEdges(fileList, resolver, projectRoot);
|
|
214
|
+
const reverse = buildReverseMap(forward);
|
|
215
|
+
const files = new Set(fileList);
|
|
216
|
+
const edgeCount = [...forward.values()].reduce((sum, edges) => sum + edges.length, 0);
|
|
217
|
+
const elapsed = (performance.now() - startTime).toFixed(0);
|
|
218
|
+
log(`Graph built: ${files.size} files, ${edgeCount} edges [${elapsed}ms]`);
|
|
219
|
+
if (parseFailures.length > 0) {
|
|
220
|
+
log(`Parse failures: ${parseFailures.length} files`);
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
graph: { forward, reverse, files },
|
|
224
|
+
resolver
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function updateFile(graph, filePath, resolver, projectRoot) {
|
|
228
|
+
const oldEdges = graph.forward.get(filePath) ?? [];
|
|
229
|
+
for (const edge of oldEdges) {
|
|
230
|
+
const revEdges = graph.reverse.get(edge.target);
|
|
231
|
+
if (revEdges) {
|
|
232
|
+
const idx = revEdges.findIndex((r) => r.target === filePath);
|
|
233
|
+
if (idx !== -1) revEdges.splice(idx, 1);
|
|
234
|
+
if (revEdges.length === 0) graph.reverse.delete(edge.target);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
let source;
|
|
238
|
+
try {
|
|
239
|
+
source = fs.readFileSync(filePath, "utf-8");
|
|
240
|
+
} catch {
|
|
241
|
+
removeFile(graph, filePath);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
let rawImports;
|
|
245
|
+
try {
|
|
246
|
+
rawImports = parseFileImports(filePath, source);
|
|
247
|
+
} catch {
|
|
248
|
+
log(`Parse error on update: ${filePath}`);
|
|
249
|
+
graph.forward.set(filePath, []);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const fromDir = path2.dirname(filePath);
|
|
253
|
+
const newEdges = [];
|
|
254
|
+
for (const raw of rawImports) {
|
|
255
|
+
const target = resolveImport(resolver, fromDir, raw.specifier, projectRoot);
|
|
256
|
+
if (target) {
|
|
257
|
+
newEdges.push({
|
|
258
|
+
target,
|
|
259
|
+
specifiers: raw.names,
|
|
260
|
+
isTypeOnly: raw.isTypeOnly,
|
|
261
|
+
isDynamic: raw.isDynamic
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
graph.forward.set(filePath, newEdges);
|
|
266
|
+
graph.files.add(filePath);
|
|
267
|
+
for (const edge of newEdges) {
|
|
268
|
+
let revEdges = graph.reverse.get(edge.target);
|
|
269
|
+
if (!revEdges) {
|
|
270
|
+
revEdges = [];
|
|
271
|
+
graph.reverse.set(edge.target, revEdges);
|
|
272
|
+
}
|
|
273
|
+
revEdges.push({
|
|
274
|
+
target: filePath,
|
|
275
|
+
specifiers: edge.specifiers,
|
|
276
|
+
isTypeOnly: edge.isTypeOnly,
|
|
277
|
+
isDynamic: edge.isDynamic
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function removeFile(graph, filePath) {
|
|
282
|
+
const edges = graph.forward.get(filePath) ?? [];
|
|
283
|
+
for (const edge of edges) {
|
|
284
|
+
const revEdges2 = graph.reverse.get(edge.target);
|
|
285
|
+
if (revEdges2) {
|
|
286
|
+
const idx = revEdges2.findIndex((r) => r.target === filePath);
|
|
287
|
+
if (idx !== -1) revEdges2.splice(idx, 1);
|
|
288
|
+
if (revEdges2.length === 0) graph.reverse.delete(edge.target);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const revEdges = graph.reverse.get(filePath) ?? [];
|
|
292
|
+
for (const revEdge of revEdges) {
|
|
293
|
+
const fwdEdges = graph.forward.get(revEdge.target);
|
|
294
|
+
if (fwdEdges) {
|
|
295
|
+
const idx = fwdEdges.findIndex((e) => e.target === filePath);
|
|
296
|
+
if (idx !== -1) fwdEdges.splice(idx, 1);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
graph.forward.delete(filePath);
|
|
300
|
+
graph.reverse.delete(filePath);
|
|
301
|
+
graph.files.delete(filePath);
|
|
302
|
+
}
|
|
303
|
+
function startWatcher(projectRoot, graph, resolver) {
|
|
304
|
+
try {
|
|
305
|
+
const watcher = fs.watch(
|
|
306
|
+
projectRoot,
|
|
307
|
+
{ recursive: true },
|
|
308
|
+
(_eventType, filename) => {
|
|
309
|
+
if (!filename) return;
|
|
310
|
+
const ext = path2.extname(filename);
|
|
311
|
+
if (!TS_EXTENSIONS.has(ext)) return;
|
|
312
|
+
const parts = filename.split(path2.sep);
|
|
313
|
+
if (parts.some((p) => SKIP_DIRS.has(p))) return;
|
|
314
|
+
if (SKIP_FILES.has(path2.basename(filename))) return;
|
|
315
|
+
if (filename.endsWith(".d.ts") || filename.endsWith(".d.mts") || filename.endsWith(".d.cts"))
|
|
316
|
+
return;
|
|
317
|
+
const absPath = path2.resolve(projectRoot, filename);
|
|
318
|
+
if (fs.existsSync(absPath)) {
|
|
319
|
+
updateFile(graph, absPath, resolver, projectRoot);
|
|
320
|
+
} else {
|
|
321
|
+
removeFile(graph, absPath);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
);
|
|
325
|
+
process.on("SIGINT", () => watcher.close());
|
|
326
|
+
process.on("SIGTERM", () => watcher.close());
|
|
327
|
+
log("File watcher started");
|
|
328
|
+
} catch (err) {
|
|
329
|
+
log("Failed to start file watcher:", err);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
var log, TS_EXTENSIONS, SKIP_DIRS, SKIP_FILES, SOURCE_EXTS;
|
|
333
|
+
var init_module_graph = __esm({
|
|
334
|
+
"module-graph.ts"() {
|
|
335
|
+
log = (...args) => console.error("[typegraph/graph]", ...args);
|
|
336
|
+
TS_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts"]);
|
|
337
|
+
SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
338
|
+
"node_modules",
|
|
339
|
+
"dist",
|
|
340
|
+
"build",
|
|
341
|
+
"out",
|
|
342
|
+
".wrangler",
|
|
343
|
+
".mf",
|
|
344
|
+
".git",
|
|
345
|
+
".next",
|
|
346
|
+
".turbo",
|
|
347
|
+
"coverage"
|
|
348
|
+
]);
|
|
349
|
+
SKIP_FILES = /* @__PURE__ */ new Set(["routeTree.gen.ts"]);
|
|
350
|
+
SOURCE_EXTS = [".ts", ".tsx", ".mts", ".cts"];
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// check.ts
|
|
355
|
+
import * as fs2 from "fs";
|
|
356
|
+
import * as path3 from "path";
|
|
5
357
|
import { createRequire } from "module";
|
|
6
358
|
import { spawn } from "child_process";
|
|
7
359
|
|
|
@@ -20,14 +372,14 @@ function resolveConfig(toolDir) {
|
|
|
20
372
|
function findFirstTsFile(dir) {
|
|
21
373
|
const skipDirs = /* @__PURE__ */ new Set(["node_modules", "dist", ".git", ".wrangler", "coverage"]);
|
|
22
374
|
try {
|
|
23
|
-
for (const entry of
|
|
375
|
+
for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
|
|
24
376
|
if (entry.isFile() && entry.name.endsWith(".ts") && !entry.name.endsWith(".d.ts")) {
|
|
25
|
-
return
|
|
377
|
+
return path3.join(dir, entry.name);
|
|
26
378
|
}
|
|
27
379
|
}
|
|
28
|
-
for (const entry of
|
|
380
|
+
for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
|
|
29
381
|
if (entry.isDirectory() && !skipDirs.has(entry.name) && !entry.name.startsWith(".")) {
|
|
30
|
-
const found = findFirstTsFile(
|
|
382
|
+
const found = findFirstTsFile(path3.join(dir, entry.name));
|
|
31
383
|
if (found) return found;
|
|
32
384
|
}
|
|
33
385
|
}
|
|
@@ -36,13 +388,13 @@ function findFirstTsFile(dir) {
|
|
|
36
388
|
return null;
|
|
37
389
|
}
|
|
38
390
|
function testTsserver(projectRoot) {
|
|
39
|
-
return new Promise((
|
|
391
|
+
return new Promise((resolve4) => {
|
|
40
392
|
let tsserverPath;
|
|
41
393
|
try {
|
|
42
|
-
const require2 = createRequire(
|
|
394
|
+
const require2 = createRequire(path3.resolve(projectRoot, "package.json"));
|
|
43
395
|
tsserverPath = require2.resolve("typescript/lib/tsserver.js");
|
|
44
396
|
} catch {
|
|
45
|
-
|
|
397
|
+
resolve4(false);
|
|
46
398
|
return;
|
|
47
399
|
}
|
|
48
400
|
const child = spawn("node", [tsserverPath, "--disableAutomaticTypingAcquisition"], {
|
|
@@ -51,7 +403,7 @@ function testTsserver(projectRoot) {
|
|
|
51
403
|
});
|
|
52
404
|
const timeout = setTimeout(() => {
|
|
53
405
|
child.kill();
|
|
54
|
-
|
|
406
|
+
resolve4(false);
|
|
55
407
|
}, 1e4);
|
|
56
408
|
let buffer = "";
|
|
57
409
|
child.stdout.on("data", (chunk) => {
|
|
@@ -59,12 +411,12 @@ function testTsserver(projectRoot) {
|
|
|
59
411
|
if (buffer.includes('"success":true')) {
|
|
60
412
|
clearTimeout(timeout);
|
|
61
413
|
child.kill();
|
|
62
|
-
|
|
414
|
+
resolve4(true);
|
|
63
415
|
}
|
|
64
416
|
});
|
|
65
417
|
child.on("error", () => {
|
|
66
418
|
clearTimeout(timeout);
|
|
67
|
-
|
|
419
|
+
resolve4(false);
|
|
68
420
|
});
|
|
69
421
|
child.on("exit", () => {
|
|
70
422
|
clearTimeout(timeout);
|
|
@@ -114,8 +466,8 @@ async function main(configOverride) {
|
|
|
114
466
|
} else {
|
|
115
467
|
fail(`Node.js ${nodeVersion} is too old`, "Upgrade Node.js to >= 18");
|
|
116
468
|
}
|
|
117
|
-
const tsxInRoot =
|
|
118
|
-
const tsxInTool =
|
|
469
|
+
const tsxInRoot = fs2.existsSync(path3.join(projectRoot, "node_modules/.bin/tsx"));
|
|
470
|
+
const tsxInTool = fs2.existsSync(path3.join(toolDir, "node_modules/.bin/tsx"));
|
|
119
471
|
if (tsxInRoot || tsxInTool) {
|
|
120
472
|
pass(`tsx available (in ${tsxInRoot ? "project" : "tool"} node_modules)`);
|
|
121
473
|
} else {
|
|
@@ -123,10 +475,10 @@ async function main(configOverride) {
|
|
|
123
475
|
}
|
|
124
476
|
let tsVersion = null;
|
|
125
477
|
try {
|
|
126
|
-
const require2 = createRequire(
|
|
478
|
+
const require2 = createRequire(path3.resolve(projectRoot, "package.json"));
|
|
127
479
|
const tsserverPath = require2.resolve("typescript/lib/tsserver.js");
|
|
128
|
-
const tsPkgPath =
|
|
129
|
-
const tsPkg = JSON.parse(
|
|
480
|
+
const tsPkgPath = path3.resolve(path3.dirname(tsserverPath), "..", "package.json");
|
|
481
|
+
const tsPkg = JSON.parse(fs2.readFileSync(tsPkgPath, "utf-8"));
|
|
130
482
|
tsVersion = tsPkg.version;
|
|
131
483
|
pass(`TypeScript found (v${tsVersion})`);
|
|
132
484
|
} catch {
|
|
@@ -135,23 +487,23 @@ async function main(configOverride) {
|
|
|
135
487
|
"Add `typescript` to devDependencies and run `npm install`"
|
|
136
488
|
);
|
|
137
489
|
}
|
|
138
|
-
const tsconfigAbs =
|
|
139
|
-
if (
|
|
490
|
+
const tsconfigAbs = path3.resolve(projectRoot, tsconfigPath);
|
|
491
|
+
if (fs2.existsSync(tsconfigAbs)) {
|
|
140
492
|
pass(`tsconfig.json exists at ${tsconfigPath}`);
|
|
141
493
|
} else {
|
|
142
494
|
fail(`tsconfig.json not found at ${tsconfigPath}`, `Create a tsconfig.json at ${tsconfigPath}`);
|
|
143
495
|
}
|
|
144
|
-
const pluginMcpPath =
|
|
145
|
-
const hasPluginMcp =
|
|
496
|
+
const pluginMcpPath = path3.join(toolDir, ".mcp.json");
|
|
497
|
+
const hasPluginMcp = fs2.existsSync(pluginMcpPath) && fs2.existsSync(path3.join(toolDir, ".claude-plugin/plugin.json"));
|
|
146
498
|
if (process.env.CLAUDE_PLUGIN_ROOT) {
|
|
147
499
|
pass("MCP registered via plugin (CLAUDE_PLUGIN_ROOT set)");
|
|
148
500
|
} else if (hasPluginMcp) {
|
|
149
501
|
pass("MCP registered via plugin (.mcp.json + .claude-plugin/ present)");
|
|
150
502
|
} else {
|
|
151
|
-
const mcpJsonPath =
|
|
152
|
-
if (
|
|
503
|
+
const mcpJsonPath = path3.resolve(projectRoot, ".claude/mcp.json");
|
|
504
|
+
if (fs2.existsSync(mcpJsonPath)) {
|
|
153
505
|
try {
|
|
154
|
-
const mcpJson = JSON.parse(
|
|
506
|
+
const mcpJson = JSON.parse(fs2.readFileSync(mcpJsonPath, "utf-8"));
|
|
155
507
|
const tsNav = mcpJson?.mcpServers?.["typegraph"];
|
|
156
508
|
if (tsNav) {
|
|
157
509
|
const hasCommand = tsNav.command === "npx";
|
|
@@ -170,7 +522,7 @@ async function main(configOverride) {
|
|
|
170
522
|
);
|
|
171
523
|
}
|
|
172
524
|
} else {
|
|
173
|
-
const serverPath = toolIsEmbedded ? `./${toolRelPath}/server.ts` :
|
|
525
|
+
const serverPath = toolIsEmbedded ? `./${toolRelPath}/server.ts` : path3.resolve(toolDir, "server.ts");
|
|
174
526
|
fail(
|
|
175
527
|
"MCP entry 'typegraph' not found in .claude/mcp.json",
|
|
176
528
|
`Add to .claude/mcp.json:
|
|
@@ -195,11 +547,11 @@ async function main(configOverride) {
|
|
|
195
547
|
fail(".claude/mcp.json not found", `Create .claude/mcp.json with typegraph server registration`);
|
|
196
548
|
}
|
|
197
549
|
}
|
|
198
|
-
const toolNodeModules =
|
|
199
|
-
if (
|
|
550
|
+
const toolNodeModules = path3.join(toolDir, "node_modules");
|
|
551
|
+
if (fs2.existsSync(toolNodeModules)) {
|
|
200
552
|
const requiredPkgs = ["@modelcontextprotocol/sdk", "oxc-parser", "oxc-resolver", "zod"];
|
|
201
553
|
const missing = requiredPkgs.filter(
|
|
202
|
-
(pkg) => !
|
|
554
|
+
(pkg) => !fs2.existsSync(path3.join(toolNodeModules, ...pkg.split("/")))
|
|
203
555
|
);
|
|
204
556
|
if (missing.length === 0) {
|
|
205
557
|
pass(`Dependencies installed (${requiredPkgs.length} packages)`);
|
|
@@ -210,9 +562,9 @@ async function main(configOverride) {
|
|
|
210
562
|
fail("typegraph-mcp dependencies not installed", `Run \`cd ${toolRelPath} && npm install\``);
|
|
211
563
|
}
|
|
212
564
|
try {
|
|
213
|
-
const oxcParserReq = createRequire(
|
|
214
|
-
const { parseSync } = await import(oxcParserReq.resolve("oxc-parser"));
|
|
215
|
-
const result =
|
|
565
|
+
const oxcParserReq = createRequire(path3.join(toolDir, "package.json"));
|
|
566
|
+
const { parseSync: parseSync2 } = await import(oxcParserReq.resolve("oxc-parser"));
|
|
567
|
+
const result = parseSync2("test.ts", 'import { x } from "./y";');
|
|
216
568
|
if (result.module?.staticImports?.length === 1) {
|
|
217
569
|
pass("oxc-parser working");
|
|
218
570
|
} else {
|
|
@@ -228,9 +580,9 @@ async function main(configOverride) {
|
|
|
228
580
|
);
|
|
229
581
|
}
|
|
230
582
|
try {
|
|
231
|
-
const oxcResolverReq = createRequire(
|
|
232
|
-
const { ResolverFactory } = await import(oxcResolverReq.resolve("oxc-resolver"));
|
|
233
|
-
const resolver = new
|
|
583
|
+
const oxcResolverReq = createRequire(path3.join(toolDir, "package.json"));
|
|
584
|
+
const { ResolverFactory: ResolverFactory2 } = await import(oxcResolverReq.resolve("oxc-resolver"));
|
|
585
|
+
const resolver = new ResolverFactory2({
|
|
234
586
|
tsconfig: { configFile: tsconfigAbs, references: "auto" },
|
|
235
587
|
extensions: [".ts", ".tsx", ".js"],
|
|
236
588
|
extensionAlias: { ".js": [".ts", ".tsx", ".js"] }
|
|
@@ -238,8 +590,8 @@ async function main(configOverride) {
|
|
|
238
590
|
let resolveOk = false;
|
|
239
591
|
const testFile = findFirstTsFile(projectRoot);
|
|
240
592
|
if (testFile) {
|
|
241
|
-
const dir =
|
|
242
|
-
const base = "./" +
|
|
593
|
+
const dir = path3.dirname(testFile);
|
|
594
|
+
const base = "./" + path3.basename(testFile);
|
|
243
595
|
const result = resolver.sync(dir, base);
|
|
244
596
|
resolveOk = !!result.path;
|
|
245
597
|
}
|
|
@@ -278,9 +630,14 @@ async function main(configOverride) {
|
|
|
278
630
|
skip("tsserver test (TypeScript not found)");
|
|
279
631
|
}
|
|
280
632
|
try {
|
|
281
|
-
|
|
633
|
+
let buildGraph2;
|
|
634
|
+
try {
|
|
635
|
+
({ buildGraph: buildGraph2 } = await import(path3.resolve(toolDir, "module-graph.js")));
|
|
636
|
+
} catch {
|
|
637
|
+
({ buildGraph: buildGraph2 } = await Promise.resolve().then(() => (init_module_graph(), module_graph_exports)));
|
|
638
|
+
}
|
|
282
639
|
const start = performance.now();
|
|
283
|
-
const { graph } = await
|
|
640
|
+
const { graph } = await buildGraph2(projectRoot, tsconfigPath);
|
|
284
641
|
const elapsed = (performance.now() - start).toFixed(0);
|
|
285
642
|
const edgeCount = [...graph.forward.values()].reduce(
|
|
286
643
|
(s, e) => s + e.length,
|
|
@@ -306,10 +663,10 @@ async function main(configOverride) {
|
|
|
306
663
|
);
|
|
307
664
|
}
|
|
308
665
|
if (toolIsEmbedded) {
|
|
309
|
-
const eslintConfigPath =
|
|
310
|
-
if (
|
|
311
|
-
const eslintContent =
|
|
312
|
-
const parentDir =
|
|
666
|
+
const eslintConfigPath = path3.resolve(projectRoot, "eslint.config.mjs");
|
|
667
|
+
if (fs2.existsSync(eslintConfigPath)) {
|
|
668
|
+
const eslintContent = fs2.readFileSync(eslintConfigPath, "utf-8");
|
|
669
|
+
const parentDir = path3.basename(path3.dirname(toolDir));
|
|
313
670
|
const parentIgnorePattern = new RegExp(`["']${parentDir}\\/\\*\\*["']`);
|
|
314
671
|
const hasParentIgnore = parentIgnorePattern.test(eslintContent);
|
|
315
672
|
if (hasParentIgnore) {
|
|
@@ -327,14 +684,14 @@ async function main(configOverride) {
|
|
|
327
684
|
} else {
|
|
328
685
|
skip("ESLint config check (typegraph-mcp is external to project)");
|
|
329
686
|
}
|
|
330
|
-
const gitignorePath =
|
|
331
|
-
if (
|
|
332
|
-
const gitignoreContent =
|
|
687
|
+
const gitignorePath = path3.resolve(projectRoot, ".gitignore");
|
|
688
|
+
if (fs2.existsSync(gitignorePath)) {
|
|
689
|
+
const gitignoreContent = fs2.readFileSync(gitignorePath, "utf-8");
|
|
333
690
|
const lines = gitignoreContent.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
334
691
|
const ignoresClaude = lines.some(
|
|
335
692
|
(l) => l === ".claude/" || l === ".claude" || l === "/.claude"
|
|
336
693
|
);
|
|
337
|
-
const parentDir = toolIsEmbedded ?
|
|
694
|
+
const parentDir = toolIsEmbedded ? path3.basename(path3.dirname(toolDir)) : null;
|
|
338
695
|
const ignoresParent = parentDir && lines.some((l) => l === `${parentDir}/` || l === parentDir || l === `/${parentDir}`);
|
|
339
696
|
if (!ignoresParent && !ignoresClaude) {
|
|
340
697
|
pass(".gitignore does not exclude .claude/" + (parentDir ? ` or ${parentDir}/` : ""));
|
|
@@ -364,13 +721,6 @@ async function main(configOverride) {
|
|
|
364
721
|
console.log("");
|
|
365
722
|
return { passed, failed, warned };
|
|
366
723
|
}
|
|
367
|
-
var isDirectRun = process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(new URL(import.meta.url).pathname);
|
|
368
|
-
if (isDirectRun) {
|
|
369
|
-
main().then((result) => process.exit(result.failed > 0 ? 1 : 0)).catch((err) => {
|
|
370
|
-
console.error("Fatal error:", err);
|
|
371
|
-
process.exit(1);
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
724
|
export {
|
|
375
725
|
main
|
|
376
726
|
};
|