typegraph-mcp 0.9.47 → 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 +1310 -1219
- package/dist/module-graph.js +22 -9
- package/dist/server.js +82 -38
- package/dist/smoke-test.js +66 -25
- 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 +15 -6
- 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/smoke-test-selection-test.ts +0 -67
|
@@ -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
|
@@ -144,8 +144,9 @@ const SKIP_DIRS = new Set([
|
|
|
144
144
|
]);
|
|
145
145
|
|
|
146
146
|
/** Find a file with imports and exported symbols (good test candidate) */
|
|
147
|
-
function findTestFile(rootDir: string): string | null {
|
|
147
|
+
function findTestFile(rootDir: string, excludedPaths: string[] = []): string | null {
|
|
148
148
|
const candidates: Array<{ file: string; size: number }> = [];
|
|
149
|
+
const normalizedExclusions = excludedPaths.map((excludedPath) => path.resolve(excludedPath));
|
|
149
150
|
|
|
150
151
|
function walk(dir: string, depth: number): void {
|
|
151
152
|
if (depth > 5 || candidates.length >= 30) return;
|
|
@@ -158,7 +159,14 @@ function findTestFile(rootDir: string): string | null {
|
|
|
158
159
|
for (const entry of entries) {
|
|
159
160
|
if (entry.isDirectory()) {
|
|
160
161
|
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
161
|
-
|
|
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);
|
|
162
170
|
} else if (entry.isFile()) {
|
|
163
171
|
const name = entry.name;
|
|
164
172
|
if (name.endsWith(".d.ts") || name.endsWith(".test.ts") || name.endsWith(".spec.ts"))
|
|
@@ -166,7 +174,7 @@ function findTestFile(rootDir: string): string | null {
|
|
|
166
174
|
if (!name.endsWith(".ts") && !name.endsWith(".tsx")) continue;
|
|
167
175
|
try {
|
|
168
176
|
const stat = fs.statSync(path.join(dir, name));
|
|
169
|
-
if (stat.size >
|
|
177
|
+
if (stat.size > 0 && stat.size < 50000) {
|
|
170
178
|
candidates.push({ file: path.join(dir, name), size: stat.size });
|
|
171
179
|
}
|
|
172
180
|
} catch {
|
|
@@ -199,8 +207,9 @@ function findImporter(graph: ModuleGraph, file: string): string | null {
|
|
|
199
207
|
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
200
208
|
|
|
201
209
|
export async function main(configOverride?: TypegraphConfig): Promise<SmokeTestResult> {
|
|
202
|
-
const { projectRoot, tsconfigPath } =
|
|
210
|
+
const { projectRoot, tsconfigPath, toolDir, toolIsEmbedded } =
|
|
203
211
|
configOverride ?? resolveConfig(import.meta.dirname);
|
|
212
|
+
const excludedPaths = toolIsEmbedded ? [toolDir] : [];
|
|
204
213
|
|
|
205
214
|
let passed = 0;
|
|
206
215
|
let failed = 0;
|
|
@@ -231,7 +240,7 @@ export async function main(configOverride?: TypegraphConfig): Promise<SmokeTestR
|
|
|
231
240
|
|
|
232
241
|
// ─── Discover a test file ───────────────────────────────────────────────
|
|
233
242
|
|
|
234
|
-
const testFile = findTestFile(projectRoot);
|
|
243
|
+
const testFile = findTestFile(projectRoot, excludedPaths);
|
|
235
244
|
if (!testFile) {
|
|
236
245
|
console.log(" No suitable .ts file found in project. Cannot run smoke tests.");
|
|
237
246
|
return { passed, failed: failed + 1, skipped };
|
|
@@ -250,7 +259,7 @@ export async function main(configOverride?: TypegraphConfig): Promise<SmokeTestR
|
|
|
250
259
|
// Graph build
|
|
251
260
|
t0 = performance.now();
|
|
252
261
|
try {
|
|
253
|
-
const result = await buildGraph(projectRoot, tsconfigPath);
|
|
262
|
+
const result = await buildGraph(projectRoot, tsconfigPath, excludedPaths);
|
|
254
263
|
graph = result.graph;
|
|
255
264
|
const ms = performance.now() - t0;
|
|
256
265
|
const edgeCount = [...graph.forward.values()].reduce((s, e) => s + e.length, 0);
|
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
|
}
|
package/engine-sync-test.ts
DELETED
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env npx tsx
|
|
2
|
-
|
|
3
|
-
import * as assert from "node:assert/strict";
|
|
4
|
-
import * as fs from "node:fs";
|
|
5
|
-
import * as os from "node:os";
|
|
6
|
-
import * as path from "node:path";
|
|
7
|
-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
8
|
-
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
9
|
-
import { CallToolResultSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
10
|
-
|
|
11
|
-
type DependencyTreeResult = {
|
|
12
|
-
root: string;
|
|
13
|
-
nodes: number;
|
|
14
|
-
files: string[];
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
type TypeInfoResult = {
|
|
18
|
-
type: string | null;
|
|
19
|
-
documentation: string | null;
|
|
20
|
-
kind?: string;
|
|
21
|
-
source?: string;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
type ModuleExportsResult = {
|
|
25
|
-
file: string;
|
|
26
|
-
exports: Array<{
|
|
27
|
-
symbol: string;
|
|
28
|
-
type: string | null;
|
|
29
|
-
}>;
|
|
30
|
-
count: number;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
function normalize(file: string): string {
|
|
34
|
-
return file.replaceAll("\\", "/");
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function sleep(ms: number): Promise<void> {
|
|
38
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async function waitFor(
|
|
42
|
-
description: string,
|
|
43
|
-
fn: () => Promise<void>,
|
|
44
|
-
timeoutMs = 5_000,
|
|
45
|
-
intervalMs = 50
|
|
46
|
-
): Promise<void> {
|
|
47
|
-
const start = Date.now();
|
|
48
|
-
let lastError: unknown;
|
|
49
|
-
|
|
50
|
-
while (Date.now() - start < timeoutMs) {
|
|
51
|
-
try {
|
|
52
|
-
await fn();
|
|
53
|
-
return;
|
|
54
|
-
} catch (err) {
|
|
55
|
-
lastError = err;
|
|
56
|
-
await sleep(intervalMs);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
throw new Error(
|
|
61
|
-
`${description} did not stabilize within ${timeoutMs}ms: ${String(lastError)}`
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function writeFile(root: string, relativePath: string, content: string): void {
|
|
66
|
-
const absPath = path.join(root, relativePath);
|
|
67
|
-
fs.mkdirSync(path.dirname(absPath), { recursive: true });
|
|
68
|
-
fs.writeFileSync(absPath, content);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async function main(): Promise<void> {
|
|
72
|
-
const repoRoot = import.meta.dirname;
|
|
73
|
-
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "typegraph-engine-sync-"));
|
|
74
|
-
const projectRoot = path.join(tempRoot, "project");
|
|
75
|
-
|
|
76
|
-
fs.mkdirSync(projectRoot, { recursive: true });
|
|
77
|
-
writeFile(
|
|
78
|
-
projectRoot,
|
|
79
|
-
"package.json",
|
|
80
|
-
JSON.stringify(
|
|
81
|
-
{
|
|
82
|
-
name: "typegraph-engine-sync-fixture",
|
|
83
|
-
private: true,
|
|
84
|
-
type: "module",
|
|
85
|
-
},
|
|
86
|
-
null,
|
|
87
|
-
2
|
|
88
|
-
) + "\n"
|
|
89
|
-
);
|
|
90
|
-
writeFile(
|
|
91
|
-
projectRoot,
|
|
92
|
-
"tsconfig.json",
|
|
93
|
-
JSON.stringify(
|
|
94
|
-
{
|
|
95
|
-
compilerOptions: {
|
|
96
|
-
target: "ES2022",
|
|
97
|
-
module: "ESNext",
|
|
98
|
-
moduleResolution: "Bundler",
|
|
99
|
-
strict: true,
|
|
100
|
-
},
|
|
101
|
-
include: ["src/**/*.ts"],
|
|
102
|
-
},
|
|
103
|
-
null,
|
|
104
|
-
2
|
|
105
|
-
) + "\n"
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
writeFile(projectRoot, "src/a.ts", 'export const current = "a" as const;\n');
|
|
109
|
-
writeFile(projectRoot, "src/b.ts", 'export const current = "b" as const;\n');
|
|
110
|
-
writeFile(
|
|
111
|
-
projectRoot,
|
|
112
|
-
"src/main.ts",
|
|
113
|
-
'import { current } from "./a";\nexport const value = current;\n'
|
|
114
|
-
);
|
|
115
|
-
writeFile(projectRoot, "src/test.ts", "export const oldName = 1 as const;\n");
|
|
116
|
-
writeFile(projectRoot, "src/util.ts", "export const helper = 1 as const;\n");
|
|
117
|
-
writeFile(projectRoot, "src/closed.ts", "export const closedValue = 1 as const;\n");
|
|
118
|
-
writeFile(
|
|
119
|
-
projectRoot,
|
|
120
|
-
"src/closed-consumer.ts",
|
|
121
|
-
'import { closedValue } from "./closed";\nexport const observed = closedValue;\n'
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
fs.mkdirSync(path.join(projectRoot, "node_modules"), { recursive: true });
|
|
125
|
-
fs.symlinkSync(
|
|
126
|
-
path.join(repoRoot, "node_modules/typescript"),
|
|
127
|
-
path.join(projectRoot, "node_modules/typescript"),
|
|
128
|
-
"dir"
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
const client = new Client({ name: "engine-sync-test", version: "1.0.0" });
|
|
132
|
-
const transport = new StdioClientTransport({
|
|
133
|
-
command: path.join(repoRoot, "node_modules/.bin/tsx"),
|
|
134
|
-
args: [path.join(repoRoot, "server.ts")],
|
|
135
|
-
cwd: projectRoot,
|
|
136
|
-
env: {
|
|
137
|
-
TYPEGRAPH_PROJECT_ROOT: projectRoot,
|
|
138
|
-
TYPEGRAPH_TSCONFIG: path.join(projectRoot, "tsconfig.json"),
|
|
139
|
-
},
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
async function callTool<T>(name: string, args: Record<string, unknown>): Promise<T> {
|
|
143
|
-
const result = await client.request(
|
|
144
|
-
{
|
|
145
|
-
method: "tools/call",
|
|
146
|
-
params: {
|
|
147
|
-
name,
|
|
148
|
-
arguments: args,
|
|
149
|
-
},
|
|
150
|
-
},
|
|
151
|
-
CallToolResultSchema
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
const content = result.content[0];
|
|
155
|
-
assert.ok(content?.type === "text", `Expected text response from ${name}`);
|
|
156
|
-
return JSON.parse(content.text) as T;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
try {
|
|
160
|
-
await client.connect(transport);
|
|
161
|
-
|
|
162
|
-
const initialType = await callTool<TypeInfoResult>("ts_type_info", {
|
|
163
|
-
file: "src/main.ts",
|
|
164
|
-
line: 2,
|
|
165
|
-
column: 14,
|
|
166
|
-
});
|
|
167
|
-
assert.match(initialType.type ?? "", /"a"/);
|
|
168
|
-
|
|
169
|
-
writeFile(projectRoot, "src/main.ts", 'import { current } from "./b";\nexport const value = current;\n');
|
|
170
|
-
|
|
171
|
-
await waitFor("import swap to synchronize graph and tsserver", async () => {
|
|
172
|
-
const deps = await callTool<DependencyTreeResult>("ts_dependency_tree", {
|
|
173
|
-
file: "src/main.ts",
|
|
174
|
-
});
|
|
175
|
-
const normalizedDeps = deps.files.map(normalize);
|
|
176
|
-
assert.ok(normalizedDeps.includes("src/b.ts"), `Expected src/b.ts in ${normalizedDeps}`);
|
|
177
|
-
assert.ok(!normalizedDeps.includes("src/a.ts"), `Expected src/a.ts to be removed from ${normalizedDeps}`);
|
|
178
|
-
|
|
179
|
-
const typeInfo = await callTool<TypeInfoResult>("ts_type_info", {
|
|
180
|
-
file: "src/main.ts",
|
|
181
|
-
line: 2,
|
|
182
|
-
column: 14,
|
|
183
|
-
});
|
|
184
|
-
assert.match(typeInfo.type ?? "", /"b"/);
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
// Open test.ts through ts_module_exports, then rename the export on disk.
|
|
188
|
-
const initialExports = await callTool<ModuleExportsResult>("ts_module_exports", {
|
|
189
|
-
file: "src/test.ts",
|
|
190
|
-
});
|
|
191
|
-
assert.ok(initialExports.exports.some((item) => item.symbol === "oldName"));
|
|
192
|
-
|
|
193
|
-
writeFile(projectRoot, "src/test.ts", "export const newName = 1 as const;\n");
|
|
194
|
-
|
|
195
|
-
await waitFor("symbol rename to refresh mixed export metadata", async () => {
|
|
196
|
-
const exportsResult = await callTool<ModuleExportsResult>("ts_module_exports", {
|
|
197
|
-
file: "src/test.ts",
|
|
198
|
-
});
|
|
199
|
-
const next = exportsResult.exports.find((item) => item.symbol === "newName");
|
|
200
|
-
assert.ok(next, `Expected newName in ${JSON.stringify(exportsResult.exports)}`);
|
|
201
|
-
assert.match(next.type ?? "", /\bnewName\b/);
|
|
202
|
-
assert.ok(!exportsResult.exports.some((item) => item.symbol === "oldName"));
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
// Change a file that tsserver has not opened. The watcher should mark
|
|
206
|
-
// projects dirty so the next semantic query reloads closed-file contents.
|
|
207
|
-
writeFile(projectRoot, "src/closed.ts", "export const closedValue = 2 as const;\n");
|
|
208
|
-
|
|
209
|
-
await waitFor("closed file update to refresh semantic project state", async () => {
|
|
210
|
-
const deps = await callTool<DependencyTreeResult>("ts_dependency_tree", {
|
|
211
|
-
file: "src/closed-consumer.ts",
|
|
212
|
-
});
|
|
213
|
-
const normalizedDeps = deps.files.map(normalize);
|
|
214
|
-
assert.ok(normalizedDeps.includes("src/closed.ts"), `Expected src/closed.ts in ${normalizedDeps}`);
|
|
215
|
-
|
|
216
|
-
const typeInfo = await callTool<TypeInfoResult>("ts_type_info", {
|
|
217
|
-
file: "src/closed-consumer.ts",
|
|
218
|
-
line: 2,
|
|
219
|
-
column: 14,
|
|
220
|
-
});
|
|
221
|
-
assert.match(typeInfo.type ?? "", /\b2\b/);
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
// Open util.ts directly so tsserver tracks it, then delete it from disk.
|
|
225
|
-
const utilInfo = await callTool<TypeInfoResult>("ts_type_info", {
|
|
226
|
-
file: "src/util.ts",
|
|
227
|
-
line: 1,
|
|
228
|
-
column: 14,
|
|
229
|
-
});
|
|
230
|
-
assert.match(utilInfo.type ?? "", /\bhelper: 1\b/);
|
|
231
|
-
fs.rmSync(path.join(projectRoot, "src/util.ts"));
|
|
232
|
-
|
|
233
|
-
await waitFor("deleted file to disappear from semantic answers", async () => {
|
|
234
|
-
const deletedInfo = await callTool<TypeInfoResult>("ts_type_info", {
|
|
235
|
-
file: "src/util.ts",
|
|
236
|
-
line: 1,
|
|
237
|
-
column: 14,
|
|
238
|
-
});
|
|
239
|
-
assert.equal(
|
|
240
|
-
deletedInfo.type,
|
|
241
|
-
null,
|
|
242
|
-
`Expected deleted file to have no type info, got ${JSON.stringify(deletedInfo)}`
|
|
243
|
-
);
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
console.log("");
|
|
247
|
-
console.log("typegraph-mcp Engine Sync Test");
|
|
248
|
-
console.log("==============================");
|
|
249
|
-
console.log(" ✓ import swaps keep dependency_tree and type_info aligned");
|
|
250
|
-
console.log(" ✓ export renames refresh ts_module_exports semantic metadata");
|
|
251
|
-
console.log(" ✓ closed-file edits refresh tsserver project state");
|
|
252
|
-
console.log(" ✓ deleted open files do not survive as tsserver ghost snapshots");
|
|
253
|
-
} finally {
|
|
254
|
-
await transport.close().catch(() => {});
|
|
255
|
-
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
main().catch((err) => {
|
|
260
|
-
console.error(err);
|
|
261
|
-
process.exit(1);
|
|
262
|
-
});
|