typegraph-mcp 0.9.46 → 0.9.48
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 +12 -3
- package/benchmark.ts +12 -3
- package/biome-config.ts +76 -0
- package/check.ts +67 -83
- package/cli.ts +43 -62
- package/commands/check.md +1 -1
- package/dist/benchmark.js +56 -19
- package/dist/check.js +213 -153
- package/dist/cli.js +1373 -1228
- package/dist/module-graph.js +22 -9
- package/dist/server.js +82 -38
- package/dist/smoke-test.js +128 -33
- package/dist/tsserver-client.js +35 -10
- package/module-graph.ts +29 -9
- package/package.json +5 -4
- package/scripts/self-install.ts +78 -0
- package/server.ts +26 -19
- package/smoke-test.ts +118 -17
- package/tsserver-client.ts +49 -11
- package/engine-sync-test.ts +0 -262
- package/export-surface-test.ts +0 -202
- package/install-oxlint-test.ts +0 -116
package/module-graph.ts
CHANGED
|
@@ -55,8 +55,22 @@ const SKIP_FILES = new Set(["routeTree.gen.ts"]);
|
|
|
55
55
|
|
|
56
56
|
// ─── File Discovery ──────────────────────────────────────────────────────────
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
function normalizeExcludedPaths(rootDir: string, excludedPaths: string[]): string[] {
|
|
59
|
+
return excludedPaths.map((excludedPath) =>
|
|
60
|
+
path.resolve(rootDir, excludedPath)
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isExcluded(filePath: string, excludedPaths: string[]): boolean {
|
|
65
|
+
return excludedPaths.some(
|
|
66
|
+
(excludedPath) =>
|
|
67
|
+
filePath === excludedPath || filePath.startsWith(excludedPath + path.sep)
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function discoverFiles(rootDir: string, excludedPaths: string[] = []): string[] {
|
|
59
72
|
const files: string[] = [];
|
|
73
|
+
const normalizedExclusions = normalizeExcludedPaths(rootDir, excludedPaths);
|
|
60
74
|
|
|
61
75
|
function walk(dir: string): void {
|
|
62
76
|
let entries: fs.Dirent[];
|
|
@@ -71,7 +85,9 @@ export function discoverFiles(rootDir: string): string[] {
|
|
|
71
85
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
72
86
|
// Skip hidden directories (except the root)
|
|
73
87
|
if (entry.name.startsWith(".") && dir !== rootDir) continue;
|
|
74
|
-
|
|
88
|
+
const childDir = path.join(dir, entry.name);
|
|
89
|
+
if (isExcluded(childDir, normalizedExclusions)) continue;
|
|
90
|
+
walk(childDir);
|
|
75
91
|
} else if (entry.isFile()) {
|
|
76
92
|
const name = entry.name;
|
|
77
93
|
if (SKIP_FILES.has(name)) continue;
|
|
@@ -240,11 +256,11 @@ export function resolveProjectImport(
|
|
|
240
256
|
}
|
|
241
257
|
|
|
242
258
|
export function createResolver(projectRoot: string, tsconfigPath: string): ResolverFactory {
|
|
259
|
+
const configFile = path.resolve(projectRoot, tsconfigPath);
|
|
243
260
|
return new ResolverFactory({
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
},
|
|
261
|
+
...(fs.existsSync(configFile)
|
|
262
|
+
? { tsconfig: { configFile, references: "auto" as const } }
|
|
263
|
+
: {}),
|
|
248
264
|
extensions: [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"],
|
|
249
265
|
extensionAlias: {
|
|
250
266
|
".js": [".ts", ".tsx", ".js"],
|
|
@@ -328,12 +344,13 @@ function buildReverseMap(forward: Map<string, ImportEdge[]>): Map<string, Import
|
|
|
328
344
|
|
|
329
345
|
export async function buildGraph(
|
|
330
346
|
projectRoot: string,
|
|
331
|
-
tsconfigPath: string
|
|
347
|
+
tsconfigPath: string,
|
|
348
|
+
excludedPaths: string[] = []
|
|
332
349
|
): Promise<BuildGraphResult> {
|
|
333
350
|
const startTime = performance.now();
|
|
334
351
|
|
|
335
352
|
const resolver = createResolver(projectRoot, tsconfigPath);
|
|
336
|
-
const fileList = discoverFiles(projectRoot);
|
|
353
|
+
const fileList = discoverFiles(projectRoot, excludedPaths);
|
|
337
354
|
|
|
338
355
|
log(`Discovered ${fileList.length} source files`);
|
|
339
356
|
|
|
@@ -464,9 +481,11 @@ export function startWatcher(
|
|
|
464
481
|
hooks?: {
|
|
465
482
|
onFileUpdated?: (filePath: string) => void | Promise<void>;
|
|
466
483
|
onFileDeleted?: (filePath: string) => void | Promise<void>;
|
|
467
|
-
}
|
|
484
|
+
},
|
|
485
|
+
excludedPaths: string[] = []
|
|
468
486
|
): void {
|
|
469
487
|
try {
|
|
488
|
+
const normalizedExclusions = normalizeExcludedPaths(projectRoot, excludedPaths);
|
|
470
489
|
const watcher = fs.watch(
|
|
471
490
|
projectRoot,
|
|
472
491
|
{ recursive: true },
|
|
@@ -489,6 +508,7 @@ export function startWatcher(
|
|
|
489
508
|
return;
|
|
490
509
|
|
|
491
510
|
const absPath = path.resolve(projectRoot, filename);
|
|
511
|
+
if (isExcluded(absPath, normalizedExclusions)) return;
|
|
492
512
|
|
|
493
513
|
if (fs.existsSync(absPath)) {
|
|
494
514
|
// File created or modified
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "typegraph-mcp",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.48",
|
|
4
4
|
"description": "Type-aware codebase navigation for AI coding agents — 14 MCP tools powered by tsserver + oxc",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -33,8 +33,9 @@
|
|
|
33
33
|
],
|
|
34
34
|
"scripts": {
|
|
35
35
|
"build": "tsup",
|
|
36
|
+
"setup:self": "tsx scripts/self-install.ts",
|
|
36
37
|
"start": "tsx server.ts",
|
|
37
|
-
"test": "tsx smoke-test.ts && tsx export-surface-test.ts && tsx engine-sync-test.ts && tsx install-oxlint-test.ts",
|
|
38
|
+
"test": "tsx tests/smoke-test-selection-test.ts && tsx tests/biome-config-test.ts && tsx tests/self-install-test.ts && tsx tests/tsserver-resolution-test.ts && tsx tests/export-surface-test.ts && tsx tests/engine-sync-test.ts && tsx tests/install-oxlint-test.ts",
|
|
38
39
|
"check": "tsx check.ts"
|
|
39
40
|
},
|
|
40
41
|
"dependencies": {
|
|
@@ -42,12 +43,12 @@
|
|
|
42
43
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
43
44
|
"oxc-parser": "^0.114.0",
|
|
44
45
|
"oxc-resolver": "^11.17.1",
|
|
46
|
+
"typescript": "^5.9.3",
|
|
45
47
|
"zod": "^4.3.6"
|
|
46
48
|
},
|
|
47
49
|
"devDependencies": {
|
|
48
50
|
"@types/node": "^25.3.0",
|
|
49
51
|
"tsup": "^8.5.0",
|
|
50
|
-
"tsx": "^4.21.0"
|
|
51
|
-
"typescript": "^5.8.0"
|
|
52
|
+
"tsx": "^4.21.0"
|
|
52
53
|
}
|
|
53
54
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
export const SELF_INSTALL_IGNORES = [
|
|
9
|
+
"/plugins/typegraph-mcp/",
|
|
10
|
+
"/.agents/",
|
|
11
|
+
"/.codex/",
|
|
12
|
+
"/AGENTS.md",
|
|
13
|
+
"/CLAUDE.md",
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
export function isGitWorkTree(projectRoot: string): boolean {
|
|
17
|
+
const result = spawnSync("git", ["-C", projectRoot, "rev-parse", "--is-inside-work-tree"], {
|
|
18
|
+
encoding: "utf-8",
|
|
19
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
20
|
+
});
|
|
21
|
+
return result.status === 0 && result.stdout.trim() === "true";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function ensureSelfInstallIgnores(projectRoot: string): string[] {
|
|
25
|
+
if (!isGitWorkTree(projectRoot)) return [];
|
|
26
|
+
|
|
27
|
+
const gitignorePath = path.join(projectRoot, ".gitignore");
|
|
28
|
+
const existing = fs.existsSync(gitignorePath)
|
|
29
|
+
? fs.readFileSync(gitignorePath, "utf-8")
|
|
30
|
+
: "";
|
|
31
|
+
const existingLines = new Set(existing.split(/\r?\n/));
|
|
32
|
+
const added = SELF_INSTALL_IGNORES.filter((pattern) => !existingLines.has(pattern));
|
|
33
|
+
|
|
34
|
+
if (added.length > 0) {
|
|
35
|
+
const separator = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
36
|
+
fs.writeFileSync(gitignorePath, `${existing}${separator}${added.join("\n")}\n`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return [...added];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function ensureAgentMarkers(projectRoot: string): void {
|
|
43
|
+
for (const fileName of ["AGENTS.md", "CLAUDE.md"]) {
|
|
44
|
+
const filePath = path.join(projectRoot, fileName);
|
|
45
|
+
if (!fs.existsSync(filePath)) fs.writeFileSync(filePath, "");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function main(): void {
|
|
50
|
+
const projectRoot = path.resolve(import.meta.dirname, "..");
|
|
51
|
+
const addedIgnores = ensureSelfInstallIgnores(projectRoot);
|
|
52
|
+
|
|
53
|
+
if (isGitWorkTree(projectRoot)) {
|
|
54
|
+
console.log(
|
|
55
|
+
addedIgnores.length > 0
|
|
56
|
+
? `Added ${addedIgnores.length} self-install entries to .gitignore`
|
|
57
|
+
: "Self-install artifacts are already ignored"
|
|
58
|
+
);
|
|
59
|
+
} else {
|
|
60
|
+
console.log("Git worktree not found; skipping .gitignore update");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
ensureAgentMarkers(projectRoot);
|
|
64
|
+
|
|
65
|
+
const tsxPath = path.join(projectRoot, "node_modules/.bin/tsx");
|
|
66
|
+
const cliPath = path.join(projectRoot, "cli.ts");
|
|
67
|
+
const result = spawnSync(tsxPath, [cliPath, "setup", "--yes"], {
|
|
68
|
+
cwd: projectRoot,
|
|
69
|
+
stdio: "inherit",
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (result.error) throw result.error;
|
|
73
|
+
process.exit(result.status ?? 1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
77
|
+
main();
|
|
78
|
+
}
|
package/server.ts
CHANGED
|
@@ -41,7 +41,8 @@ import { resolveConfig } from "./config.js";
|
|
|
41
41
|
|
|
42
42
|
// ─── Configuration ───────────────────────────────────────────────────────────
|
|
43
43
|
|
|
44
|
-
const { projectRoot, tsconfigPath } = resolveConfig(import.meta.dirname);
|
|
44
|
+
const { projectRoot, tsconfigPath, toolDir, toolIsEmbedded } = resolveConfig(import.meta.dirname);
|
|
45
|
+
const excludedPaths = toolIsEmbedded ? [toolDir] : [];
|
|
45
46
|
|
|
46
47
|
const log = (...args: unknown[]) => console.error("[typegraph]", ...args);
|
|
47
48
|
|
|
@@ -1127,29 +1128,35 @@ async function main() {
|
|
|
1127
1128
|
// Start tsserver and build module graph concurrently
|
|
1128
1129
|
const [, graphResult] = await Promise.all([
|
|
1129
1130
|
client.start(),
|
|
1130
|
-
buildGraph(projectRoot, tsconfigPath),
|
|
1131
|
+
buildGraph(projectRoot, tsconfigPath, excludedPaths),
|
|
1131
1132
|
]);
|
|
1132
1133
|
|
|
1133
1134
|
moduleGraph = graphResult.graph;
|
|
1134
1135
|
moduleResolver = graphResult.resolver;
|
|
1135
|
-
startWatcher(
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1136
|
+
startWatcher(
|
|
1137
|
+
projectRoot,
|
|
1138
|
+
moduleGraph,
|
|
1139
|
+
graphResult.resolver,
|
|
1140
|
+
{
|
|
1141
|
+
onFileUpdated: (filePath) =>
|
|
1142
|
+
client.reloadOpenFile(filePath).then(
|
|
1143
|
+
(wasOpen) => {
|
|
1144
|
+
// Closed files: tsserver's own disk watching decays in long-lived
|
|
1145
|
+
// instances; force a projects reload before the next query.
|
|
1146
|
+
if (!wasOpen) client.markProjectsDirty();
|
|
1147
|
+
},
|
|
1148
|
+
(err) => {
|
|
1149
|
+
client.markProjectsDirty();
|
|
1150
|
+
log(`Failed to reload open file ${relPath(filePath)}:`, err);
|
|
1151
|
+
}
|
|
1152
|
+
),
|
|
1153
|
+
onFileDeleted: (filePath) => {
|
|
1154
|
+
client.closeFile(filePath);
|
|
1155
|
+
client.markProjectsDirty();
|
|
1156
|
+
},
|
|
1151
1157
|
},
|
|
1152
|
-
|
|
1158
|
+
excludedPaths
|
|
1159
|
+
);
|
|
1153
1160
|
|
|
1154
1161
|
const transport = new StdioServerTransport();
|
|
1155
1162
|
await mcpServer.connect(transport);
|
package/smoke-test.ts
CHANGED
|
@@ -13,7 +13,11 @@
|
|
|
13
13
|
|
|
14
14
|
import * as fs from "node:fs";
|
|
15
15
|
import * as path from "node:path";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
TsServerClient,
|
|
18
|
+
type NavBarItem,
|
|
19
|
+
type QuickInfoResult,
|
|
20
|
+
} from "./tsserver-client.js";
|
|
17
21
|
import { buildGraph, type ModuleGraph } from "./module-graph.js";
|
|
18
22
|
import {
|
|
19
23
|
dependencyTree,
|
|
@@ -54,6 +58,81 @@ function findInNavBar(
|
|
|
54
58
|
return null;
|
|
55
59
|
}
|
|
56
60
|
|
|
61
|
+
function symbolPositions(
|
|
62
|
+
symbol: NavBarItem,
|
|
63
|
+
sourceLines: string[]
|
|
64
|
+
): Array<{ line: number; offset: number }> {
|
|
65
|
+
const span = symbol.spans[0]!;
|
|
66
|
+
const positions: Array<{ line: number; offset: number }> = [];
|
|
67
|
+
const lineText = sourceLines[span.start.line - 1];
|
|
68
|
+
|
|
69
|
+
if (lineText) {
|
|
70
|
+
const nameIndex = lineText.indexOf(symbol.text, Math.max(0, span.start.offset - 1));
|
|
71
|
+
if (nameIndex >= 0) {
|
|
72
|
+
positions.push({ line: span.start.line, offset: nameIndex + 1 });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!positions.some((position) => position.offset === span.start.offset)) {
|
|
77
|
+
positions.push(span.start);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return positions;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function probeQuickInfo(
|
|
84
|
+
client: Pick<TsServerClient, "quickinfo">,
|
|
85
|
+
file: string,
|
|
86
|
+
symbol: NavBarItem,
|
|
87
|
+
sourceLines: string[]
|
|
88
|
+
): Promise<{
|
|
89
|
+
info: QuickInfoResult;
|
|
90
|
+
position: { line: number; offset: number };
|
|
91
|
+
} | null> {
|
|
92
|
+
for (const position of symbolPositions(symbol, sourceLines)) {
|
|
93
|
+
const info = await client.quickinfo(file, position.line, position.offset);
|
|
94
|
+
if (info) return { info, position };
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function selectQuickInfoSymbol(
|
|
100
|
+
client: Pick<TsServerClient, "quickinfo">,
|
|
101
|
+
file: string,
|
|
102
|
+
symbols: NavBarItem[],
|
|
103
|
+
sourceLines: string[]
|
|
104
|
+
): Promise<{
|
|
105
|
+
symbol: NavBarItem;
|
|
106
|
+
info: QuickInfoResult;
|
|
107
|
+
position: { line: number; offset: number };
|
|
108
|
+
} | null> {
|
|
109
|
+
const kindPriority = new Map([
|
|
110
|
+
["const", 0],
|
|
111
|
+
["let", 1],
|
|
112
|
+
["var", 2],
|
|
113
|
+
["class", 3],
|
|
114
|
+
["enum", 4],
|
|
115
|
+
["function", 5],
|
|
116
|
+
["method", 6],
|
|
117
|
+
]);
|
|
118
|
+
const concreteSymbols = symbols
|
|
119
|
+
.map((symbol, index) => ({ symbol, index }))
|
|
120
|
+
.filter(({ symbol }) => kindPriority.has(symbol.kind))
|
|
121
|
+
.sort(
|
|
122
|
+
(a, b) =>
|
|
123
|
+
kindPriority.get(a.symbol.kind)! - kindPriority.get(b.symbol.kind)! ||
|
|
124
|
+
a.index - b.index
|
|
125
|
+
)
|
|
126
|
+
.map(({ symbol }) => symbol);
|
|
127
|
+
|
|
128
|
+
for (const symbol of concreteSymbols) {
|
|
129
|
+
const probe = await probeQuickInfo(client, file, symbol, sourceLines);
|
|
130
|
+
if (probe) return { symbol, ...probe };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
57
136
|
const SKIP_DIRS = new Set([
|
|
58
137
|
"node_modules",
|
|
59
138
|
"dist",
|
|
@@ -65,8 +144,9 @@ const SKIP_DIRS = new Set([
|
|
|
65
144
|
]);
|
|
66
145
|
|
|
67
146
|
/** Find a file with imports and exported symbols (good test candidate) */
|
|
68
|
-
function findTestFile(rootDir: string): string | null {
|
|
147
|
+
function findTestFile(rootDir: string, excludedPaths: string[] = []): string | null {
|
|
69
148
|
const candidates: Array<{ file: string; size: number }> = [];
|
|
149
|
+
const normalizedExclusions = excludedPaths.map((excludedPath) => path.resolve(excludedPath));
|
|
70
150
|
|
|
71
151
|
function walk(dir: string, depth: number): void {
|
|
72
152
|
if (depth > 5 || candidates.length >= 30) return;
|
|
@@ -79,7 +159,14 @@ function findTestFile(rootDir: string): string | null {
|
|
|
79
159
|
for (const entry of entries) {
|
|
80
160
|
if (entry.isDirectory()) {
|
|
81
161
|
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
82
|
-
|
|
162
|
+
const childDir = path.join(dir, entry.name);
|
|
163
|
+
if (
|
|
164
|
+
normalizedExclusions.some(
|
|
165
|
+
(excludedPath) =>
|
|
166
|
+
childDir === excludedPath || childDir.startsWith(excludedPath + path.sep)
|
|
167
|
+
)
|
|
168
|
+
) continue;
|
|
169
|
+
walk(childDir, depth + 1);
|
|
83
170
|
} else if (entry.isFile()) {
|
|
84
171
|
const name = entry.name;
|
|
85
172
|
if (name.endsWith(".d.ts") || name.endsWith(".test.ts") || name.endsWith(".spec.ts"))
|
|
@@ -87,7 +174,7 @@ function findTestFile(rootDir: string): string | null {
|
|
|
87
174
|
if (!name.endsWith(".ts") && !name.endsWith(".tsx")) continue;
|
|
88
175
|
try {
|
|
89
176
|
const stat = fs.statSync(path.join(dir, name));
|
|
90
|
-
if (stat.size >
|
|
177
|
+
if (stat.size > 0 && stat.size < 50000) {
|
|
91
178
|
candidates.push({ file: path.join(dir, name), size: stat.size });
|
|
92
179
|
}
|
|
93
180
|
} catch {
|
|
@@ -120,8 +207,9 @@ function findImporter(graph: ModuleGraph, file: string): string | null {
|
|
|
120
207
|
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
121
208
|
|
|
122
209
|
export async function main(configOverride?: TypegraphConfig): Promise<SmokeTestResult> {
|
|
123
|
-
const { projectRoot, tsconfigPath } =
|
|
210
|
+
const { projectRoot, tsconfigPath, toolDir, toolIsEmbedded } =
|
|
124
211
|
configOverride ?? resolveConfig(import.meta.dirname);
|
|
212
|
+
const excludedPaths = toolIsEmbedded ? [toolDir] : [];
|
|
125
213
|
|
|
126
214
|
let passed = 0;
|
|
127
215
|
let failed = 0;
|
|
@@ -152,7 +240,7 @@ export async function main(configOverride?: TypegraphConfig): Promise<SmokeTestR
|
|
|
152
240
|
|
|
153
241
|
// ─── Discover a test file ───────────────────────────────────────────────
|
|
154
242
|
|
|
155
|
-
const testFile = findTestFile(projectRoot);
|
|
243
|
+
const testFile = findTestFile(projectRoot, excludedPaths);
|
|
156
244
|
if (!testFile) {
|
|
157
245
|
console.log(" No suitable .ts file found in project. Cannot run smoke tests.");
|
|
158
246
|
return { passed, failed: failed + 1, skipped };
|
|
@@ -171,7 +259,7 @@ export async function main(configOverride?: TypegraphConfig): Promise<SmokeTestR
|
|
|
171
259
|
// Graph build
|
|
172
260
|
t0 = performance.now();
|
|
173
261
|
try {
|
|
174
|
-
const result = await buildGraph(projectRoot, tsconfigPath);
|
|
262
|
+
const result = await buildGraph(projectRoot, tsconfigPath, excludedPaths);
|
|
175
263
|
graph = result.graph;
|
|
176
264
|
const ms = performance.now() - t0;
|
|
177
265
|
const edgeCount = [...graph.forward.values()].reduce((s, e) => s + e.length, 0);
|
|
@@ -309,10 +397,21 @@ export async function main(configOverride?: TypegraphConfig): Promise<SmokeTestR
|
|
|
309
397
|
fail("navbar", `No symbols found in ${testFileRel}`, navbarMs);
|
|
310
398
|
}
|
|
311
399
|
|
|
312
|
-
// Prefer concrete
|
|
313
|
-
//
|
|
314
|
-
|
|
315
|
-
const
|
|
400
|
+
// Prefer a concrete symbol that quickinfo can actually resolve. Navbar may
|
|
401
|
+
// include synthetic entries such as "Alpine.data(...) callback" that are
|
|
402
|
+
// useful for navigation but do not correspond to a hoverable source token.
|
|
403
|
+
const source = fs.readFileSync(testFile, "utf-8");
|
|
404
|
+
const sourceLines = source.split(/\r?\n/);
|
|
405
|
+
const quickInfoSelection = await selectQuickInfoSymbol(
|
|
406
|
+
client,
|
|
407
|
+
testFileRel,
|
|
408
|
+
allSymbols,
|
|
409
|
+
sourceLines
|
|
410
|
+
);
|
|
411
|
+
const sym = quickInfoSelection?.symbol ?? allSymbols[0];
|
|
412
|
+
const selectedQuickInfo = quickInfoSelection?.info ?? null;
|
|
413
|
+
const selectedPosition = quickInfoSelection?.position ?? sym?.spans[0]?.start;
|
|
414
|
+
|
|
316
415
|
if (!sym) {
|
|
317
416
|
const toolNames = [
|
|
318
417
|
"find_symbol",
|
|
@@ -327,6 +426,7 @@ export async function main(configOverride?: TypegraphConfig): Promise<SmokeTestR
|
|
|
327
426
|
for (const name of toolNames) skip(name, "No symbol discovered");
|
|
328
427
|
} else {
|
|
329
428
|
const span = sym.spans[0]!;
|
|
429
|
+
const point = selectedPosition ?? span.start;
|
|
330
430
|
|
|
331
431
|
// find_symbol
|
|
332
432
|
t0 = performance.now();
|
|
@@ -343,7 +443,7 @@ export async function main(configOverride?: TypegraphConfig): Promise<SmokeTestR
|
|
|
343
443
|
|
|
344
444
|
// definition
|
|
345
445
|
t0 = performance.now();
|
|
346
|
-
const defs = await client.definition(testFileRel,
|
|
446
|
+
const defs = await client.definition(testFileRel, point.line, point.offset);
|
|
347
447
|
if (defs.length > 0) {
|
|
348
448
|
const def = defs[0]!;
|
|
349
449
|
pass("definition", `${sym.text} -> ${def.file}:${def.start.line}`, performance.now() - t0);
|
|
@@ -353,7 +453,7 @@ export async function main(configOverride?: TypegraphConfig): Promise<SmokeTestR
|
|
|
353
453
|
|
|
354
454
|
// references
|
|
355
455
|
t0 = performance.now();
|
|
356
|
-
const refs = await client.references(testFileRel,
|
|
456
|
+
const refs = await client.references(testFileRel, point.line, point.offset);
|
|
357
457
|
const refFiles = new Set(refs.map((r) => r.file));
|
|
358
458
|
pass(
|
|
359
459
|
"references",
|
|
@@ -363,7 +463,10 @@ export async function main(configOverride?: TypegraphConfig): Promise<SmokeTestR
|
|
|
363
463
|
|
|
364
464
|
// type_info
|
|
365
465
|
t0 = performance.now();
|
|
366
|
-
let info =
|
|
466
|
+
let info = selectedQuickInfo;
|
|
467
|
+
if (!info) {
|
|
468
|
+
info = await client.quickinfo(testFileRel, point.line, point.offset);
|
|
469
|
+
}
|
|
367
470
|
// Span start may point to a keyword (class, function) — retry at the name position
|
|
368
471
|
if (!info && defs.length > 0) {
|
|
369
472
|
const def = defs[0]!;
|
|
@@ -376,7 +479,7 @@ export async function main(configOverride?: TypegraphConfig): Promise<SmokeTestR
|
|
|
376
479
|
: info.displayString;
|
|
377
480
|
pass("type_info", typeStr, performance.now() - t0);
|
|
378
481
|
} else {
|
|
379
|
-
|
|
482
|
+
skip("type_info", `No discovered symbol returned type info`);
|
|
380
483
|
}
|
|
381
484
|
|
|
382
485
|
// navigate_to
|
|
@@ -416,7 +519,6 @@ export async function main(configOverride?: TypegraphConfig): Promise<SmokeTestR
|
|
|
416
519
|
|
|
417
520
|
// trace_chain — follow an import to its source
|
|
418
521
|
t0 = performance.now();
|
|
419
|
-
const source = fs.readFileSync(testFile, "utf-8");
|
|
420
522
|
const importMatch = source.match(/^import\s+\{([^}]+)\}\s+from\s+["']([^"']+)["']/m);
|
|
421
523
|
if (importMatch) {
|
|
422
524
|
const firstName = importMatch[1]!
|
|
@@ -488,4 +590,3 @@ export async function main(configOverride?: TypegraphConfig): Promise<SmokeTestR
|
|
|
488
590
|
|
|
489
591
|
return { passed, failed, skipped };
|
|
490
592
|
}
|
|
491
|
-
|
package/tsserver-client.ts
CHANGED
|
@@ -80,6 +80,43 @@ interface PendingRequest {
|
|
|
80
80
|
command: string;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
export interface TsServerResolution {
|
|
84
|
+
path: string;
|
|
85
|
+
version: string;
|
|
86
|
+
source: "project" | "typegraph";
|
|
87
|
+
projectVersion?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function readPackageVersion(packagePath: string): string {
|
|
91
|
+
const pkg = JSON.parse(fs.readFileSync(packagePath, "utf-8")) as { version?: string };
|
|
92
|
+
return pkg.version ?? "unknown";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Resolve legacy tsserver, falling back when the project uses TypeScript 7's LSP runtime. */
|
|
96
|
+
export function resolveTsServer(projectRoot: string): TsServerResolution {
|
|
97
|
+
const projectRequire = createRequire(path.resolve(projectRoot, "package.json"));
|
|
98
|
+
let projectVersion: string | undefined;
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const packagePath = projectRequire.resolve("typescript/package.json");
|
|
102
|
+
projectVersion = readPackageVersion(packagePath);
|
|
103
|
+
const serverPath = projectRequire.resolve("typescript/lib/tsserver.js");
|
|
104
|
+
return { path: serverPath, version: projectVersion, source: "project" };
|
|
105
|
+
} catch {
|
|
106
|
+
// TypeScript 7 is installed without the legacy tsserver entrypoint.
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const toolRequire = createRequire(import.meta.url);
|
|
110
|
+
const packagePath = toolRequire.resolve("typescript/package.json");
|
|
111
|
+
const serverPath = toolRequire.resolve("typescript/lib/tsserver.js");
|
|
112
|
+
return {
|
|
113
|
+
path: serverPath,
|
|
114
|
+
version: readPackageVersion(packagePath),
|
|
115
|
+
source: "typegraph",
|
|
116
|
+
projectVersion,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
83
120
|
export class TsServerClient {
|
|
84
121
|
private child: ChildProcess | null = null;
|
|
85
122
|
private seq = 0;
|
|
@@ -124,11 +161,15 @@ export class TsServerClient {
|
|
|
124
161
|
async start(): Promise<void> {
|
|
125
162
|
if (this.child) return;
|
|
126
163
|
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
const tsserverPath = require.resolve("typescript/lib/tsserver.js");
|
|
164
|
+
const resolution = resolveTsServer(this.projectRoot);
|
|
165
|
+
const tsserverPath = resolution.path;
|
|
130
166
|
|
|
131
|
-
|
|
167
|
+
const fallbackNote = resolution.projectVersion
|
|
168
|
+
? `; project TypeScript ${resolution.projectVersion} has no legacy tsserver`
|
|
169
|
+
: "";
|
|
170
|
+
log(
|
|
171
|
+
`Spawning tsserver: ${tsserverPath} (TypeScript ${resolution.version}, ${resolution.source}${fallbackNote})`
|
|
172
|
+
);
|
|
132
173
|
log(`Project root: ${this.projectRoot}`);
|
|
133
174
|
log(`tsconfig: ${this.tsconfigPath}`);
|
|
134
175
|
|
|
@@ -164,14 +205,11 @@ export class TsServerClient {
|
|
|
164
205
|
},
|
|
165
206
|
});
|
|
166
207
|
|
|
167
|
-
//
|
|
208
|
+
// Keep inferred projects useful when the target has no tsconfig or a file sits outside it.
|
|
168
209
|
const warmStart = performance.now();
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
options: { allowJs: true, checkJs: false },
|
|
173
|
-
});
|
|
174
|
-
}
|
|
210
|
+
await this.sendRequest("compilerOptionsForInferredProjects", {
|
|
211
|
+
options: { allowJs: true, checkJs: false },
|
|
212
|
+
});
|
|
175
213
|
this.ready = true;
|
|
176
214
|
log(`Ready [${(performance.now() - warmStart).toFixed(0)}ms configure]`);
|
|
177
215
|
}
|