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/dist/module-graph.js
CHANGED
|
@@ -18,8 +18,19 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
|
18
18
|
"coverage"
|
|
19
19
|
]);
|
|
20
20
|
var SKIP_FILES = /* @__PURE__ */ new Set(["routeTree.gen.ts"]);
|
|
21
|
-
function
|
|
21
|
+
function normalizeExcludedPaths(rootDir, excludedPaths) {
|
|
22
|
+
return excludedPaths.map(
|
|
23
|
+
(excludedPath) => path.resolve(rootDir, excludedPath)
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
function isExcluded(filePath, excludedPaths) {
|
|
27
|
+
return excludedPaths.some(
|
|
28
|
+
(excludedPath) => filePath === excludedPath || filePath.startsWith(excludedPath + path.sep)
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
function discoverFiles(rootDir, excludedPaths = []) {
|
|
22
32
|
const files = [];
|
|
33
|
+
const normalizedExclusions = normalizeExcludedPaths(rootDir, excludedPaths);
|
|
23
34
|
function walk(dir) {
|
|
24
35
|
let entries;
|
|
25
36
|
try {
|
|
@@ -31,7 +42,9 @@ function discoverFiles(rootDir) {
|
|
|
31
42
|
if (entry.isDirectory()) {
|
|
32
43
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
33
44
|
if (entry.name.startsWith(".") && dir !== rootDir) continue;
|
|
34
|
-
|
|
45
|
+
const childDir = path.join(dir, entry.name);
|
|
46
|
+
if (isExcluded(childDir, normalizedExclusions)) continue;
|
|
47
|
+
walk(childDir);
|
|
35
48
|
} else if (entry.isFile()) {
|
|
36
49
|
const name = entry.name;
|
|
37
50
|
if (SKIP_FILES.has(name)) continue;
|
|
@@ -132,11 +145,9 @@ function resolveProjectImport(resolver, fromDir, specifier, projectRoot) {
|
|
|
132
145
|
return null;
|
|
133
146
|
}
|
|
134
147
|
function createResolver(projectRoot, tsconfigPath) {
|
|
148
|
+
const configFile = path.resolve(projectRoot, tsconfigPath);
|
|
135
149
|
return new ResolverFactory({
|
|
136
|
-
tsconfig: {
|
|
137
|
-
configFile: path.resolve(projectRoot, tsconfigPath),
|
|
138
|
-
references: "auto"
|
|
139
|
-
},
|
|
150
|
+
...fs.existsSync(configFile) ? { tsconfig: { configFile, references: "auto" } } : {},
|
|
140
151
|
extensions: [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"],
|
|
141
152
|
extensionAlias: {
|
|
142
153
|
".js": [".ts", ".tsx", ".js"],
|
|
@@ -202,10 +213,10 @@ function buildReverseMap(forward) {
|
|
|
202
213
|
}
|
|
203
214
|
return reverse;
|
|
204
215
|
}
|
|
205
|
-
async function buildGraph(projectRoot, tsconfigPath) {
|
|
216
|
+
async function buildGraph(projectRoot, tsconfigPath, excludedPaths = []) {
|
|
206
217
|
const startTime = performance.now();
|
|
207
218
|
const resolver = createResolver(projectRoot, tsconfigPath);
|
|
208
|
-
const fileList = discoverFiles(projectRoot);
|
|
219
|
+
const fileList = discoverFiles(projectRoot, excludedPaths);
|
|
209
220
|
log(`Discovered ${fileList.length} source files`);
|
|
210
221
|
const { forward, parseFailures } = buildForwardEdges(fileList, resolver, projectRoot);
|
|
211
222
|
const reverse = buildReverseMap(forward);
|
|
@@ -297,8 +308,9 @@ function removeFile(graph, filePath) {
|
|
|
297
308
|
graph.reverse.delete(filePath);
|
|
298
309
|
graph.files.delete(filePath);
|
|
299
310
|
}
|
|
300
|
-
function startWatcher(projectRoot, graph, resolver, hooks) {
|
|
311
|
+
function startWatcher(projectRoot, graph, resolver, hooks, excludedPaths = []) {
|
|
301
312
|
try {
|
|
313
|
+
const normalizedExclusions = normalizeExcludedPaths(projectRoot, excludedPaths);
|
|
302
314
|
const watcher = fs.watch(
|
|
303
315
|
projectRoot,
|
|
304
316
|
{ recursive: true },
|
|
@@ -312,6 +324,7 @@ function startWatcher(projectRoot, graph, resolver, hooks) {
|
|
|
312
324
|
if (filename.endsWith(".d.ts") || filename.endsWith(".d.mts") || filename.endsWith(".d.cts"))
|
|
313
325
|
return;
|
|
314
326
|
const absPath = path.resolve(projectRoot, filename);
|
|
327
|
+
if (isExcluded(absPath, normalizedExclusions)) return;
|
|
315
328
|
if (fs.existsSync(absPath)) {
|
|
316
329
|
updateFile(graph, absPath, resolver, projectRoot);
|
|
317
330
|
void hooks?.onFileUpdated?.(absPath);
|
package/dist/server.js
CHANGED
|
@@ -12,6 +12,30 @@ import * as fs from "fs";
|
|
|
12
12
|
import { createRequire } from "module";
|
|
13
13
|
var log = (...args) => console.error("[typegraph/tsserver]", ...args);
|
|
14
14
|
var REQUEST_TIMEOUT_MS = 1e4;
|
|
15
|
+
function readPackageVersion(packagePath) {
|
|
16
|
+
const pkg = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
|
|
17
|
+
return pkg.version ?? "unknown";
|
|
18
|
+
}
|
|
19
|
+
function resolveTsServer(projectRoot2) {
|
|
20
|
+
const projectRequire = createRequire(path.resolve(projectRoot2, "package.json"));
|
|
21
|
+
let projectVersion;
|
|
22
|
+
try {
|
|
23
|
+
const packagePath2 = projectRequire.resolve("typescript/package.json");
|
|
24
|
+
projectVersion = readPackageVersion(packagePath2);
|
|
25
|
+
const serverPath2 = projectRequire.resolve("typescript/lib/tsserver.js");
|
|
26
|
+
return { path: serverPath2, version: projectVersion, source: "project" };
|
|
27
|
+
} catch {
|
|
28
|
+
}
|
|
29
|
+
const toolRequire = createRequire(import.meta.url);
|
|
30
|
+
const packagePath = toolRequire.resolve("typescript/package.json");
|
|
31
|
+
const serverPath = toolRequire.resolve("typescript/lib/tsserver.js");
|
|
32
|
+
return {
|
|
33
|
+
path: serverPath,
|
|
34
|
+
version: readPackageVersion(packagePath),
|
|
35
|
+
source: "typegraph",
|
|
36
|
+
projectVersion
|
|
37
|
+
};
|
|
38
|
+
}
|
|
15
39
|
var TsServerClient = class {
|
|
16
40
|
constructor(projectRoot2, tsconfigPath2 = "./tsconfig.json") {
|
|
17
41
|
this.projectRoot = projectRoot2;
|
|
@@ -48,9 +72,12 @@ var TsServerClient = class {
|
|
|
48
72
|
// ─── Lifecycle ──────────────────────────────────────────────────────────
|
|
49
73
|
async start() {
|
|
50
74
|
if (this.child) return;
|
|
51
|
-
const
|
|
52
|
-
const tsserverPath =
|
|
53
|
-
|
|
75
|
+
const resolution = resolveTsServer(this.projectRoot);
|
|
76
|
+
const tsserverPath = resolution.path;
|
|
77
|
+
const fallbackNote = resolution.projectVersion ? `; project TypeScript ${resolution.projectVersion} has no legacy tsserver` : "";
|
|
78
|
+
log(
|
|
79
|
+
`Spawning tsserver: ${tsserverPath} (TypeScript ${resolution.version}, ${resolution.source}${fallbackNote})`
|
|
80
|
+
);
|
|
54
81
|
log(`Project root: ${this.projectRoot}`);
|
|
55
82
|
log(`tsconfig: ${this.tsconfigPath}`);
|
|
56
83
|
this.child = spawn("node", [tsserverPath, "--disableAutomaticTypingAcquisition"], {
|
|
@@ -79,12 +106,9 @@ var TsServerClient = class {
|
|
|
79
106
|
}
|
|
80
107
|
});
|
|
81
108
|
const warmStart = performance.now();
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
options: { allowJs: true, checkJs: false }
|
|
86
|
-
});
|
|
87
|
-
}
|
|
109
|
+
await this.sendRequest("compilerOptionsForInferredProjects", {
|
|
110
|
+
options: { allowJs: true, checkJs: false }
|
|
111
|
+
});
|
|
88
112
|
this.ready = true;
|
|
89
113
|
log(`Ready [${(performance.now() - warmStart).toFixed(0)}ms configure]`);
|
|
90
114
|
}
|
|
@@ -328,8 +352,19 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
|
328
352
|
"coverage"
|
|
329
353
|
]);
|
|
330
354
|
var SKIP_FILES = /* @__PURE__ */ new Set(["routeTree.gen.ts"]);
|
|
331
|
-
function
|
|
355
|
+
function normalizeExcludedPaths(rootDir, excludedPaths2) {
|
|
356
|
+
return excludedPaths2.map(
|
|
357
|
+
(excludedPath) => path2.resolve(rootDir, excludedPath)
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
function isExcluded(filePath, excludedPaths2) {
|
|
361
|
+
return excludedPaths2.some(
|
|
362
|
+
(excludedPath) => filePath === excludedPath || filePath.startsWith(excludedPath + path2.sep)
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
function discoverFiles(rootDir, excludedPaths2 = []) {
|
|
332
366
|
const files = [];
|
|
367
|
+
const normalizedExclusions = normalizeExcludedPaths(rootDir, excludedPaths2);
|
|
333
368
|
function walk(dir) {
|
|
334
369
|
let entries;
|
|
335
370
|
try {
|
|
@@ -341,7 +376,9 @@ function discoverFiles(rootDir) {
|
|
|
341
376
|
if (entry.isDirectory()) {
|
|
342
377
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
343
378
|
if (entry.name.startsWith(".") && dir !== rootDir) continue;
|
|
344
|
-
|
|
379
|
+
const childDir = path2.join(dir, entry.name);
|
|
380
|
+
if (isExcluded(childDir, normalizedExclusions)) continue;
|
|
381
|
+
walk(childDir);
|
|
345
382
|
} else if (entry.isFile()) {
|
|
346
383
|
const name = entry.name;
|
|
347
384
|
if (SKIP_FILES.has(name)) continue;
|
|
@@ -442,11 +479,9 @@ function resolveProjectImport(resolver, fromDir, specifier, projectRoot2) {
|
|
|
442
479
|
return null;
|
|
443
480
|
}
|
|
444
481
|
function createResolver(projectRoot2, tsconfigPath2) {
|
|
482
|
+
const configFile = path2.resolve(projectRoot2, tsconfigPath2);
|
|
445
483
|
return new ResolverFactory({
|
|
446
|
-
tsconfig: {
|
|
447
|
-
configFile: path2.resolve(projectRoot2, tsconfigPath2),
|
|
448
|
-
references: "auto"
|
|
449
|
-
},
|
|
484
|
+
...fs2.existsSync(configFile) ? { tsconfig: { configFile, references: "auto" } } : {},
|
|
450
485
|
extensions: [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"],
|
|
451
486
|
extensionAlias: {
|
|
452
487
|
".js": [".ts", ".tsx", ".js"],
|
|
@@ -512,10 +547,10 @@ function buildReverseMap(forward) {
|
|
|
512
547
|
}
|
|
513
548
|
return reverse;
|
|
514
549
|
}
|
|
515
|
-
async function buildGraph(projectRoot2, tsconfigPath2) {
|
|
550
|
+
async function buildGraph(projectRoot2, tsconfigPath2, excludedPaths2 = []) {
|
|
516
551
|
const startTime = performance.now();
|
|
517
552
|
const resolver = createResolver(projectRoot2, tsconfigPath2);
|
|
518
|
-
const fileList = discoverFiles(projectRoot2);
|
|
553
|
+
const fileList = discoverFiles(projectRoot2, excludedPaths2);
|
|
519
554
|
log2(`Discovered ${fileList.length} source files`);
|
|
520
555
|
const { forward, parseFailures } = buildForwardEdges(fileList, resolver, projectRoot2);
|
|
521
556
|
const reverse = buildReverseMap(forward);
|
|
@@ -607,8 +642,9 @@ function removeFile(graph, filePath) {
|
|
|
607
642
|
graph.reverse.delete(filePath);
|
|
608
643
|
graph.files.delete(filePath);
|
|
609
644
|
}
|
|
610
|
-
function startWatcher(projectRoot2, graph, resolver, hooks) {
|
|
645
|
+
function startWatcher(projectRoot2, graph, resolver, hooks, excludedPaths2 = []) {
|
|
611
646
|
try {
|
|
647
|
+
const normalizedExclusions = normalizeExcludedPaths(projectRoot2, excludedPaths2);
|
|
612
648
|
const watcher = fs2.watch(
|
|
613
649
|
projectRoot2,
|
|
614
650
|
{ recursive: true },
|
|
@@ -622,6 +658,7 @@ function startWatcher(projectRoot2, graph, resolver, hooks) {
|
|
|
622
658
|
if (filename.endsWith(".d.ts") || filename.endsWith(".d.mts") || filename.endsWith(".d.cts"))
|
|
623
659
|
return;
|
|
624
660
|
const absPath2 = path2.resolve(projectRoot2, filename);
|
|
661
|
+
if (isExcluded(absPath2, normalizedExclusions)) return;
|
|
625
662
|
if (fs2.existsSync(absPath2)) {
|
|
626
663
|
updateFile(graph, absPath2, resolver, projectRoot2);
|
|
627
664
|
void hooks?.onFileUpdated?.(absPath2);
|
|
@@ -917,17 +954,18 @@ import * as path5 from "path";
|
|
|
917
954
|
|
|
918
955
|
// config.ts
|
|
919
956
|
import * as path4 from "path";
|
|
920
|
-
function resolveConfig(
|
|
957
|
+
function resolveConfig(toolDir2) {
|
|
921
958
|
const cwd = process.cwd();
|
|
922
|
-
const projectRoot2 = process.env["TYPEGRAPH_PROJECT_ROOT"] ? path4.resolve(cwd, process.env["TYPEGRAPH_PROJECT_ROOT"]) : path4.basename(path4.dirname(
|
|
959
|
+
const projectRoot2 = process.env["TYPEGRAPH_PROJECT_ROOT"] ? path4.resolve(cwd, process.env["TYPEGRAPH_PROJECT_ROOT"]) : path4.basename(path4.dirname(toolDir2)) === "plugins" ? path4.resolve(toolDir2, "../..") : cwd;
|
|
923
960
|
const tsconfigPath2 = process.env["TYPEGRAPH_TSCONFIG"] || "./tsconfig.json";
|
|
924
|
-
const
|
|
925
|
-
const toolRelPath =
|
|
926
|
-
return { projectRoot: projectRoot2, tsconfigPath: tsconfigPath2, toolDir, toolIsEmbedded, toolRelPath };
|
|
961
|
+
const toolIsEmbedded2 = toolDir2.startsWith(projectRoot2 + path4.sep);
|
|
962
|
+
const toolRelPath = toolIsEmbedded2 ? path4.relative(projectRoot2, toolDir2) : toolDir2;
|
|
963
|
+
return { projectRoot: projectRoot2, tsconfigPath: tsconfigPath2, toolDir: toolDir2, toolIsEmbedded: toolIsEmbedded2, toolRelPath };
|
|
927
964
|
}
|
|
928
965
|
|
|
929
966
|
// server.ts
|
|
930
|
-
var { projectRoot, tsconfigPath } = resolveConfig(import.meta.dirname);
|
|
967
|
+
var { projectRoot, tsconfigPath, toolDir, toolIsEmbedded } = resolveConfig(import.meta.dirname);
|
|
968
|
+
var excludedPaths = toolIsEmbedded ? [toolDir] : [];
|
|
931
969
|
var log3 = (...args) => console.error("[typegraph]", ...args);
|
|
932
970
|
var client = new TsServerClient(projectRoot, tsconfigPath);
|
|
933
971
|
var moduleGraph;
|
|
@@ -1714,25 +1752,31 @@ async function main() {
|
|
|
1714
1752
|
log3(`tsconfig: ${tsconfigPath}`);
|
|
1715
1753
|
const [, graphResult] = await Promise.all([
|
|
1716
1754
|
client.start(),
|
|
1717
|
-
buildGraph(projectRoot, tsconfigPath)
|
|
1755
|
+
buildGraph(projectRoot, tsconfigPath, excludedPaths)
|
|
1718
1756
|
]);
|
|
1719
1757
|
moduleGraph = graphResult.graph;
|
|
1720
1758
|
moduleResolver = graphResult.resolver;
|
|
1721
|
-
startWatcher(
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
(
|
|
1759
|
+
startWatcher(
|
|
1760
|
+
projectRoot,
|
|
1761
|
+
moduleGraph,
|
|
1762
|
+
graphResult.resolver,
|
|
1763
|
+
{
|
|
1764
|
+
onFileUpdated: (filePath) => client.reloadOpenFile(filePath).then(
|
|
1765
|
+
(wasOpen) => {
|
|
1766
|
+
if (!wasOpen) client.markProjectsDirty();
|
|
1767
|
+
},
|
|
1768
|
+
(err) => {
|
|
1769
|
+
client.markProjectsDirty();
|
|
1770
|
+
log3(`Failed to reload open file ${relPath(filePath)}:`, err);
|
|
1771
|
+
}
|
|
1772
|
+
),
|
|
1773
|
+
onFileDeleted: (filePath) => {
|
|
1774
|
+
client.closeFile(filePath);
|
|
1727
1775
|
client.markProjectsDirty();
|
|
1728
|
-
log3(`Failed to reload open file ${relPath(filePath)}:`, err);
|
|
1729
1776
|
}
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
client.markProjectsDirty();
|
|
1734
|
-
}
|
|
1735
|
-
});
|
|
1777
|
+
},
|
|
1778
|
+
excludedPaths
|
|
1779
|
+
);
|
|
1736
1780
|
const transport = new StdioServerTransport();
|
|
1737
1781
|
await mcpServer.connect(transport);
|
|
1738
1782
|
log3("MCP server connected and ready");
|
package/dist/smoke-test.js
CHANGED
|
@@ -10,6 +10,30 @@ import * as fs from "fs";
|
|
|
10
10
|
import { createRequire } from "module";
|
|
11
11
|
var log = (...args) => console.error("[typegraph/tsserver]", ...args);
|
|
12
12
|
var REQUEST_TIMEOUT_MS = 1e4;
|
|
13
|
+
function readPackageVersion(packagePath) {
|
|
14
|
+
const pkg = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
|
|
15
|
+
return pkg.version ?? "unknown";
|
|
16
|
+
}
|
|
17
|
+
function resolveTsServer(projectRoot) {
|
|
18
|
+
const projectRequire = createRequire(path.resolve(projectRoot, "package.json"));
|
|
19
|
+
let projectVersion;
|
|
20
|
+
try {
|
|
21
|
+
const packagePath2 = projectRequire.resolve("typescript/package.json");
|
|
22
|
+
projectVersion = readPackageVersion(packagePath2);
|
|
23
|
+
const serverPath2 = projectRequire.resolve("typescript/lib/tsserver.js");
|
|
24
|
+
return { path: serverPath2, version: projectVersion, source: "project" };
|
|
25
|
+
} catch {
|
|
26
|
+
}
|
|
27
|
+
const toolRequire = createRequire(import.meta.url);
|
|
28
|
+
const packagePath = toolRequire.resolve("typescript/package.json");
|
|
29
|
+
const serverPath = toolRequire.resolve("typescript/lib/tsserver.js");
|
|
30
|
+
return {
|
|
31
|
+
path: serverPath,
|
|
32
|
+
version: readPackageVersion(packagePath),
|
|
33
|
+
source: "typegraph",
|
|
34
|
+
projectVersion
|
|
35
|
+
};
|
|
36
|
+
}
|
|
13
37
|
var TsServerClient = class {
|
|
14
38
|
constructor(projectRoot, tsconfigPath = "./tsconfig.json") {
|
|
15
39
|
this.projectRoot = projectRoot;
|
|
@@ -46,9 +70,12 @@ var TsServerClient = class {
|
|
|
46
70
|
// ─── Lifecycle ──────────────────────────────────────────────────────────
|
|
47
71
|
async start() {
|
|
48
72
|
if (this.child) return;
|
|
49
|
-
const
|
|
50
|
-
const tsserverPath =
|
|
51
|
-
|
|
73
|
+
const resolution = resolveTsServer(this.projectRoot);
|
|
74
|
+
const tsserverPath = resolution.path;
|
|
75
|
+
const fallbackNote = resolution.projectVersion ? `; project TypeScript ${resolution.projectVersion} has no legacy tsserver` : "";
|
|
76
|
+
log(
|
|
77
|
+
`Spawning tsserver: ${tsserverPath} (TypeScript ${resolution.version}, ${resolution.source}${fallbackNote})`
|
|
78
|
+
);
|
|
52
79
|
log(`Project root: ${this.projectRoot}`);
|
|
53
80
|
log(`tsconfig: ${this.tsconfigPath}`);
|
|
54
81
|
this.child = spawn("node", [tsserverPath, "--disableAutomaticTypingAcquisition"], {
|
|
@@ -77,12 +104,9 @@ var TsServerClient = class {
|
|
|
77
104
|
}
|
|
78
105
|
});
|
|
79
106
|
const warmStart = performance.now();
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
options: { allowJs: true, checkJs: false }
|
|
84
|
-
});
|
|
85
|
-
}
|
|
107
|
+
await this.sendRequest("compilerOptionsForInferredProjects", {
|
|
108
|
+
options: { allowJs: true, checkJs: false }
|
|
109
|
+
});
|
|
86
110
|
this.ready = true;
|
|
87
111
|
log(`Ready [${(performance.now() - warmStart).toFixed(0)}ms configure]`);
|
|
88
112
|
}
|
|
@@ -177,12 +201,12 @@ var TsServerClient = class {
|
|
|
177
201
|
command,
|
|
178
202
|
arguments: args
|
|
179
203
|
};
|
|
180
|
-
return new Promise((
|
|
204
|
+
return new Promise((resolve6, reject) => {
|
|
181
205
|
const timer = setTimeout(() => {
|
|
182
206
|
this.pending.delete(seq);
|
|
183
207
|
reject(new Error(`tsserver ${command} timed out after ${REQUEST_TIMEOUT_MS}ms`));
|
|
184
208
|
}, REQUEST_TIMEOUT_MS);
|
|
185
|
-
this.pending.set(seq, { resolve:
|
|
209
|
+
this.pending.set(seq, { resolve: resolve6, reject, timer, command });
|
|
186
210
|
this.child.stdin.write(JSON.stringify(request) + "\n");
|
|
187
211
|
});
|
|
188
212
|
}
|
|
@@ -326,8 +350,19 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
|
326
350
|
"coverage"
|
|
327
351
|
]);
|
|
328
352
|
var SKIP_FILES = /* @__PURE__ */ new Set(["routeTree.gen.ts"]);
|
|
329
|
-
function
|
|
353
|
+
function normalizeExcludedPaths(rootDir, excludedPaths) {
|
|
354
|
+
return excludedPaths.map(
|
|
355
|
+
(excludedPath) => path2.resolve(rootDir, excludedPath)
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
function isExcluded(filePath, excludedPaths) {
|
|
359
|
+
return excludedPaths.some(
|
|
360
|
+
(excludedPath) => filePath === excludedPath || filePath.startsWith(excludedPath + path2.sep)
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
function discoverFiles(rootDir, excludedPaths = []) {
|
|
330
364
|
const files = [];
|
|
365
|
+
const normalizedExclusions = normalizeExcludedPaths(rootDir, excludedPaths);
|
|
331
366
|
function walk(dir) {
|
|
332
367
|
let entries;
|
|
333
368
|
try {
|
|
@@ -339,7 +374,9 @@ function discoverFiles(rootDir) {
|
|
|
339
374
|
if (entry.isDirectory()) {
|
|
340
375
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
341
376
|
if (entry.name.startsWith(".") && dir !== rootDir) continue;
|
|
342
|
-
|
|
377
|
+
const childDir = path2.join(dir, entry.name);
|
|
378
|
+
if (isExcluded(childDir, normalizedExclusions)) continue;
|
|
379
|
+
walk(childDir);
|
|
343
380
|
} else if (entry.isFile()) {
|
|
344
381
|
const name = entry.name;
|
|
345
382
|
if (SKIP_FILES.has(name)) continue;
|
|
@@ -440,11 +477,9 @@ function resolveProjectImport(resolver, fromDir, specifier, projectRoot) {
|
|
|
440
477
|
return null;
|
|
441
478
|
}
|
|
442
479
|
function createResolver(projectRoot, tsconfigPath) {
|
|
480
|
+
const configFile = path2.resolve(projectRoot, tsconfigPath);
|
|
443
481
|
return new ResolverFactory({
|
|
444
|
-
tsconfig: {
|
|
445
|
-
configFile: path2.resolve(projectRoot, tsconfigPath),
|
|
446
|
-
references: "auto"
|
|
447
|
-
},
|
|
482
|
+
...fs2.existsSync(configFile) ? { tsconfig: { configFile, references: "auto" } } : {},
|
|
448
483
|
extensions: [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"],
|
|
449
484
|
extensionAlias: {
|
|
450
485
|
".js": [".ts", ".tsx", ".js"],
|
|
@@ -510,10 +545,10 @@ function buildReverseMap(forward) {
|
|
|
510
545
|
}
|
|
511
546
|
return reverse;
|
|
512
547
|
}
|
|
513
|
-
async function buildGraph(projectRoot, tsconfigPath) {
|
|
548
|
+
async function buildGraph(projectRoot, tsconfigPath, excludedPaths = []) {
|
|
514
549
|
const startTime = performance.now();
|
|
515
550
|
const resolver = createResolver(projectRoot, tsconfigPath);
|
|
516
|
-
const fileList = discoverFiles(projectRoot);
|
|
551
|
+
const fileList = discoverFiles(projectRoot, excludedPaths);
|
|
517
552
|
log2(`Discovered ${fileList.length} source files`);
|
|
518
553
|
const { forward, parseFailures } = buildForwardEdges(fileList, resolver, projectRoot);
|
|
519
554
|
const reverse = buildReverseMap(forward);
|
|
@@ -827,6 +862,47 @@ function findInNavBar(items, predicate) {
|
|
|
827
862
|
}
|
|
828
863
|
return null;
|
|
829
864
|
}
|
|
865
|
+
function symbolPositions(symbol, sourceLines) {
|
|
866
|
+
const span = symbol.spans[0];
|
|
867
|
+
const positions = [];
|
|
868
|
+
const lineText = sourceLines[span.start.line - 1];
|
|
869
|
+
if (lineText) {
|
|
870
|
+
const nameIndex = lineText.indexOf(symbol.text, Math.max(0, span.start.offset - 1));
|
|
871
|
+
if (nameIndex >= 0) {
|
|
872
|
+
positions.push({ line: span.start.line, offset: nameIndex + 1 });
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
if (!positions.some((position) => position.offset === span.start.offset)) {
|
|
876
|
+
positions.push(span.start);
|
|
877
|
+
}
|
|
878
|
+
return positions;
|
|
879
|
+
}
|
|
880
|
+
async function probeQuickInfo(client, file, symbol, sourceLines) {
|
|
881
|
+
for (const position of symbolPositions(symbol, sourceLines)) {
|
|
882
|
+
const info = await client.quickinfo(file, position.line, position.offset);
|
|
883
|
+
if (info) return { info, position };
|
|
884
|
+
}
|
|
885
|
+
return null;
|
|
886
|
+
}
|
|
887
|
+
async function selectQuickInfoSymbol(client, file, symbols, sourceLines) {
|
|
888
|
+
const kindPriority = /* @__PURE__ */ new Map([
|
|
889
|
+
["const", 0],
|
|
890
|
+
["let", 1],
|
|
891
|
+
["var", 2],
|
|
892
|
+
["class", 3],
|
|
893
|
+
["enum", 4],
|
|
894
|
+
["function", 5],
|
|
895
|
+
["method", 6]
|
|
896
|
+
]);
|
|
897
|
+
const concreteSymbols = symbols.map((symbol, index) => ({ symbol, index })).filter(({ symbol }) => kindPriority.has(symbol.kind)).sort(
|
|
898
|
+
(a, b) => kindPriority.get(a.symbol.kind) - kindPriority.get(b.symbol.kind) || a.index - b.index
|
|
899
|
+
).map(({ symbol }) => symbol);
|
|
900
|
+
for (const symbol of concreteSymbols) {
|
|
901
|
+
const probe = await probeQuickInfo(client, file, symbol, sourceLines);
|
|
902
|
+
if (probe) return { symbol, ...probe };
|
|
903
|
+
}
|
|
904
|
+
return null;
|
|
905
|
+
}
|
|
830
906
|
var SKIP_DIRS2 = /* @__PURE__ */ new Set([
|
|
831
907
|
"node_modules",
|
|
832
908
|
"dist",
|
|
@@ -836,8 +912,9 @@ var SKIP_DIRS2 = /* @__PURE__ */ new Set([
|
|
|
836
912
|
"coverage",
|
|
837
913
|
"out"
|
|
838
914
|
]);
|
|
839
|
-
function findTestFile(rootDir) {
|
|
915
|
+
function findTestFile(rootDir, excludedPaths = []) {
|
|
840
916
|
const candidates = [];
|
|
917
|
+
const normalizedExclusions = excludedPaths.map((excludedPath) => path5.resolve(excludedPath));
|
|
841
918
|
function walk(dir, depth) {
|
|
842
919
|
if (depth > 5 || candidates.length >= 30) return;
|
|
843
920
|
let entries;
|
|
@@ -849,7 +926,11 @@ function findTestFile(rootDir) {
|
|
|
849
926
|
for (const entry of entries) {
|
|
850
927
|
if (entry.isDirectory()) {
|
|
851
928
|
if (SKIP_DIRS2.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
852
|
-
|
|
929
|
+
const childDir = path5.join(dir, entry.name);
|
|
930
|
+
if (normalizedExclusions.some(
|
|
931
|
+
(excludedPath) => childDir === excludedPath || childDir.startsWith(excludedPath + path5.sep)
|
|
932
|
+
)) continue;
|
|
933
|
+
walk(childDir, depth + 1);
|
|
853
934
|
} else if (entry.isFile()) {
|
|
854
935
|
const name = entry.name;
|
|
855
936
|
if (name.endsWith(".d.ts") || name.endsWith(".test.ts") || name.endsWith(".spec.ts"))
|
|
@@ -857,7 +938,7 @@ function findTestFile(rootDir) {
|
|
|
857
938
|
if (!name.endsWith(".ts") && !name.endsWith(".tsx")) continue;
|
|
858
939
|
try {
|
|
859
940
|
const stat = fs4.statSync(path5.join(dir, name));
|
|
860
|
-
if (stat.size >
|
|
941
|
+
if (stat.size > 0 && stat.size < 5e4) {
|
|
861
942
|
candidates.push({ file: path5.join(dir, name), size: stat.size });
|
|
862
943
|
}
|
|
863
944
|
} catch {
|
|
@@ -881,7 +962,8 @@ function findImporter(graph, file) {
|
|
|
881
962
|
return (preferred ?? revEdges[0]).target;
|
|
882
963
|
}
|
|
883
964
|
async function main(configOverride) {
|
|
884
|
-
const { projectRoot, tsconfigPath } = configOverride ?? resolveConfig(import.meta.dirname);
|
|
965
|
+
const { projectRoot, tsconfigPath, toolDir, toolIsEmbedded } = configOverride ?? resolveConfig(import.meta.dirname);
|
|
966
|
+
const excludedPaths = toolIsEmbedded ? [toolDir] : [];
|
|
885
967
|
let passed = 0;
|
|
886
968
|
let failed = 0;
|
|
887
969
|
let skipped = 0;
|
|
@@ -904,7 +986,7 @@ async function main(configOverride) {
|
|
|
904
986
|
console.log("=====================");
|
|
905
987
|
console.log(`Project root: ${projectRoot}`);
|
|
906
988
|
console.log("");
|
|
907
|
-
const testFile = findTestFile(projectRoot);
|
|
989
|
+
const testFile = findTestFile(projectRoot, excludedPaths);
|
|
908
990
|
if (!testFile) {
|
|
909
991
|
console.log(" No suitable .ts file found in project. Cannot run smoke tests.");
|
|
910
992
|
return { passed, failed: failed + 1, skipped };
|
|
@@ -917,7 +999,7 @@ async function main(configOverride) {
|
|
|
917
999
|
let t0;
|
|
918
1000
|
t0 = performance.now();
|
|
919
1001
|
try {
|
|
920
|
-
const result = await buildGraph(projectRoot, tsconfigPath);
|
|
1002
|
+
const result = await buildGraph(projectRoot, tsconfigPath, excludedPaths);
|
|
921
1003
|
graph = result.graph;
|
|
922
1004
|
const ms = performance.now() - t0;
|
|
923
1005
|
const edgeCount = [...graph.forward.values()].reduce((s, e) => s + e.length, 0);
|
|
@@ -1034,8 +1116,17 @@ async function main(configOverride) {
|
|
|
1034
1116
|
} else {
|
|
1035
1117
|
fail("navbar", `No symbols found in ${testFileRel}`, navbarMs);
|
|
1036
1118
|
}
|
|
1037
|
-
const
|
|
1038
|
-
const
|
|
1119
|
+
const source = fs4.readFileSync(testFile, "utf-8");
|
|
1120
|
+
const sourceLines = source.split(/\r?\n/);
|
|
1121
|
+
const quickInfoSelection = await selectQuickInfoSymbol(
|
|
1122
|
+
client,
|
|
1123
|
+
testFileRel,
|
|
1124
|
+
allSymbols,
|
|
1125
|
+
sourceLines
|
|
1126
|
+
);
|
|
1127
|
+
const sym = quickInfoSelection?.symbol ?? allSymbols[0];
|
|
1128
|
+
const selectedQuickInfo = quickInfoSelection?.info ?? null;
|
|
1129
|
+
const selectedPosition = quickInfoSelection?.position ?? sym?.spans[0]?.start;
|
|
1039
1130
|
if (!sym) {
|
|
1040
1131
|
const toolNames = [
|
|
1041
1132
|
"find_symbol",
|
|
@@ -1050,6 +1141,7 @@ async function main(configOverride) {
|
|
|
1050
1141
|
for (const name of toolNames) skip(name, "No symbol discovered");
|
|
1051
1142
|
} else {
|
|
1052
1143
|
const span = sym.spans[0];
|
|
1144
|
+
const point = selectedPosition ?? span.start;
|
|
1053
1145
|
t0 = performance.now();
|
|
1054
1146
|
const found = findInNavBar(bar, (item) => item.text === sym.text && item.kind === sym.kind);
|
|
1055
1147
|
if (found && found.spans.length > 0) {
|
|
@@ -1062,7 +1154,7 @@ async function main(configOverride) {
|
|
|
1062
1154
|
fail("find_symbol", `Could not re-find ${sym.text}`, performance.now() - t0);
|
|
1063
1155
|
}
|
|
1064
1156
|
t0 = performance.now();
|
|
1065
|
-
const defs = await client.definition(testFileRel,
|
|
1157
|
+
const defs = await client.definition(testFileRel, point.line, point.offset);
|
|
1066
1158
|
if (defs.length > 0) {
|
|
1067
1159
|
const def = defs[0];
|
|
1068
1160
|
pass("definition", `${sym.text} -> ${def.file}:${def.start.line}`, performance.now() - t0);
|
|
@@ -1070,7 +1162,7 @@ async function main(configOverride) {
|
|
|
1070
1162
|
pass("definition", `${sym.text} is its own definition`, performance.now() - t0);
|
|
1071
1163
|
}
|
|
1072
1164
|
t0 = performance.now();
|
|
1073
|
-
const refs = await client.references(testFileRel,
|
|
1165
|
+
const refs = await client.references(testFileRel, point.line, point.offset);
|
|
1074
1166
|
const refFiles = new Set(refs.map((r) => r.file));
|
|
1075
1167
|
pass(
|
|
1076
1168
|
"references",
|
|
@@ -1078,7 +1170,10 @@ async function main(configOverride) {
|
|
|
1078
1170
|
performance.now() - t0
|
|
1079
1171
|
);
|
|
1080
1172
|
t0 = performance.now();
|
|
1081
|
-
let info =
|
|
1173
|
+
let info = selectedQuickInfo;
|
|
1174
|
+
if (!info) {
|
|
1175
|
+
info = await client.quickinfo(testFileRel, point.line, point.offset);
|
|
1176
|
+
}
|
|
1082
1177
|
if (!info && defs.length > 0) {
|
|
1083
1178
|
const def = defs[0];
|
|
1084
1179
|
info = await client.quickinfo(testFileRel, def.start.line, def.start.offset);
|
|
@@ -1087,7 +1182,7 @@ async function main(configOverride) {
|
|
|
1087
1182
|
const typeStr = info.displayString.length > 80 ? info.displayString.slice(0, 80) + "..." : info.displayString;
|
|
1088
1183
|
pass("type_info", typeStr, performance.now() - t0);
|
|
1089
1184
|
} else {
|
|
1090
|
-
|
|
1185
|
+
skip("type_info", `No discovered symbol returned type info`);
|
|
1091
1186
|
}
|
|
1092
1187
|
t0 = performance.now();
|
|
1093
1188
|
const navItems = await client.navto(sym.text, 5);
|
|
@@ -1119,7 +1214,6 @@ async function main(configOverride) {
|
|
|
1119
1214
|
const exportSymbols = topItems.filter((item) => symbolKinds.has(item.kind));
|
|
1120
1215
|
pass("module_exports", `${exportSymbols.length} top-level symbol(s)`, performance.now() - t0);
|
|
1121
1216
|
t0 = performance.now();
|
|
1122
|
-
const source = fs4.readFileSync(testFile, "utf-8");
|
|
1123
1217
|
const importMatch = source.match(/^import\s+\{([^}]+)\}\s+from\s+["']([^"']+)["']/m);
|
|
1124
1218
|
if (importMatch) {
|
|
1125
1219
|
const firstName = importMatch[1].split(",")[0].replace(/^type\s+/, "").trim();
|
|
@@ -1180,5 +1274,6 @@ async function main(configOverride) {
|
|
|
1180
1274
|
return { passed, failed, skipped };
|
|
1181
1275
|
}
|
|
1182
1276
|
export {
|
|
1183
|
-
main
|
|
1277
|
+
main,
|
|
1278
|
+
selectQuickInfoSymbol
|
|
1184
1279
|
};
|
package/dist/tsserver-client.js
CHANGED
|
@@ -5,6 +5,30 @@ import * as fs from "fs";
|
|
|
5
5
|
import { createRequire } from "module";
|
|
6
6
|
var log = (...args) => console.error("[typegraph/tsserver]", ...args);
|
|
7
7
|
var REQUEST_TIMEOUT_MS = 1e4;
|
|
8
|
+
function readPackageVersion(packagePath) {
|
|
9
|
+
const pkg = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
|
|
10
|
+
return pkg.version ?? "unknown";
|
|
11
|
+
}
|
|
12
|
+
function resolveTsServer(projectRoot) {
|
|
13
|
+
const projectRequire = createRequire(path.resolve(projectRoot, "package.json"));
|
|
14
|
+
let projectVersion;
|
|
15
|
+
try {
|
|
16
|
+
const packagePath2 = projectRequire.resolve("typescript/package.json");
|
|
17
|
+
projectVersion = readPackageVersion(packagePath2);
|
|
18
|
+
const serverPath2 = projectRequire.resolve("typescript/lib/tsserver.js");
|
|
19
|
+
return { path: serverPath2, version: projectVersion, source: "project" };
|
|
20
|
+
} catch {
|
|
21
|
+
}
|
|
22
|
+
const toolRequire = createRequire(import.meta.url);
|
|
23
|
+
const packagePath = toolRequire.resolve("typescript/package.json");
|
|
24
|
+
const serverPath = toolRequire.resolve("typescript/lib/tsserver.js");
|
|
25
|
+
return {
|
|
26
|
+
path: serverPath,
|
|
27
|
+
version: readPackageVersion(packagePath),
|
|
28
|
+
source: "typegraph",
|
|
29
|
+
projectVersion
|
|
30
|
+
};
|
|
31
|
+
}
|
|
8
32
|
var TsServerClient = class {
|
|
9
33
|
constructor(projectRoot, tsconfigPath = "./tsconfig.json") {
|
|
10
34
|
this.projectRoot = projectRoot;
|
|
@@ -41,9 +65,12 @@ var TsServerClient = class {
|
|
|
41
65
|
// ─── Lifecycle ──────────────────────────────────────────────────────────
|
|
42
66
|
async start() {
|
|
43
67
|
if (this.child) return;
|
|
44
|
-
const
|
|
45
|
-
const tsserverPath =
|
|
46
|
-
|
|
68
|
+
const resolution = resolveTsServer(this.projectRoot);
|
|
69
|
+
const tsserverPath = resolution.path;
|
|
70
|
+
const fallbackNote = resolution.projectVersion ? `; project TypeScript ${resolution.projectVersion} has no legacy tsserver` : "";
|
|
71
|
+
log(
|
|
72
|
+
`Spawning tsserver: ${tsserverPath} (TypeScript ${resolution.version}, ${resolution.source}${fallbackNote})`
|
|
73
|
+
);
|
|
47
74
|
log(`Project root: ${this.projectRoot}`);
|
|
48
75
|
log(`tsconfig: ${this.tsconfigPath}`);
|
|
49
76
|
this.child = spawn("node", [tsserverPath, "--disableAutomaticTypingAcquisition"], {
|
|
@@ -72,12 +99,9 @@ var TsServerClient = class {
|
|
|
72
99
|
}
|
|
73
100
|
});
|
|
74
101
|
const warmStart = performance.now();
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
options: { allowJs: true, checkJs: false }
|
|
79
|
-
});
|
|
80
|
-
}
|
|
102
|
+
await this.sendRequest("compilerOptionsForInferredProjects", {
|
|
103
|
+
options: { allowJs: true, checkJs: false }
|
|
104
|
+
});
|
|
81
105
|
this.ready = true;
|
|
82
106
|
log(`Ready [${(performance.now() - warmStart).toFixed(0)}ms configure]`);
|
|
83
107
|
}
|
|
@@ -301,5 +325,6 @@ var TsServerClient = class {
|
|
|
301
325
|
}
|
|
302
326
|
};
|
|
303
327
|
export {
|
|
304
|
-
TsServerClient
|
|
328
|
+
TsServerClient,
|
|
329
|
+
resolveTsServer
|
|
305
330
|
};
|