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
package/dist/cli.js
CHANGED
|
@@ -24,26 +24,369 @@ var init_config = __esm({
|
|
|
24
24
|
}
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
+
// module-graph.ts
|
|
28
|
+
var module_graph_exports = {};
|
|
29
|
+
__export(module_graph_exports, {
|
|
30
|
+
buildGraph: () => buildGraph,
|
|
31
|
+
createResolver: () => createResolver,
|
|
32
|
+
discoverFiles: () => discoverFiles,
|
|
33
|
+
removeFile: () => removeFile,
|
|
34
|
+
startWatcher: () => startWatcher,
|
|
35
|
+
updateFile: () => updateFile
|
|
36
|
+
});
|
|
37
|
+
import { parseSync } from "oxc-parser";
|
|
38
|
+
import { ResolverFactory } from "oxc-resolver";
|
|
39
|
+
import * as fs from "fs";
|
|
40
|
+
import * as path2 from "path";
|
|
41
|
+
function discoverFiles(rootDir) {
|
|
42
|
+
const files = [];
|
|
43
|
+
function walk(dir) {
|
|
44
|
+
let entries;
|
|
45
|
+
try {
|
|
46
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
47
|
+
} catch {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
if (entry.isDirectory()) {
|
|
52
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
53
|
+
if (entry.name.startsWith(".") && dir !== rootDir) continue;
|
|
54
|
+
walk(path2.join(dir, entry.name));
|
|
55
|
+
} else if (entry.isFile()) {
|
|
56
|
+
const name = entry.name;
|
|
57
|
+
if (SKIP_FILES.has(name)) continue;
|
|
58
|
+
if (name.endsWith(".d.ts") || name.endsWith(".d.mts") || name.endsWith(".d.cts")) continue;
|
|
59
|
+
const ext = path2.extname(name);
|
|
60
|
+
if (TS_EXTENSIONS.has(ext)) {
|
|
61
|
+
files.push(path2.join(dir, name));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
walk(rootDir);
|
|
67
|
+
return files;
|
|
68
|
+
}
|
|
69
|
+
function parseFileImports(filePath, source) {
|
|
70
|
+
const result = parseSync(filePath, source);
|
|
71
|
+
const imports = [];
|
|
72
|
+
for (const imp of result.module.staticImports) {
|
|
73
|
+
const specifier = imp.moduleRequest.value;
|
|
74
|
+
const names = [];
|
|
75
|
+
let allTypeOnly = true;
|
|
76
|
+
for (const entry of imp.entries) {
|
|
77
|
+
const kind = entry.importName.kind;
|
|
78
|
+
const name = kind === "Default" ? "default" : kind === "All" || kind === "AllButDefault" || kind === "NamespaceObject" ? "*" : entry.importName.name ?? entry.localName.value;
|
|
79
|
+
names.push(name);
|
|
80
|
+
if (!entry.isType) allTypeOnly = false;
|
|
81
|
+
}
|
|
82
|
+
if (names.length === 0) {
|
|
83
|
+
imports.push({ specifier, names: ["*"], isTypeOnly: false, isDynamic: false });
|
|
84
|
+
} else {
|
|
85
|
+
imports.push({ specifier, names, isTypeOnly: allTypeOnly, isDynamic: false });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
for (const exp of result.module.staticExports) {
|
|
89
|
+
for (const entry of exp.entries) {
|
|
90
|
+
const moduleRequest = entry.moduleRequest;
|
|
91
|
+
if (!moduleRequest) continue;
|
|
92
|
+
const specifier = moduleRequest.value;
|
|
93
|
+
const entryKind = entry.importName.kind;
|
|
94
|
+
const name = entryKind === "AllButDefault" || entryKind === "All" || entryKind === "NamespaceObject" ? "*" : entry.importName.name ?? "*";
|
|
95
|
+
const existing = imports.find((i) => i.specifier === specifier && !i.isDynamic);
|
|
96
|
+
if (existing) {
|
|
97
|
+
if (!existing.names.includes(name)) existing.names.push(name);
|
|
98
|
+
} else {
|
|
99
|
+
imports.push({ specifier, names: [name], isTypeOnly: false, isDynamic: false });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
for (const di of result.module.dynamicImports) {
|
|
104
|
+
if (di.moduleRequest) {
|
|
105
|
+
const sliced = source.slice(di.moduleRequest.start, di.moduleRequest.end);
|
|
106
|
+
if (sliced.startsWith("'") || sliced.startsWith('"')) {
|
|
107
|
+
const specifier = sliced.slice(1, -1);
|
|
108
|
+
imports.push({ specifier, names: ["*"], isTypeOnly: false, isDynamic: true });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return imports;
|
|
113
|
+
}
|
|
114
|
+
function distToSource(resolvedPath, projectRoot2) {
|
|
115
|
+
if (!resolvedPath.startsWith(projectRoot2)) return resolvedPath;
|
|
116
|
+
const rel2 = path2.relative(projectRoot2, resolvedPath);
|
|
117
|
+
const distIdx = rel2.indexOf("dist" + path2.sep);
|
|
118
|
+
if (distIdx === -1) return resolvedPath;
|
|
119
|
+
const prefix = rel2.slice(0, distIdx);
|
|
120
|
+
const afterDist = rel2.slice(distIdx + 5);
|
|
121
|
+
const withoutExt = afterDist.replace(/\.(m?j|c)s$/, "");
|
|
122
|
+
for (const ext of SOURCE_EXTS) {
|
|
123
|
+
const candidate = path2.resolve(projectRoot2, prefix, "src", withoutExt + ext);
|
|
124
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
125
|
+
}
|
|
126
|
+
for (const ext of SOURCE_EXTS) {
|
|
127
|
+
const candidate = path2.resolve(projectRoot2, prefix, withoutExt + ext);
|
|
128
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
129
|
+
}
|
|
130
|
+
if (withoutExt.endsWith("/index")) {
|
|
131
|
+
const dirPath = withoutExt.slice(0, -6);
|
|
132
|
+
for (const ext of SOURCE_EXTS) {
|
|
133
|
+
const candidate = path2.resolve(projectRoot2, prefix, "src", dirPath + ext);
|
|
134
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return resolvedPath;
|
|
138
|
+
}
|
|
139
|
+
function resolveImport(resolver, fromDir, specifier, projectRoot2) {
|
|
140
|
+
try {
|
|
141
|
+
const result = resolver.sync(fromDir, specifier);
|
|
142
|
+
if (result.path && !result.path.includes("node_modules")) {
|
|
143
|
+
const mapped = distToSource(result.path, projectRoot2);
|
|
144
|
+
const ext = path2.extname(mapped);
|
|
145
|
+
if (!TS_EXTENSIONS.has(ext)) return null;
|
|
146
|
+
if (SKIP_FILES.has(path2.basename(mapped))) return null;
|
|
147
|
+
return mapped;
|
|
148
|
+
}
|
|
149
|
+
} catch {
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
function createResolver(projectRoot2, tsconfigPath2) {
|
|
154
|
+
return new ResolverFactory({
|
|
155
|
+
tsconfig: {
|
|
156
|
+
configFile: path2.resolve(projectRoot2, tsconfigPath2),
|
|
157
|
+
references: "auto"
|
|
158
|
+
},
|
|
159
|
+
extensions: [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"],
|
|
160
|
+
extensionAlias: {
|
|
161
|
+
".js": [".ts", ".tsx", ".js"],
|
|
162
|
+
".jsx": [".tsx", ".jsx"],
|
|
163
|
+
".mjs": [".mts", ".mjs"],
|
|
164
|
+
".cjs": [".cts", ".cjs"]
|
|
165
|
+
},
|
|
166
|
+
conditionNames: ["import", "require"],
|
|
167
|
+
mainFields: ["module", "main"]
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
function buildForwardEdges(files, resolver, projectRoot2) {
|
|
171
|
+
const forward = /* @__PURE__ */ new Map();
|
|
172
|
+
const parseFailures = [];
|
|
173
|
+
for (const filePath of files) {
|
|
174
|
+
let source;
|
|
175
|
+
try {
|
|
176
|
+
source = fs.readFileSync(filePath, "utf-8");
|
|
177
|
+
} catch {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
let rawImports;
|
|
181
|
+
try {
|
|
182
|
+
rawImports = parseFileImports(filePath, source);
|
|
183
|
+
} catch (err) {
|
|
184
|
+
parseFailures.push(filePath);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const edges = [];
|
|
188
|
+
const fromDir = path2.dirname(filePath);
|
|
189
|
+
for (const raw of rawImports) {
|
|
190
|
+
const target = resolveImport(resolver, fromDir, raw.specifier, projectRoot2);
|
|
191
|
+
if (target) {
|
|
192
|
+
edges.push({
|
|
193
|
+
target,
|
|
194
|
+
specifiers: raw.names,
|
|
195
|
+
isTypeOnly: raw.isTypeOnly,
|
|
196
|
+
isDynamic: raw.isDynamic
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
forward.set(filePath, edges);
|
|
201
|
+
}
|
|
202
|
+
return { forward, parseFailures };
|
|
203
|
+
}
|
|
204
|
+
function buildReverseMap(forward) {
|
|
205
|
+
const reverse = /* @__PURE__ */ new Map();
|
|
206
|
+
for (const [source, edges] of forward) {
|
|
207
|
+
for (const edge of edges) {
|
|
208
|
+
let revEdges = reverse.get(edge.target);
|
|
209
|
+
if (!revEdges) {
|
|
210
|
+
revEdges = [];
|
|
211
|
+
reverse.set(edge.target, revEdges);
|
|
212
|
+
}
|
|
213
|
+
revEdges.push({
|
|
214
|
+
target: source,
|
|
215
|
+
// reverse: the "target" is the file that imports
|
|
216
|
+
specifiers: edge.specifiers,
|
|
217
|
+
isTypeOnly: edge.isTypeOnly,
|
|
218
|
+
isDynamic: edge.isDynamic
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return reverse;
|
|
223
|
+
}
|
|
224
|
+
async function buildGraph(projectRoot2, tsconfigPath2) {
|
|
225
|
+
const startTime = performance.now();
|
|
226
|
+
const resolver = createResolver(projectRoot2, tsconfigPath2);
|
|
227
|
+
const fileList = discoverFiles(projectRoot2);
|
|
228
|
+
log(`Discovered ${fileList.length} source files`);
|
|
229
|
+
const { forward, parseFailures } = buildForwardEdges(fileList, resolver, projectRoot2);
|
|
230
|
+
const reverse = buildReverseMap(forward);
|
|
231
|
+
const files = new Set(fileList);
|
|
232
|
+
const edgeCount = [...forward.values()].reduce((sum, edges) => sum + edges.length, 0);
|
|
233
|
+
const elapsed = (performance.now() - startTime).toFixed(0);
|
|
234
|
+
log(`Graph built: ${files.size} files, ${edgeCount} edges [${elapsed}ms]`);
|
|
235
|
+
if (parseFailures.length > 0) {
|
|
236
|
+
log(`Parse failures: ${parseFailures.length} files`);
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
graph: { forward, reverse, files },
|
|
240
|
+
resolver
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function updateFile(graph, filePath, resolver, projectRoot2) {
|
|
244
|
+
const oldEdges = graph.forward.get(filePath) ?? [];
|
|
245
|
+
for (const edge of oldEdges) {
|
|
246
|
+
const revEdges = graph.reverse.get(edge.target);
|
|
247
|
+
if (revEdges) {
|
|
248
|
+
const idx = revEdges.findIndex((r) => r.target === filePath);
|
|
249
|
+
if (idx !== -1) revEdges.splice(idx, 1);
|
|
250
|
+
if (revEdges.length === 0) graph.reverse.delete(edge.target);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
let source;
|
|
254
|
+
try {
|
|
255
|
+
source = fs.readFileSync(filePath, "utf-8");
|
|
256
|
+
} catch {
|
|
257
|
+
removeFile(graph, filePath);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
let rawImports;
|
|
261
|
+
try {
|
|
262
|
+
rawImports = parseFileImports(filePath, source);
|
|
263
|
+
} catch {
|
|
264
|
+
log(`Parse error on update: ${filePath}`);
|
|
265
|
+
graph.forward.set(filePath, []);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const fromDir = path2.dirname(filePath);
|
|
269
|
+
const newEdges = [];
|
|
270
|
+
for (const raw of rawImports) {
|
|
271
|
+
const target = resolveImport(resolver, fromDir, raw.specifier, projectRoot2);
|
|
272
|
+
if (target) {
|
|
273
|
+
newEdges.push({
|
|
274
|
+
target,
|
|
275
|
+
specifiers: raw.names,
|
|
276
|
+
isTypeOnly: raw.isTypeOnly,
|
|
277
|
+
isDynamic: raw.isDynamic
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
graph.forward.set(filePath, newEdges);
|
|
282
|
+
graph.files.add(filePath);
|
|
283
|
+
for (const edge of newEdges) {
|
|
284
|
+
let revEdges = graph.reverse.get(edge.target);
|
|
285
|
+
if (!revEdges) {
|
|
286
|
+
revEdges = [];
|
|
287
|
+
graph.reverse.set(edge.target, revEdges);
|
|
288
|
+
}
|
|
289
|
+
revEdges.push({
|
|
290
|
+
target: filePath,
|
|
291
|
+
specifiers: edge.specifiers,
|
|
292
|
+
isTypeOnly: edge.isTypeOnly,
|
|
293
|
+
isDynamic: edge.isDynamic
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function removeFile(graph, filePath) {
|
|
298
|
+
const edges = graph.forward.get(filePath) ?? [];
|
|
299
|
+
for (const edge of edges) {
|
|
300
|
+
const revEdges2 = graph.reverse.get(edge.target);
|
|
301
|
+
if (revEdges2) {
|
|
302
|
+
const idx = revEdges2.findIndex((r) => r.target === filePath);
|
|
303
|
+
if (idx !== -1) revEdges2.splice(idx, 1);
|
|
304
|
+
if (revEdges2.length === 0) graph.reverse.delete(edge.target);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const revEdges = graph.reverse.get(filePath) ?? [];
|
|
308
|
+
for (const revEdge of revEdges) {
|
|
309
|
+
const fwdEdges = graph.forward.get(revEdge.target);
|
|
310
|
+
if (fwdEdges) {
|
|
311
|
+
const idx = fwdEdges.findIndex((e) => e.target === filePath);
|
|
312
|
+
if (idx !== -1) fwdEdges.splice(idx, 1);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
graph.forward.delete(filePath);
|
|
316
|
+
graph.reverse.delete(filePath);
|
|
317
|
+
graph.files.delete(filePath);
|
|
318
|
+
}
|
|
319
|
+
function startWatcher(projectRoot2, graph, resolver) {
|
|
320
|
+
try {
|
|
321
|
+
const watcher = fs.watch(
|
|
322
|
+
projectRoot2,
|
|
323
|
+
{ recursive: true },
|
|
324
|
+
(_eventType, filename) => {
|
|
325
|
+
if (!filename) return;
|
|
326
|
+
const ext = path2.extname(filename);
|
|
327
|
+
if (!TS_EXTENSIONS.has(ext)) return;
|
|
328
|
+
const parts = filename.split(path2.sep);
|
|
329
|
+
if (parts.some((p2) => SKIP_DIRS.has(p2))) return;
|
|
330
|
+
if (SKIP_FILES.has(path2.basename(filename))) return;
|
|
331
|
+
if (filename.endsWith(".d.ts") || filename.endsWith(".d.mts") || filename.endsWith(".d.cts"))
|
|
332
|
+
return;
|
|
333
|
+
const absPath2 = path2.resolve(projectRoot2, filename);
|
|
334
|
+
if (fs.existsSync(absPath2)) {
|
|
335
|
+
updateFile(graph, absPath2, resolver, projectRoot2);
|
|
336
|
+
} else {
|
|
337
|
+
removeFile(graph, absPath2);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
);
|
|
341
|
+
process.on("SIGINT", () => watcher.close());
|
|
342
|
+
process.on("SIGTERM", () => watcher.close());
|
|
343
|
+
log("File watcher started");
|
|
344
|
+
} catch (err) {
|
|
345
|
+
log("Failed to start file watcher:", err);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
var log, TS_EXTENSIONS, SKIP_DIRS, SKIP_FILES, SOURCE_EXTS;
|
|
349
|
+
var init_module_graph = __esm({
|
|
350
|
+
"module-graph.ts"() {
|
|
351
|
+
log = (...args2) => console.error("[typegraph/graph]", ...args2);
|
|
352
|
+
TS_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts"]);
|
|
353
|
+
SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
354
|
+
"node_modules",
|
|
355
|
+
"dist",
|
|
356
|
+
"build",
|
|
357
|
+
"out",
|
|
358
|
+
".wrangler",
|
|
359
|
+
".mf",
|
|
360
|
+
".git",
|
|
361
|
+
".next",
|
|
362
|
+
".turbo",
|
|
363
|
+
"coverage"
|
|
364
|
+
]);
|
|
365
|
+
SKIP_FILES = /* @__PURE__ */ new Set(["routeTree.gen.ts"]);
|
|
366
|
+
SOURCE_EXTS = [".ts", ".tsx", ".mts", ".cts"];
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
27
370
|
// check.ts
|
|
28
371
|
var check_exports = {};
|
|
29
372
|
__export(check_exports, {
|
|
30
373
|
main: () => main
|
|
31
374
|
});
|
|
32
|
-
import * as
|
|
33
|
-
import * as
|
|
375
|
+
import * as fs2 from "fs";
|
|
376
|
+
import * as path3 from "path";
|
|
34
377
|
import { createRequire } from "module";
|
|
35
378
|
import { spawn } from "child_process";
|
|
36
379
|
function findFirstTsFile(dir) {
|
|
37
380
|
const skipDirs = /* @__PURE__ */ new Set(["node_modules", "dist", ".git", ".wrangler", "coverage"]);
|
|
38
381
|
try {
|
|
39
|
-
for (const entry of
|
|
382
|
+
for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
|
|
40
383
|
if (entry.isFile() && entry.name.endsWith(".ts") && !entry.name.endsWith(".d.ts")) {
|
|
41
|
-
return
|
|
384
|
+
return path3.join(dir, entry.name);
|
|
42
385
|
}
|
|
43
386
|
}
|
|
44
|
-
for (const entry of
|
|
387
|
+
for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
|
|
45
388
|
if (entry.isDirectory() && !skipDirs.has(entry.name) && !entry.name.startsWith(".")) {
|
|
46
|
-
const found = findFirstTsFile(
|
|
389
|
+
const found = findFirstTsFile(path3.join(dir, entry.name));
|
|
47
390
|
if (found) return found;
|
|
48
391
|
}
|
|
49
392
|
}
|
|
@@ -55,7 +398,7 @@ function testTsserver(projectRoot2) {
|
|
|
55
398
|
return new Promise((resolve8) => {
|
|
56
399
|
let tsserverPath;
|
|
57
400
|
try {
|
|
58
|
-
const require2 = createRequire(
|
|
401
|
+
const require2 = createRequire(path3.resolve(projectRoot2, "package.json"));
|
|
59
402
|
tsserverPath = require2.resolve("typescript/lib/tsserver.js");
|
|
60
403
|
} catch {
|
|
61
404
|
resolve8(false);
|
|
@@ -130,8 +473,8 @@ async function main(configOverride) {
|
|
|
130
473
|
} else {
|
|
131
474
|
fail(`Node.js ${nodeVersion} is too old`, "Upgrade Node.js to >= 18");
|
|
132
475
|
}
|
|
133
|
-
const tsxInRoot =
|
|
134
|
-
const tsxInTool =
|
|
476
|
+
const tsxInRoot = fs2.existsSync(path3.join(projectRoot2, "node_modules/.bin/tsx"));
|
|
477
|
+
const tsxInTool = fs2.existsSync(path3.join(toolDir, "node_modules/.bin/tsx"));
|
|
135
478
|
if (tsxInRoot || tsxInTool) {
|
|
136
479
|
pass(`tsx available (in ${tsxInRoot ? "project" : "tool"} node_modules)`);
|
|
137
480
|
} else {
|
|
@@ -139,10 +482,10 @@ async function main(configOverride) {
|
|
|
139
482
|
}
|
|
140
483
|
let tsVersion = null;
|
|
141
484
|
try {
|
|
142
|
-
const require2 = createRequire(
|
|
485
|
+
const require2 = createRequire(path3.resolve(projectRoot2, "package.json"));
|
|
143
486
|
const tsserverPath = require2.resolve("typescript/lib/tsserver.js");
|
|
144
|
-
const tsPkgPath =
|
|
145
|
-
const tsPkg = JSON.parse(
|
|
487
|
+
const tsPkgPath = path3.resolve(path3.dirname(tsserverPath), "..", "package.json");
|
|
488
|
+
const tsPkg = JSON.parse(fs2.readFileSync(tsPkgPath, "utf-8"));
|
|
146
489
|
tsVersion = tsPkg.version;
|
|
147
490
|
pass(`TypeScript found (v${tsVersion})`);
|
|
148
491
|
} catch {
|
|
@@ -151,23 +494,23 @@ async function main(configOverride) {
|
|
|
151
494
|
"Add `typescript` to devDependencies and run `npm install`"
|
|
152
495
|
);
|
|
153
496
|
}
|
|
154
|
-
const tsconfigAbs =
|
|
155
|
-
if (
|
|
497
|
+
const tsconfigAbs = path3.resolve(projectRoot2, tsconfigPath2);
|
|
498
|
+
if (fs2.existsSync(tsconfigAbs)) {
|
|
156
499
|
pass(`tsconfig.json exists at ${tsconfigPath2}`);
|
|
157
500
|
} else {
|
|
158
501
|
fail(`tsconfig.json not found at ${tsconfigPath2}`, `Create a tsconfig.json at ${tsconfigPath2}`);
|
|
159
502
|
}
|
|
160
|
-
const pluginMcpPath =
|
|
161
|
-
const hasPluginMcp =
|
|
503
|
+
const pluginMcpPath = path3.join(toolDir, ".mcp.json");
|
|
504
|
+
const hasPluginMcp = fs2.existsSync(pluginMcpPath) && fs2.existsSync(path3.join(toolDir, ".claude-plugin/plugin.json"));
|
|
162
505
|
if (process.env.CLAUDE_PLUGIN_ROOT) {
|
|
163
506
|
pass("MCP registered via plugin (CLAUDE_PLUGIN_ROOT set)");
|
|
164
507
|
} else if (hasPluginMcp) {
|
|
165
508
|
pass("MCP registered via plugin (.mcp.json + .claude-plugin/ present)");
|
|
166
509
|
} else {
|
|
167
|
-
const mcpJsonPath =
|
|
168
|
-
if (
|
|
510
|
+
const mcpJsonPath = path3.resolve(projectRoot2, ".claude/mcp.json");
|
|
511
|
+
if (fs2.existsSync(mcpJsonPath)) {
|
|
169
512
|
try {
|
|
170
|
-
const mcpJson = JSON.parse(
|
|
513
|
+
const mcpJson = JSON.parse(fs2.readFileSync(mcpJsonPath, "utf-8"));
|
|
171
514
|
const tsNav = mcpJson?.mcpServers?.["typegraph"];
|
|
172
515
|
if (tsNav) {
|
|
173
516
|
const hasCommand = tsNav.command === "npx";
|
|
@@ -186,7 +529,7 @@ async function main(configOverride) {
|
|
|
186
529
|
);
|
|
187
530
|
}
|
|
188
531
|
} else {
|
|
189
|
-
const serverPath = toolIsEmbedded ? `./${toolRelPath}/server.ts` :
|
|
532
|
+
const serverPath = toolIsEmbedded ? `./${toolRelPath}/server.ts` : path3.resolve(toolDir, "server.ts");
|
|
190
533
|
fail(
|
|
191
534
|
"MCP entry 'typegraph' not found in .claude/mcp.json",
|
|
192
535
|
`Add to .claude/mcp.json:
|
|
@@ -211,11 +554,11 @@ async function main(configOverride) {
|
|
|
211
554
|
fail(".claude/mcp.json not found", `Create .claude/mcp.json with typegraph server registration`);
|
|
212
555
|
}
|
|
213
556
|
}
|
|
214
|
-
const toolNodeModules =
|
|
215
|
-
if (
|
|
557
|
+
const toolNodeModules = path3.join(toolDir, "node_modules");
|
|
558
|
+
if (fs2.existsSync(toolNodeModules)) {
|
|
216
559
|
const requiredPkgs = ["@modelcontextprotocol/sdk", "oxc-parser", "oxc-resolver", "zod"];
|
|
217
560
|
const missing = requiredPkgs.filter(
|
|
218
|
-
(pkg) => !
|
|
561
|
+
(pkg) => !fs2.existsSync(path3.join(toolNodeModules, ...pkg.split("/")))
|
|
219
562
|
);
|
|
220
563
|
if (missing.length === 0) {
|
|
221
564
|
pass(`Dependencies installed (${requiredPkgs.length} packages)`);
|
|
@@ -226,7 +569,7 @@ async function main(configOverride) {
|
|
|
226
569
|
fail("typegraph-mcp dependencies not installed", `Run \`cd ${toolRelPath} && npm install\``);
|
|
227
570
|
}
|
|
228
571
|
try {
|
|
229
|
-
const oxcParserReq = createRequire(
|
|
572
|
+
const oxcParserReq = createRequire(path3.join(toolDir, "package.json"));
|
|
230
573
|
const { parseSync: parseSync2 } = await import(oxcParserReq.resolve("oxc-parser"));
|
|
231
574
|
const result = parseSync2("test.ts", 'import { x } from "./y";');
|
|
232
575
|
if (result.module?.staticImports?.length === 1) {
|
|
@@ -244,7 +587,7 @@ async function main(configOverride) {
|
|
|
244
587
|
);
|
|
245
588
|
}
|
|
246
589
|
try {
|
|
247
|
-
const oxcResolverReq = createRequire(
|
|
590
|
+
const oxcResolverReq = createRequire(path3.join(toolDir, "package.json"));
|
|
248
591
|
const { ResolverFactory: ResolverFactory2 } = await import(oxcResolverReq.resolve("oxc-resolver"));
|
|
249
592
|
const resolver = new ResolverFactory2({
|
|
250
593
|
tsconfig: { configFile: tsconfigAbs, references: "auto" },
|
|
@@ -254,8 +597,8 @@ async function main(configOverride) {
|
|
|
254
597
|
let resolveOk = false;
|
|
255
598
|
const testFile = findFirstTsFile(projectRoot2);
|
|
256
599
|
if (testFile) {
|
|
257
|
-
const dir =
|
|
258
|
-
const base = "./" +
|
|
600
|
+
const dir = path3.dirname(testFile);
|
|
601
|
+
const base = "./" + path3.basename(testFile);
|
|
259
602
|
const result = resolver.sync(dir, base);
|
|
260
603
|
resolveOk = !!result.path;
|
|
261
604
|
}
|
|
@@ -294,7 +637,12 @@ async function main(configOverride) {
|
|
|
294
637
|
skip("tsserver test (TypeScript not found)");
|
|
295
638
|
}
|
|
296
639
|
try {
|
|
297
|
-
|
|
640
|
+
let buildGraph2;
|
|
641
|
+
try {
|
|
642
|
+
({ buildGraph: buildGraph2 } = await import(path3.resolve(toolDir, "module-graph.js")));
|
|
643
|
+
} catch {
|
|
644
|
+
({ buildGraph: buildGraph2 } = await Promise.resolve().then(() => (init_module_graph(), module_graph_exports)));
|
|
645
|
+
}
|
|
298
646
|
const start2 = performance.now();
|
|
299
647
|
const { graph } = await buildGraph2(projectRoot2, tsconfigPath2);
|
|
300
648
|
const elapsed = (performance.now() - start2).toFixed(0);
|
|
@@ -322,10 +670,10 @@ async function main(configOverride) {
|
|
|
322
670
|
);
|
|
323
671
|
}
|
|
324
672
|
if (toolIsEmbedded) {
|
|
325
|
-
const eslintConfigPath =
|
|
326
|
-
if (
|
|
327
|
-
const eslintContent =
|
|
328
|
-
const parentDir =
|
|
673
|
+
const eslintConfigPath = path3.resolve(projectRoot2, "eslint.config.mjs");
|
|
674
|
+
if (fs2.existsSync(eslintConfigPath)) {
|
|
675
|
+
const eslintContent = fs2.readFileSync(eslintConfigPath, "utf-8");
|
|
676
|
+
const parentDir = path3.basename(path3.dirname(toolDir));
|
|
329
677
|
const parentIgnorePattern = new RegExp(`["']${parentDir}\\/\\*\\*["']`);
|
|
330
678
|
const hasParentIgnore = parentIgnorePattern.test(eslintContent);
|
|
331
679
|
if (hasParentIgnore) {
|
|
@@ -343,14 +691,14 @@ async function main(configOverride) {
|
|
|
343
691
|
} else {
|
|
344
692
|
skip("ESLint config check (typegraph-mcp is external to project)");
|
|
345
693
|
}
|
|
346
|
-
const gitignorePath =
|
|
347
|
-
if (
|
|
348
|
-
const gitignoreContent =
|
|
694
|
+
const gitignorePath = path3.resolve(projectRoot2, ".gitignore");
|
|
695
|
+
if (fs2.existsSync(gitignorePath)) {
|
|
696
|
+
const gitignoreContent = fs2.readFileSync(gitignorePath, "utf-8");
|
|
349
697
|
const lines = gitignoreContent.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
350
698
|
const ignoresClaude = lines.some(
|
|
351
699
|
(l) => l === ".claude/" || l === ".claude" || l === "/.claude"
|
|
352
700
|
);
|
|
353
|
-
const parentDir = toolIsEmbedded ?
|
|
701
|
+
const parentDir = toolIsEmbedded ? path3.basename(path3.dirname(toolDir)) : null;
|
|
354
702
|
const ignoresParent = parentDir && lines.some((l) => l === `${parentDir}/` || l === parentDir || l === `/${parentDir}`);
|
|
355
703
|
if (!ignoresParent && !ignoresClaude) {
|
|
356
704
|
pass(".gitignore does not exclude .claude/" + (parentDir ? ` or ${parentDir}/` : ""));
|
|
@@ -380,29 +728,21 @@ async function main(configOverride) {
|
|
|
380
728
|
console.log("");
|
|
381
729
|
return { passed, failed, warned };
|
|
382
730
|
}
|
|
383
|
-
var isDirectRun;
|
|
384
731
|
var init_check = __esm({
|
|
385
732
|
"check.ts"() {
|
|
386
733
|
init_config();
|
|
387
|
-
isDirectRun = process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(new URL(import.meta.url).pathname);
|
|
388
|
-
if (isDirectRun) {
|
|
389
|
-
main().then((result) => process.exit(result.failed > 0 ? 1 : 0)).catch((err) => {
|
|
390
|
-
console.error("Fatal error:", err);
|
|
391
|
-
process.exit(1);
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
734
|
}
|
|
395
735
|
});
|
|
396
736
|
|
|
397
737
|
// tsserver-client.ts
|
|
398
738
|
import { spawn as spawn2 } from "child_process";
|
|
399
|
-
import * as
|
|
400
|
-
import * as
|
|
739
|
+
import * as path4 from "path";
|
|
740
|
+
import * as fs3 from "fs";
|
|
401
741
|
import { createRequire as createRequire2 } from "module";
|
|
402
|
-
var
|
|
742
|
+
var log2, REQUEST_TIMEOUT_MS, TsServerClient;
|
|
403
743
|
var init_tsserver_client = __esm({
|
|
404
744
|
"tsserver-client.ts"() {
|
|
405
|
-
|
|
745
|
+
log2 = (...args2) => console.error("[typegraph/tsserver]", ...args2);
|
|
406
746
|
REQUEST_TIMEOUT_MS = 1e4;
|
|
407
747
|
TsServerClient = class {
|
|
408
748
|
constructor(projectRoot2, tsconfigPath2 = "./tsconfig.json") {
|
|
@@ -420,16 +760,16 @@ var init_tsserver_client = __esm({
|
|
|
420
760
|
maxRestarts = 3;
|
|
421
761
|
// ─── Path Resolution ────────────────────────────────────────────────────
|
|
422
762
|
resolvePath(file) {
|
|
423
|
-
return
|
|
763
|
+
return path4.isAbsolute(file) ? file : path4.resolve(this.projectRoot, file);
|
|
424
764
|
}
|
|
425
765
|
relativePath(file) {
|
|
426
|
-
return
|
|
766
|
+
return path4.relative(this.projectRoot, file);
|
|
427
767
|
}
|
|
428
768
|
/** Read a line from a file (1-based line number). Returns trimmed content. */
|
|
429
769
|
readLine(file, line) {
|
|
430
770
|
try {
|
|
431
771
|
const absPath2 = this.resolvePath(file);
|
|
432
|
-
const content =
|
|
772
|
+
const content = fs3.readFileSync(absPath2, "utf-8");
|
|
433
773
|
const lines = content.split("\n");
|
|
434
774
|
return lines[line - 1]?.trim() ?? "";
|
|
435
775
|
} catch {
|
|
@@ -439,11 +779,11 @@ var init_tsserver_client = __esm({
|
|
|
439
779
|
// ─── Lifecycle ──────────────────────────────────────────────────────────
|
|
440
780
|
async start() {
|
|
441
781
|
if (this.child) return;
|
|
442
|
-
const require2 = createRequire2(
|
|
782
|
+
const require2 = createRequire2(path4.resolve(this.projectRoot, "package.json"));
|
|
443
783
|
const tsserverPath = require2.resolve("typescript/lib/tsserver.js");
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
784
|
+
log2(`Spawning tsserver: ${tsserverPath}`);
|
|
785
|
+
log2(`Project root: ${this.projectRoot}`);
|
|
786
|
+
log2(`tsconfig: ${this.tsconfigPath}`);
|
|
447
787
|
this.child = spawn2("node", [tsserverPath, "--disableAutomaticTypingAcquisition"], {
|
|
448
788
|
cwd: this.projectRoot,
|
|
449
789
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -452,16 +792,16 @@ var init_tsserver_client = __esm({
|
|
|
452
792
|
this.child.stdout.on("data", (chunk) => this.onData(chunk));
|
|
453
793
|
this.child.stderr.on("data", (chunk) => {
|
|
454
794
|
const text = chunk.toString().trim();
|
|
455
|
-
if (text)
|
|
795
|
+
if (text) log2(`[stderr] ${text}`);
|
|
456
796
|
});
|
|
457
797
|
this.child.on("close", (code) => {
|
|
458
|
-
|
|
798
|
+
log2(`tsserver exited with code ${code}`);
|
|
459
799
|
this.child = null;
|
|
460
800
|
this.rejectAllPending(new Error(`tsserver exited with code ${code}`));
|
|
461
801
|
this.tryRestart();
|
|
462
802
|
});
|
|
463
803
|
this.child.on("error", (err) => {
|
|
464
|
-
|
|
804
|
+
log2(`tsserver error: ${err.message}`);
|
|
465
805
|
this.rejectAllPending(err);
|
|
466
806
|
});
|
|
467
807
|
await this.sendRequest("configure", {
|
|
@@ -471,13 +811,13 @@ var init_tsserver_client = __esm({
|
|
|
471
811
|
});
|
|
472
812
|
const warmStart = performance.now();
|
|
473
813
|
const tsconfigAbs = this.resolvePath(this.tsconfigPath);
|
|
474
|
-
if (
|
|
814
|
+
if (fs3.existsSync(tsconfigAbs)) {
|
|
475
815
|
await this.sendRequest("compilerOptionsForInferredProjects", {
|
|
476
816
|
options: { allowJs: true, checkJs: false }
|
|
477
817
|
});
|
|
478
818
|
}
|
|
479
819
|
this.ready = true;
|
|
480
|
-
|
|
820
|
+
log2(`Ready [${(performance.now() - warmStart).toFixed(0)}ms configure]`);
|
|
481
821
|
}
|
|
482
822
|
shutdown() {
|
|
483
823
|
this.shuttingDown = true;
|
|
@@ -490,11 +830,11 @@ var init_tsserver_client = __esm({
|
|
|
490
830
|
tryRestart() {
|
|
491
831
|
if (this.shuttingDown) return;
|
|
492
832
|
if (this.restartCount >= this.maxRestarts) {
|
|
493
|
-
|
|
833
|
+
log2(`Max restarts (${this.maxRestarts}) reached, not restarting`);
|
|
494
834
|
return;
|
|
495
835
|
}
|
|
496
836
|
this.restartCount++;
|
|
497
|
-
|
|
837
|
+
log2(`Restarting tsserver (attempt ${this.restartCount})...`);
|
|
498
838
|
this.buffer = Buffer.alloc(0);
|
|
499
839
|
const filesToReopen = [...this.openFiles];
|
|
500
840
|
this.openFiles.clear();
|
|
@@ -538,7 +878,7 @@ var init_tsserver_client = __esm({
|
|
|
538
878
|
const message = JSON.parse(bodyBytes.toString("utf-8"));
|
|
539
879
|
this.onMessage(message);
|
|
540
880
|
} catch {
|
|
541
|
-
|
|
881
|
+
log2("Failed to parse tsserver message");
|
|
542
882
|
}
|
|
543
883
|
}
|
|
544
884
|
}
|
|
@@ -551,449 +891,115 @@ var init_tsserver_client = __esm({
|
|
|
551
891
|
if (message.success) {
|
|
552
892
|
pending.resolve(message.body);
|
|
553
893
|
} else {
|
|
554
|
-
pending.reject(
|
|
555
|
-
new Error(`tsserver ${pending.command} failed: ${message.message ?? "unknown error"}`)
|
|
556
|
-
);
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
// ─── Protocol: Sending ──────────────────────────────────────────────────
|
|
562
|
-
sendRequest(command2, args2) {
|
|
563
|
-
if (!this.child?.stdin?.writable) {
|
|
564
|
-
return Promise.reject(new Error("tsserver not running"));
|
|
565
|
-
}
|
|
566
|
-
const seq = ++this.seq;
|
|
567
|
-
const request = {
|
|
568
|
-
seq,
|
|
569
|
-
type: "request",
|
|
570
|
-
command: command2,
|
|
571
|
-
arguments: args2
|
|
572
|
-
};
|
|
573
|
-
return new Promise((resolve8, reject) => {
|
|
574
|
-
const timer = setTimeout(() => {
|
|
575
|
-
this.pending.delete(seq);
|
|
576
|
-
reject(new Error(`tsserver ${command2} timed out after ${REQUEST_TIMEOUT_MS}ms`));
|
|
577
|
-
}, REQUEST_TIMEOUT_MS);
|
|
578
|
-
this.pending.set(seq, { resolve: resolve8, reject, timer, command: command2 });
|
|
579
|
-
this.child.stdin.write(JSON.stringify(request) + "\n");
|
|
580
|
-
});
|
|
581
|
-
}
|
|
582
|
-
// Fire-and-forget — for commands like `open` that may not send a response
|
|
583
|
-
sendNotification(command2, args2) {
|
|
584
|
-
if (!this.child?.stdin?.writable) return;
|
|
585
|
-
const seq = ++this.seq;
|
|
586
|
-
const request = { seq, type: "request", command: command2, arguments: args2 };
|
|
587
|
-
this.child.stdin.write(JSON.stringify(request) + "\n");
|
|
588
|
-
}
|
|
589
|
-
// ─── File Management ───────────────────────────────────────────────────
|
|
590
|
-
async ensureOpen(file) {
|
|
591
|
-
const absPath2 = this.resolvePath(file);
|
|
592
|
-
if (this.openFiles.has(absPath2)) return;
|
|
593
|
-
this.openFiles.add(absPath2);
|
|
594
|
-
this.sendNotification("open", { file: absPath2 });
|
|
595
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
596
|
-
}
|
|
597
|
-
// ─── Public API ────────────────────────────────────────────────────────
|
|
598
|
-
async definition(file, line, offset) {
|
|
599
|
-
const absPath2 = this.resolvePath(file);
|
|
600
|
-
await this.ensureOpen(absPath2);
|
|
601
|
-
const body = await this.sendRequest("definition", {
|
|
602
|
-
file: absPath2,
|
|
603
|
-
line,
|
|
604
|
-
offset
|
|
605
|
-
});
|
|
606
|
-
if (!body || !Array.isArray(body)) return [];
|
|
607
|
-
return body.map((d) => ({
|
|
608
|
-
...d,
|
|
609
|
-
file: this.relativePath(d.file)
|
|
610
|
-
}));
|
|
611
|
-
}
|
|
612
|
-
async references(file, line, offset) {
|
|
613
|
-
const absPath2 = this.resolvePath(file);
|
|
614
|
-
await this.ensureOpen(absPath2);
|
|
615
|
-
const body = await this.sendRequest("references", {
|
|
616
|
-
file: absPath2,
|
|
617
|
-
line,
|
|
618
|
-
offset
|
|
619
|
-
});
|
|
620
|
-
if (!body?.refs) return [];
|
|
621
|
-
return body.refs.map((r) => ({
|
|
622
|
-
...r,
|
|
623
|
-
file: this.relativePath(r.file)
|
|
624
|
-
}));
|
|
625
|
-
}
|
|
626
|
-
async quickinfo(file, line, offset) {
|
|
627
|
-
const absPath2 = this.resolvePath(file);
|
|
628
|
-
await this.ensureOpen(absPath2);
|
|
629
|
-
try {
|
|
630
|
-
const body = await this.sendRequest("quickinfo", {
|
|
631
|
-
file: absPath2,
|
|
632
|
-
line,
|
|
633
|
-
offset
|
|
634
|
-
});
|
|
635
|
-
return body ?? null;
|
|
636
|
-
} catch {
|
|
637
|
-
return null;
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
async navto(searchValue, maxResults = 10, file) {
|
|
641
|
-
if (file) await this.ensureOpen(file);
|
|
642
|
-
const args2 = {
|
|
643
|
-
searchValue,
|
|
644
|
-
maxResultCount: maxResults
|
|
645
|
-
};
|
|
646
|
-
if (file) args2["file"] = this.resolvePath(file);
|
|
647
|
-
const body = await this.sendRequest("navto", args2);
|
|
648
|
-
if (!body || !Array.isArray(body)) return [];
|
|
649
|
-
return body.map((item) => ({
|
|
650
|
-
...item,
|
|
651
|
-
file: this.relativePath(item.file)
|
|
652
|
-
}));
|
|
653
|
-
}
|
|
654
|
-
async navbar(file) {
|
|
655
|
-
const absPath2 = this.resolvePath(file);
|
|
656
|
-
await this.ensureOpen(absPath2);
|
|
657
|
-
const body = await this.sendRequest("navbar", {
|
|
658
|
-
file: absPath2
|
|
659
|
-
});
|
|
660
|
-
return body ?? [];
|
|
661
|
-
}
|
|
662
|
-
};
|
|
663
|
-
}
|
|
664
|
-
});
|
|
665
|
-
|
|
666
|
-
// module-graph.ts
|
|
667
|
-
import { parseSync } from "oxc-parser";
|
|
668
|
-
import { ResolverFactory } from "oxc-resolver";
|
|
669
|
-
import * as fs3 from "fs";
|
|
670
|
-
import * as path4 from "path";
|
|
671
|
-
function discoverFiles(rootDir) {
|
|
672
|
-
const files = [];
|
|
673
|
-
function walk(dir) {
|
|
674
|
-
let entries;
|
|
675
|
-
try {
|
|
676
|
-
entries = fs3.readdirSync(dir, { withFileTypes: true });
|
|
677
|
-
} catch {
|
|
678
|
-
return;
|
|
679
|
-
}
|
|
680
|
-
for (const entry of entries) {
|
|
681
|
-
if (entry.isDirectory()) {
|
|
682
|
-
if (SKIP_DIRS.has(entry.name)) continue;
|
|
683
|
-
if (entry.name.startsWith(".") && dir !== rootDir) continue;
|
|
684
|
-
walk(path4.join(dir, entry.name));
|
|
685
|
-
} else if (entry.isFile()) {
|
|
686
|
-
const name = entry.name;
|
|
687
|
-
if (SKIP_FILES.has(name)) continue;
|
|
688
|
-
if (name.endsWith(".d.ts") || name.endsWith(".d.mts") || name.endsWith(".d.cts")) continue;
|
|
689
|
-
const ext = path4.extname(name);
|
|
690
|
-
if (TS_EXTENSIONS.has(ext)) {
|
|
691
|
-
files.push(path4.join(dir, name));
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
walk(rootDir);
|
|
697
|
-
return files;
|
|
698
|
-
}
|
|
699
|
-
function parseFileImports(filePath, source) {
|
|
700
|
-
const result = parseSync(filePath, source);
|
|
701
|
-
const imports = [];
|
|
702
|
-
for (const imp of result.module.staticImports) {
|
|
703
|
-
const specifier = imp.moduleRequest.value;
|
|
704
|
-
const names = [];
|
|
705
|
-
let allTypeOnly = true;
|
|
706
|
-
for (const entry of imp.entries) {
|
|
707
|
-
const kind = entry.importName.kind;
|
|
708
|
-
const name = kind === "Default" ? "default" : kind === "All" || kind === "AllButDefault" || kind === "NamespaceObject" ? "*" : entry.importName.name ?? entry.localName.value;
|
|
709
|
-
names.push(name);
|
|
710
|
-
if (!entry.isType) allTypeOnly = false;
|
|
711
|
-
}
|
|
712
|
-
if (names.length === 0) {
|
|
713
|
-
imports.push({ specifier, names: ["*"], isTypeOnly: false, isDynamic: false });
|
|
714
|
-
} else {
|
|
715
|
-
imports.push({ specifier, names, isTypeOnly: allTypeOnly, isDynamic: false });
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
for (const exp of result.module.staticExports) {
|
|
719
|
-
for (const entry of exp.entries) {
|
|
720
|
-
const moduleRequest = entry.moduleRequest;
|
|
721
|
-
if (!moduleRequest) continue;
|
|
722
|
-
const specifier = moduleRequest.value;
|
|
723
|
-
const entryKind = entry.importName.kind;
|
|
724
|
-
const name = entryKind === "AllButDefault" || entryKind === "All" || entryKind === "NamespaceObject" ? "*" : entry.importName.name ?? "*";
|
|
725
|
-
const existing = imports.find((i) => i.specifier === specifier && !i.isDynamic);
|
|
726
|
-
if (existing) {
|
|
727
|
-
if (!existing.names.includes(name)) existing.names.push(name);
|
|
728
|
-
} else {
|
|
729
|
-
imports.push({ specifier, names: [name], isTypeOnly: false, isDynamic: false });
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
for (const di of result.module.dynamicImports) {
|
|
734
|
-
if (di.moduleRequest) {
|
|
735
|
-
const sliced = source.slice(di.moduleRequest.start, di.moduleRequest.end);
|
|
736
|
-
if (sliced.startsWith("'") || sliced.startsWith('"')) {
|
|
737
|
-
const specifier = sliced.slice(1, -1);
|
|
738
|
-
imports.push({ specifier, names: ["*"], isTypeOnly: false, isDynamic: true });
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
return imports;
|
|
743
|
-
}
|
|
744
|
-
function distToSource(resolvedPath, projectRoot2) {
|
|
745
|
-
if (!resolvedPath.startsWith(projectRoot2)) return resolvedPath;
|
|
746
|
-
const rel2 = path4.relative(projectRoot2, resolvedPath);
|
|
747
|
-
const distIdx = rel2.indexOf("dist" + path4.sep);
|
|
748
|
-
if (distIdx === -1) return resolvedPath;
|
|
749
|
-
const prefix = rel2.slice(0, distIdx);
|
|
750
|
-
const afterDist = rel2.slice(distIdx + 5);
|
|
751
|
-
const withoutExt = afterDist.replace(/\.(m?j|c)s$/, "");
|
|
752
|
-
for (const ext of SOURCE_EXTS) {
|
|
753
|
-
const candidate = path4.resolve(projectRoot2, prefix, "src", withoutExt + ext);
|
|
754
|
-
if (fs3.existsSync(candidate)) return candidate;
|
|
755
|
-
}
|
|
756
|
-
for (const ext of SOURCE_EXTS) {
|
|
757
|
-
const candidate = path4.resolve(projectRoot2, prefix, withoutExt + ext);
|
|
758
|
-
if (fs3.existsSync(candidate)) return candidate;
|
|
759
|
-
}
|
|
760
|
-
if (withoutExt.endsWith("/index")) {
|
|
761
|
-
const dirPath = withoutExt.slice(0, -6);
|
|
762
|
-
for (const ext of SOURCE_EXTS) {
|
|
763
|
-
const candidate = path4.resolve(projectRoot2, prefix, "src", dirPath + ext);
|
|
764
|
-
if (fs3.existsSync(candidate)) return candidate;
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
return resolvedPath;
|
|
768
|
-
}
|
|
769
|
-
function resolveImport(resolver, fromDir, specifier, projectRoot2) {
|
|
770
|
-
try {
|
|
771
|
-
const result = resolver.sync(fromDir, specifier);
|
|
772
|
-
if (result.path && !result.path.includes("node_modules")) {
|
|
773
|
-
const mapped = distToSource(result.path, projectRoot2);
|
|
774
|
-
const ext = path4.extname(mapped);
|
|
775
|
-
if (!TS_EXTENSIONS.has(ext)) return null;
|
|
776
|
-
if (SKIP_FILES.has(path4.basename(mapped))) return null;
|
|
777
|
-
return mapped;
|
|
778
|
-
}
|
|
779
|
-
} catch {
|
|
780
|
-
}
|
|
781
|
-
return null;
|
|
782
|
-
}
|
|
783
|
-
function createResolver(projectRoot2, tsconfigPath2) {
|
|
784
|
-
return new ResolverFactory({
|
|
785
|
-
tsconfig: {
|
|
786
|
-
configFile: path4.resolve(projectRoot2, tsconfigPath2),
|
|
787
|
-
references: "auto"
|
|
788
|
-
},
|
|
789
|
-
extensions: [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"],
|
|
790
|
-
extensionAlias: {
|
|
791
|
-
".js": [".ts", ".tsx", ".js"],
|
|
792
|
-
".jsx": [".tsx", ".jsx"],
|
|
793
|
-
".mjs": [".mts", ".mjs"],
|
|
794
|
-
".cjs": [".cts", ".cjs"]
|
|
795
|
-
},
|
|
796
|
-
conditionNames: ["import", "require"],
|
|
797
|
-
mainFields: ["module", "main"]
|
|
798
|
-
});
|
|
799
|
-
}
|
|
800
|
-
function buildForwardEdges(files, resolver, projectRoot2) {
|
|
801
|
-
const forward = /* @__PURE__ */ new Map();
|
|
802
|
-
const parseFailures = [];
|
|
803
|
-
for (const filePath of files) {
|
|
804
|
-
let source;
|
|
805
|
-
try {
|
|
806
|
-
source = fs3.readFileSync(filePath, "utf-8");
|
|
807
|
-
} catch {
|
|
808
|
-
continue;
|
|
809
|
-
}
|
|
810
|
-
let rawImports;
|
|
811
|
-
try {
|
|
812
|
-
rawImports = parseFileImports(filePath, source);
|
|
813
|
-
} catch (err) {
|
|
814
|
-
parseFailures.push(filePath);
|
|
815
|
-
continue;
|
|
816
|
-
}
|
|
817
|
-
const edges = [];
|
|
818
|
-
const fromDir = path4.dirname(filePath);
|
|
819
|
-
for (const raw of rawImports) {
|
|
820
|
-
const target = resolveImport(resolver, fromDir, raw.specifier, projectRoot2);
|
|
821
|
-
if (target) {
|
|
822
|
-
edges.push({
|
|
823
|
-
target,
|
|
824
|
-
specifiers: raw.names,
|
|
825
|
-
isTypeOnly: raw.isTypeOnly,
|
|
826
|
-
isDynamic: raw.isDynamic
|
|
894
|
+
pending.reject(
|
|
895
|
+
new Error(`tsserver ${pending.command} failed: ${message.message ?? "unknown error"}`)
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
// ─── Protocol: Sending ──────────────────────────────────────────────────
|
|
902
|
+
sendRequest(command2, args2) {
|
|
903
|
+
if (!this.child?.stdin?.writable) {
|
|
904
|
+
return Promise.reject(new Error("tsserver not running"));
|
|
905
|
+
}
|
|
906
|
+
const seq = ++this.seq;
|
|
907
|
+
const request = {
|
|
908
|
+
seq,
|
|
909
|
+
type: "request",
|
|
910
|
+
command: command2,
|
|
911
|
+
arguments: args2
|
|
912
|
+
};
|
|
913
|
+
return new Promise((resolve8, reject) => {
|
|
914
|
+
const timer = setTimeout(() => {
|
|
915
|
+
this.pending.delete(seq);
|
|
916
|
+
reject(new Error(`tsserver ${command2} timed out after ${REQUEST_TIMEOUT_MS}ms`));
|
|
917
|
+
}, REQUEST_TIMEOUT_MS);
|
|
918
|
+
this.pending.set(seq, { resolve: resolve8, reject, timer, command: command2 });
|
|
919
|
+
this.child.stdin.write(JSON.stringify(request) + "\n");
|
|
827
920
|
});
|
|
828
921
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
const reverse = /* @__PURE__ */ new Map();
|
|
836
|
-
for (const [source, edges] of forward) {
|
|
837
|
-
for (const edge of edges) {
|
|
838
|
-
let revEdges = reverse.get(edge.target);
|
|
839
|
-
if (!revEdges) {
|
|
840
|
-
revEdges = [];
|
|
841
|
-
reverse.set(edge.target, revEdges);
|
|
922
|
+
// Fire-and-forget — for commands like `open` that may not send a response
|
|
923
|
+
sendNotification(command2, args2) {
|
|
924
|
+
if (!this.child?.stdin?.writable) return;
|
|
925
|
+
const seq = ++this.seq;
|
|
926
|
+
const request = { seq, type: "request", command: command2, arguments: args2 };
|
|
927
|
+
this.child.stdin.write(JSON.stringify(request) + "\n");
|
|
842
928
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
rawImports = parseFileImports(filePath, source);
|
|
893
|
-
} catch {
|
|
894
|
-
log2(`Parse error on update: ${filePath}`);
|
|
895
|
-
graph.forward.set(filePath, []);
|
|
896
|
-
return;
|
|
897
|
-
}
|
|
898
|
-
const fromDir = path4.dirname(filePath);
|
|
899
|
-
const newEdges = [];
|
|
900
|
-
for (const raw of rawImports) {
|
|
901
|
-
const target = resolveImport(resolver, fromDir, raw.specifier, projectRoot2);
|
|
902
|
-
if (target) {
|
|
903
|
-
newEdges.push({
|
|
904
|
-
target,
|
|
905
|
-
specifiers: raw.names,
|
|
906
|
-
isTypeOnly: raw.isTypeOnly,
|
|
907
|
-
isDynamic: raw.isDynamic
|
|
908
|
-
});
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
graph.forward.set(filePath, newEdges);
|
|
912
|
-
graph.files.add(filePath);
|
|
913
|
-
for (const edge of newEdges) {
|
|
914
|
-
let revEdges = graph.reverse.get(edge.target);
|
|
915
|
-
if (!revEdges) {
|
|
916
|
-
revEdges = [];
|
|
917
|
-
graph.reverse.set(edge.target, revEdges);
|
|
918
|
-
}
|
|
919
|
-
revEdges.push({
|
|
920
|
-
target: filePath,
|
|
921
|
-
specifiers: edge.specifiers,
|
|
922
|
-
isTypeOnly: edge.isTypeOnly,
|
|
923
|
-
isDynamic: edge.isDynamic
|
|
924
|
-
});
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
function removeFile(graph, filePath) {
|
|
928
|
-
const edges = graph.forward.get(filePath) ?? [];
|
|
929
|
-
for (const edge of edges) {
|
|
930
|
-
const revEdges2 = graph.reverse.get(edge.target);
|
|
931
|
-
if (revEdges2) {
|
|
932
|
-
const idx = revEdges2.findIndex((r) => r.target === filePath);
|
|
933
|
-
if (idx !== -1) revEdges2.splice(idx, 1);
|
|
934
|
-
if (revEdges2.length === 0) graph.reverse.delete(edge.target);
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
const revEdges = graph.reverse.get(filePath) ?? [];
|
|
938
|
-
for (const revEdge of revEdges) {
|
|
939
|
-
const fwdEdges = graph.forward.get(revEdge.target);
|
|
940
|
-
if (fwdEdges) {
|
|
941
|
-
const idx = fwdEdges.findIndex((e) => e.target === filePath);
|
|
942
|
-
if (idx !== -1) fwdEdges.splice(idx, 1);
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
graph.forward.delete(filePath);
|
|
946
|
-
graph.reverse.delete(filePath);
|
|
947
|
-
graph.files.delete(filePath);
|
|
948
|
-
}
|
|
949
|
-
function startWatcher(projectRoot2, graph, resolver) {
|
|
950
|
-
try {
|
|
951
|
-
const watcher = fs3.watch(
|
|
952
|
-
projectRoot2,
|
|
953
|
-
{ recursive: true },
|
|
954
|
-
(_eventType, filename) => {
|
|
955
|
-
if (!filename) return;
|
|
956
|
-
const ext = path4.extname(filename);
|
|
957
|
-
if (!TS_EXTENSIONS.has(ext)) return;
|
|
958
|
-
const parts = filename.split(path4.sep);
|
|
959
|
-
if (parts.some((p2) => SKIP_DIRS.has(p2))) return;
|
|
960
|
-
if (SKIP_FILES.has(path4.basename(filename))) return;
|
|
961
|
-
if (filename.endsWith(".d.ts") || filename.endsWith(".d.mts") || filename.endsWith(".d.cts"))
|
|
962
|
-
return;
|
|
963
|
-
const absPath2 = path4.resolve(projectRoot2, filename);
|
|
964
|
-
if (fs3.existsSync(absPath2)) {
|
|
965
|
-
updateFile(graph, absPath2, resolver, projectRoot2);
|
|
966
|
-
} else {
|
|
967
|
-
removeFile(graph, absPath2);
|
|
929
|
+
// ─── File Management ───────────────────────────────────────────────────
|
|
930
|
+
async ensureOpen(file) {
|
|
931
|
+
const absPath2 = this.resolvePath(file);
|
|
932
|
+
if (this.openFiles.has(absPath2)) return;
|
|
933
|
+
this.openFiles.add(absPath2);
|
|
934
|
+
this.sendNotification("open", { file: absPath2 });
|
|
935
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
936
|
+
}
|
|
937
|
+
// ─── Public API ────────────────────────────────────────────────────────
|
|
938
|
+
async definition(file, line, offset) {
|
|
939
|
+
const absPath2 = this.resolvePath(file);
|
|
940
|
+
await this.ensureOpen(absPath2);
|
|
941
|
+
const body = await this.sendRequest("definition", {
|
|
942
|
+
file: absPath2,
|
|
943
|
+
line,
|
|
944
|
+
offset
|
|
945
|
+
});
|
|
946
|
+
if (!body || !Array.isArray(body)) return [];
|
|
947
|
+
return body.map((d) => ({
|
|
948
|
+
...d,
|
|
949
|
+
file: this.relativePath(d.file)
|
|
950
|
+
}));
|
|
951
|
+
}
|
|
952
|
+
async references(file, line, offset) {
|
|
953
|
+
const absPath2 = this.resolvePath(file);
|
|
954
|
+
await this.ensureOpen(absPath2);
|
|
955
|
+
const body = await this.sendRequest("references", {
|
|
956
|
+
file: absPath2,
|
|
957
|
+
line,
|
|
958
|
+
offset
|
|
959
|
+
});
|
|
960
|
+
if (!body?.refs) return [];
|
|
961
|
+
return body.refs.map((r) => ({
|
|
962
|
+
...r,
|
|
963
|
+
file: this.relativePath(r.file)
|
|
964
|
+
}));
|
|
965
|
+
}
|
|
966
|
+
async quickinfo(file, line, offset) {
|
|
967
|
+
const absPath2 = this.resolvePath(file);
|
|
968
|
+
await this.ensureOpen(absPath2);
|
|
969
|
+
try {
|
|
970
|
+
const body = await this.sendRequest("quickinfo", {
|
|
971
|
+
file: absPath2,
|
|
972
|
+
line,
|
|
973
|
+
offset
|
|
974
|
+
});
|
|
975
|
+
return body ?? null;
|
|
976
|
+
} catch {
|
|
977
|
+
return null;
|
|
968
978
|
}
|
|
969
979
|
}
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
"coverage"
|
|
994
|
-
]);
|
|
995
|
-
SKIP_FILES = /* @__PURE__ */ new Set(["routeTree.gen.ts"]);
|
|
996
|
-
SOURCE_EXTS = [".ts", ".tsx", ".mts", ".cts"];
|
|
980
|
+
async navto(searchValue, maxResults = 10, file) {
|
|
981
|
+
if (file) await this.ensureOpen(file);
|
|
982
|
+
const args2 = {
|
|
983
|
+
searchValue,
|
|
984
|
+
maxResultCount: maxResults
|
|
985
|
+
};
|
|
986
|
+
if (file) args2["file"] = this.resolvePath(file);
|
|
987
|
+
const body = await this.sendRequest("navto", args2);
|
|
988
|
+
if (!body || !Array.isArray(body)) return [];
|
|
989
|
+
return body.map((item) => ({
|
|
990
|
+
...item,
|
|
991
|
+
file: this.relativePath(item.file)
|
|
992
|
+
}));
|
|
993
|
+
}
|
|
994
|
+
async navbar(file) {
|
|
995
|
+
const absPath2 = this.resolvePath(file);
|
|
996
|
+
await this.ensureOpen(absPath2);
|
|
997
|
+
const body = await this.sendRequest("navbar", {
|
|
998
|
+
file: absPath2
|
|
999
|
+
});
|
|
1000
|
+
return body ?? [];
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
997
1003
|
}
|
|
998
1004
|
});
|
|
999
1005
|
|
|
@@ -1633,7 +1639,7 @@ async function main2(configOverride) {
|
|
|
1633
1639
|
console.log("");
|
|
1634
1640
|
return { passed, failed, skipped };
|
|
1635
1641
|
}
|
|
1636
|
-
var SKIP_DIRS2
|
|
1642
|
+
var SKIP_DIRS2;
|
|
1637
1643
|
var init_smoke_test = __esm({
|
|
1638
1644
|
"smoke-test.ts"() {
|
|
1639
1645
|
init_tsserver_client();
|
|
@@ -1649,13 +1655,6 @@ var init_smoke_test = __esm({
|
|
|
1649
1655
|
"coverage",
|
|
1650
1656
|
"out"
|
|
1651
1657
|
]);
|
|
1652
|
-
isDirectRun2 = process.argv[1] && fs5.realpathSync(process.argv[1]) === fs5.realpathSync(new URL(import.meta.url).pathname);
|
|
1653
|
-
if (isDirectRun2) {
|
|
1654
|
-
main2().then((result) => process.exit(result.failed > 0 ? 1 : 0)).catch((err) => {
|
|
1655
|
-
console.error("Fatal:", err);
|
|
1656
|
-
process.exit(1);
|
|
1657
|
-
});
|
|
1658
|
-
}
|
|
1659
1658
|
}
|
|
1660
1659
|
});
|
|
1661
1660
|
|
|
@@ -2782,14 +2781,21 @@ async function remove(yes2) {
|
|
|
2782
2781
|
}
|
|
2783
2782
|
await removePlugin(projectRoot2, pluginDir);
|
|
2784
2783
|
}
|
|
2784
|
+
function resolvePluginDir() {
|
|
2785
|
+
const installed = path8.resolve(process.cwd(), PLUGIN_DIR_NAME);
|
|
2786
|
+
if (fs7.existsSync(installed)) return installed;
|
|
2787
|
+
return path8.basename(import.meta.dirname) === "dist" ? path8.resolve(import.meta.dirname, "..") : import.meta.dirname;
|
|
2788
|
+
}
|
|
2785
2789
|
async function check() {
|
|
2790
|
+
const config = resolveConfig(resolvePluginDir());
|
|
2786
2791
|
const { main: checkMain } = await Promise.resolve().then(() => (init_check(), check_exports));
|
|
2787
|
-
const result = await checkMain();
|
|
2792
|
+
const result = await checkMain(config);
|
|
2788
2793
|
process.exit(result.failed > 0 ? 1 : 0);
|
|
2789
2794
|
}
|
|
2790
2795
|
async function test() {
|
|
2796
|
+
const config = resolveConfig(resolvePluginDir());
|
|
2791
2797
|
const { main: testMain } = await Promise.resolve().then(() => (init_smoke_test(), smoke_test_exports));
|
|
2792
|
-
const result = await testMain();
|
|
2798
|
+
const result = await testMain(config);
|
|
2793
2799
|
process.exit(result.failed > 0 ? 1 : 0);
|
|
2794
2800
|
}
|
|
2795
2801
|
async function start() {
|