typegraph-mcp 0.9.37 → 0.9.39
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/check.ts +55 -16
- package/cli.ts +131 -46
- package/dist/benchmark.js +2 -2
- package/dist/check.js +48 -18
- package/dist/cli.js +419 -97
- package/dist/module-graph.js +4 -3
- package/dist/server.js +261 -43
- package/dist/smoke-test.js +2 -2
- package/export-surface-test.ts +202 -0
- package/install-oxlint-test.ts +95 -0
- package/module-graph.ts +3 -3
- package/package.json +2 -2
- package/server.ts +375 -55
- package/skills/code-exploration/SKILL.md +7 -0
- package/skills/deep-survey/SKILL.md +4 -0
- package/skills/tool-selection/SKILL.md +3 -0
package/dist/module-graph.js
CHANGED
|
@@ -117,7 +117,7 @@ function distToSource(resolvedPath, projectRoot) {
|
|
|
117
117
|
}
|
|
118
118
|
return resolvedPath;
|
|
119
119
|
}
|
|
120
|
-
function
|
|
120
|
+
function resolveProjectImport(resolver, fromDir, specifier, projectRoot) {
|
|
121
121
|
try {
|
|
122
122
|
const result = resolver.sync(fromDir, specifier);
|
|
123
123
|
if (result.path && !result.path.includes("node_modules")) {
|
|
@@ -168,7 +168,7 @@ function buildForwardEdges(files, resolver, projectRoot) {
|
|
|
168
168
|
const edges = [];
|
|
169
169
|
const fromDir = path.dirname(filePath);
|
|
170
170
|
for (const raw of rawImports) {
|
|
171
|
-
const target =
|
|
171
|
+
const target = resolveProjectImport(resolver, fromDir, raw.specifier, projectRoot);
|
|
172
172
|
if (target) {
|
|
173
173
|
edges.push({
|
|
174
174
|
target,
|
|
@@ -249,7 +249,7 @@ function updateFile(graph, filePath, resolver, projectRoot) {
|
|
|
249
249
|
const fromDir = path.dirname(filePath);
|
|
250
250
|
const newEdges = [];
|
|
251
251
|
for (const raw of rawImports) {
|
|
252
|
-
const target =
|
|
252
|
+
const target = resolveProjectImport(resolver, fromDir, raw.specifier, projectRoot);
|
|
253
253
|
if (target) {
|
|
254
254
|
newEdges.push({
|
|
255
255
|
target,
|
|
@@ -331,6 +331,7 @@ export {
|
|
|
331
331
|
createResolver,
|
|
332
332
|
discoverFiles,
|
|
333
333
|
removeFile,
|
|
334
|
+
resolveProjectImport,
|
|
334
335
|
startWatcher,
|
|
335
336
|
updateFile
|
|
336
337
|
};
|
package/dist/server.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// server.ts
|
|
3
3
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { parseSync as parseSync2 } from "oxc-parser";
|
|
5
6
|
import { z } from "zod";
|
|
6
7
|
|
|
7
8
|
// tsserver-client.ts
|
|
@@ -387,7 +388,7 @@ function distToSource(resolvedPath, projectRoot2) {
|
|
|
387
388
|
}
|
|
388
389
|
return resolvedPath;
|
|
389
390
|
}
|
|
390
|
-
function
|
|
391
|
+
function resolveProjectImport(resolver, fromDir, specifier, projectRoot2) {
|
|
391
392
|
try {
|
|
392
393
|
const result = resolver.sync(fromDir, specifier);
|
|
393
394
|
if (result.path && !result.path.includes("node_modules")) {
|
|
@@ -438,7 +439,7 @@ function buildForwardEdges(files, resolver, projectRoot2) {
|
|
|
438
439
|
const edges = [];
|
|
439
440
|
const fromDir = path2.dirname(filePath);
|
|
440
441
|
for (const raw of rawImports) {
|
|
441
|
-
const target =
|
|
442
|
+
const target = resolveProjectImport(resolver, fromDir, raw.specifier, projectRoot2);
|
|
442
443
|
if (target) {
|
|
443
444
|
edges.push({
|
|
444
445
|
target,
|
|
@@ -519,7 +520,7 @@ function updateFile(graph, filePath, resolver, projectRoot2) {
|
|
|
519
520
|
const fromDir = path2.dirname(filePath);
|
|
520
521
|
const newEdges = [];
|
|
521
522
|
for (const raw of rawImports) {
|
|
522
|
-
const target =
|
|
523
|
+
const target = resolveProjectImport(resolver, fromDir, raw.specifier, projectRoot2);
|
|
523
524
|
if (target) {
|
|
524
525
|
newEdges.push({
|
|
525
526
|
target,
|
|
@@ -889,6 +890,7 @@ var { projectRoot, tsconfigPath } = resolveConfig(import.meta.dirname);
|
|
|
889
890
|
var log3 = (...args) => console.error("[typegraph]", ...args);
|
|
890
891
|
var client = new TsServerClient(projectRoot, tsconfigPath);
|
|
891
892
|
var moduleGraph;
|
|
893
|
+
var moduleResolver;
|
|
892
894
|
var mcpServer = new McpServer({
|
|
893
895
|
name: "typegraph",
|
|
894
896
|
version: "1.0.0"
|
|
@@ -960,6 +962,237 @@ async function resolveParams(params) {
|
|
|
960
962
|
}
|
|
961
963
|
return { error: "Either line+column or symbol must be provided" };
|
|
962
964
|
}
|
|
965
|
+
var exportKinds = /* @__PURE__ */ new Set([
|
|
966
|
+
"function",
|
|
967
|
+
"const",
|
|
968
|
+
"class",
|
|
969
|
+
"interface",
|
|
970
|
+
"type",
|
|
971
|
+
"enum",
|
|
972
|
+
"var",
|
|
973
|
+
"let",
|
|
974
|
+
"method"
|
|
975
|
+
]);
|
|
976
|
+
function exportPriority(source) {
|
|
977
|
+
switch (source) {
|
|
978
|
+
case "local":
|
|
979
|
+
return 3;
|
|
980
|
+
case "re-export":
|
|
981
|
+
return 2;
|
|
982
|
+
case "star-re-export":
|
|
983
|
+
return 1;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
function exportKey(item) {
|
|
987
|
+
return `${item.symbol}:${item.exportKind}`;
|
|
988
|
+
}
|
|
989
|
+
function sameExportOrigin(a, b) {
|
|
990
|
+
return a.symbol === b.symbol && a.exportKind === b.exportKind && a.from === b.from && a.definedIn === b.definedIn && a.definedLine === b.definedLine;
|
|
991
|
+
}
|
|
992
|
+
function kindImpliesTypeOnly(kind) {
|
|
993
|
+
return kind === "type" || kind === "interface";
|
|
994
|
+
}
|
|
995
|
+
function normalizeExportKindLabel(kind, exportKind) {
|
|
996
|
+
if (exportKind === "type" && !kindImpliesTypeOnly(kind)) {
|
|
997
|
+
return "type";
|
|
998
|
+
}
|
|
999
|
+
return kind;
|
|
1000
|
+
}
|
|
1001
|
+
function upsertExport(map, conflicts, nextExport) {
|
|
1002
|
+
const key = exportKey(nextExport);
|
|
1003
|
+
if (conflicts.has(key)) {
|
|
1004
|
+
if (nextExport.source === "star-re-export") return;
|
|
1005
|
+
conflicts.delete(key);
|
|
1006
|
+
map.set(key, nextExport);
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
const existing = map.get(key);
|
|
1010
|
+
if (existing && existing.source === "star-re-export" && nextExport.source === "star-re-export" && !sameExportOrigin(existing, nextExport)) {
|
|
1011
|
+
map.delete(key);
|
|
1012
|
+
conflicts.add(key);
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
if (!existing || exportPriority(nextExport.source) > exportPriority(existing.source)) {
|
|
1016
|
+
map.set(key, nextExport);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
function offsetToLineColumn(source, offset) {
|
|
1020
|
+
const safeOffset = Math.max(0, Math.min(offset ?? 0, source.length));
|
|
1021
|
+
const prefix = source.slice(0, safeOffset);
|
|
1022
|
+
const lines = prefix.split("\n");
|
|
1023
|
+
return {
|
|
1024
|
+
line: lines.length,
|
|
1025
|
+
column: (lines.at(-1)?.length ?? 0) + 1
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
function normalizeExistingPath(filePath) {
|
|
1029
|
+
const resolved = path5.resolve(filePath);
|
|
1030
|
+
try {
|
|
1031
|
+
return fs4.realpathSync.native(resolved);
|
|
1032
|
+
} catch {
|
|
1033
|
+
return resolved;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
var normalizedProjectRoot = normalizeExistingPath(projectRoot);
|
|
1037
|
+
function projectPath(file) {
|
|
1038
|
+
return path5.isAbsolute(file) ? relPath(file) : file;
|
|
1039
|
+
}
|
|
1040
|
+
function exportSymbol(entry) {
|
|
1041
|
+
if (entry.exportName.kind === "Default") return "default";
|
|
1042
|
+
return entry.exportName.name ?? entry.localName.name ?? entry.importName.name;
|
|
1043
|
+
}
|
|
1044
|
+
function exportLookupOffset(entry) {
|
|
1045
|
+
if (entry.moduleRequest) {
|
|
1046
|
+
return entry.importName.start ?? entry.exportName.start ?? entry.start;
|
|
1047
|
+
}
|
|
1048
|
+
if (entry.exportName.kind === "Default") {
|
|
1049
|
+
return entry.localName.start ?? entry.exportName.start ?? entry.start;
|
|
1050
|
+
}
|
|
1051
|
+
return entry.exportName.start ?? entry.localName.start ?? entry.start;
|
|
1052
|
+
}
|
|
1053
|
+
async function resolveExportMetadata(file, line, column, fallbackKind) {
|
|
1054
|
+
const defs = await client.definition(file, line, column);
|
|
1055
|
+
const def = defs[0] ?? null;
|
|
1056
|
+
let info = await client.quickinfo(file, line, column);
|
|
1057
|
+
if ((!info || info.kind === "alias") && def) {
|
|
1058
|
+
info = await client.quickinfo(def.file, def.start.line, def.start.offset) ?? info;
|
|
1059
|
+
}
|
|
1060
|
+
return {
|
|
1061
|
+
kind: info?.kind ?? fallbackKind,
|
|
1062
|
+
type: info?.displayString ?? null,
|
|
1063
|
+
definedIn: projectPath(def?.file ?? file),
|
|
1064
|
+
definedLine: def?.start.line ?? null
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
async function getModuleExports(file, visited = /* @__PURE__ */ new Set()) {
|
|
1068
|
+
const relFile = path5.isAbsolute(file) ? relPath(file) : file;
|
|
1069
|
+
const absFile = normalizeExistingPath(client.resolvePath(relFile));
|
|
1070
|
+
if (visited.has(absFile)) return [];
|
|
1071
|
+
const nextVisited = new Set(visited);
|
|
1072
|
+
nextVisited.add(absFile);
|
|
1073
|
+
const exportMap = /* @__PURE__ */ new Map();
|
|
1074
|
+
const conflictingStarExports = /* @__PURE__ */ new Set();
|
|
1075
|
+
let source;
|
|
1076
|
+
try {
|
|
1077
|
+
source = fs4.readFileSync(absFile, "utf-8");
|
|
1078
|
+
} catch {
|
|
1079
|
+
return [...exportMap.values()];
|
|
1080
|
+
}
|
|
1081
|
+
let parsed;
|
|
1082
|
+
try {
|
|
1083
|
+
parsed = parseSync2(absFile, source);
|
|
1084
|
+
} catch {
|
|
1085
|
+
return [...exportMap.values()];
|
|
1086
|
+
}
|
|
1087
|
+
for (const exp of parsed.module.staticExports) {
|
|
1088
|
+
for (const entry of exp.entries) {
|
|
1089
|
+
const moduleRequest = entry.moduleRequest;
|
|
1090
|
+
if (!moduleRequest) continue;
|
|
1091
|
+
const targetFile = resolveProjectImport(
|
|
1092
|
+
moduleResolver,
|
|
1093
|
+
path5.dirname(absFile),
|
|
1094
|
+
moduleRequest.value,
|
|
1095
|
+
projectRoot
|
|
1096
|
+
);
|
|
1097
|
+
const exportLoc = offsetToLineColumn(
|
|
1098
|
+
source,
|
|
1099
|
+
entry.exportName.start ?? entry.localName.start ?? entry.importName.start ?? entry.start
|
|
1100
|
+
);
|
|
1101
|
+
const importKind = entry.importName.kind;
|
|
1102
|
+
const exportKind = entry.exportName.kind;
|
|
1103
|
+
if (importKind === "AllButDefault" && exportKind === "None") {
|
|
1104
|
+
if (!targetFile) continue;
|
|
1105
|
+
const nestedExports = await getModuleExports(targetFile, nextVisited);
|
|
1106
|
+
for (const nested of nestedExports) {
|
|
1107
|
+
if (nested.symbol === "default") continue;
|
|
1108
|
+
const starExportKind = entry.isType ? "type" : nested.exportKind;
|
|
1109
|
+
upsertExport(exportMap, conflictingStarExports, {
|
|
1110
|
+
...nested,
|
|
1111
|
+
line: exportLoc.line,
|
|
1112
|
+
exportKind: starExportKind,
|
|
1113
|
+
isTypeOnly: starExportKind === "type",
|
|
1114
|
+
source: "star-re-export",
|
|
1115
|
+
from: relPath(targetFile)
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
const symbol = exportSymbol(entry);
|
|
1121
|
+
if (!symbol) continue;
|
|
1122
|
+
const importedSymbol = importKind === "Default" ? "default" : importKind === "Name" ? entry.importName.name : null;
|
|
1123
|
+
const nestedMatch = targetFile && importedSymbol ? (await getModuleExports(targetFile, nextVisited)).find(
|
|
1124
|
+
(item) => item.symbol === importedSymbol
|
|
1125
|
+
) ?? null : null;
|
|
1126
|
+
const lookupLoc = offsetToLineColumn(
|
|
1127
|
+
source,
|
|
1128
|
+
exportLookupOffset(entry)
|
|
1129
|
+
);
|
|
1130
|
+
const metadata = await resolveExportMetadata(
|
|
1131
|
+
relFile,
|
|
1132
|
+
lookupLoc.line,
|
|
1133
|
+
lookupLoc.column,
|
|
1134
|
+
importKind === "All" ? "namespace" : "alias"
|
|
1135
|
+
);
|
|
1136
|
+
const resolvedExportKind = entry.isType || nestedMatch?.exportKind === "type" || kindImpliesTypeOnly(nestedMatch?.kind ?? metadata.kind) ? "type" : "value";
|
|
1137
|
+
const resolvedKind = normalizeExportKindLabel(
|
|
1138
|
+
nestedMatch?.kind ?? metadata.kind,
|
|
1139
|
+
resolvedExportKind
|
|
1140
|
+
);
|
|
1141
|
+
upsertExport(exportMap, conflictingStarExports, {
|
|
1142
|
+
symbol,
|
|
1143
|
+
kind: resolvedKind,
|
|
1144
|
+
line: exportLoc.line,
|
|
1145
|
+
type: nestedMatch?.type ?? metadata.type,
|
|
1146
|
+
exportKind: resolvedExportKind,
|
|
1147
|
+
isTypeOnly: resolvedExportKind === "type",
|
|
1148
|
+
isNamespace: importKind === "All",
|
|
1149
|
+
source: "re-export",
|
|
1150
|
+
from: targetFile ? relPath(targetFile) : moduleRequest.value,
|
|
1151
|
+
definedIn: nestedMatch?.definedIn ?? metadata.definedIn,
|
|
1152
|
+
definedLine: nestedMatch?.definedLine ?? metadata.definedLine
|
|
1153
|
+
});
|
|
1154
|
+
continue;
|
|
1155
|
+
}
|
|
1156
|
+
for (const entry of exp.entries) {
|
|
1157
|
+
const moduleRequest = entry.moduleRequest;
|
|
1158
|
+
if (moduleRequest) continue;
|
|
1159
|
+
const symbol = exportSymbol(entry);
|
|
1160
|
+
if (!symbol) continue;
|
|
1161
|
+
const exportLoc = offsetToLineColumn(
|
|
1162
|
+
source,
|
|
1163
|
+
entry.exportName.start ?? entry.localName.start ?? entry.start
|
|
1164
|
+
);
|
|
1165
|
+
const lookupLoc = offsetToLineColumn(source, exportLookupOffset(entry));
|
|
1166
|
+
const metadata = await resolveExportMetadata(
|
|
1167
|
+
relFile,
|
|
1168
|
+
lookupLoc.line,
|
|
1169
|
+
lookupLoc.column,
|
|
1170
|
+
entry.isType ? "type" : "value"
|
|
1171
|
+
);
|
|
1172
|
+
const resolvedExportKind = entry.isType || kindImpliesTypeOnly(metadata.kind) ? "type" : "value";
|
|
1173
|
+
const resolvedKind = normalizeExportKindLabel(metadata.kind, resolvedExportKind);
|
|
1174
|
+
if (resolvedExportKind === "value" && symbol !== "default" && !exportKinds.has(resolvedKind) && resolvedKind !== "namespace" && resolvedKind !== "class") {
|
|
1175
|
+
continue;
|
|
1176
|
+
}
|
|
1177
|
+
upsertExport(exportMap, conflictingStarExports, {
|
|
1178
|
+
symbol,
|
|
1179
|
+
kind: resolvedKind,
|
|
1180
|
+
line: exportLoc.line,
|
|
1181
|
+
type: metadata.type,
|
|
1182
|
+
exportKind: resolvedExportKind,
|
|
1183
|
+
isTypeOnly: resolvedExportKind === "type",
|
|
1184
|
+
isNamespace: false,
|
|
1185
|
+
source: "local",
|
|
1186
|
+
from: null,
|
|
1187
|
+
definedIn: relFile,
|
|
1188
|
+
definedLine: resolvedExportKind === "type" ? exportLoc.line : metadata.definedLine
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
return [...exportMap.values()].sort(
|
|
1193
|
+
(a, b) => a.line - b.line || a.symbol.localeCompare(b.symbol)
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
963
1196
|
mcpServer.tool(
|
|
964
1197
|
"ts_find_symbol",
|
|
965
1198
|
"Find a symbol's location in a file by name. Entry point for navigating without exact coordinates.",
|
|
@@ -1225,60 +1458,44 @@ mcpServer.tool(
|
|
|
1225
1458
|
);
|
|
1226
1459
|
mcpServer.tool(
|
|
1227
1460
|
"ts_module_exports",
|
|
1228
|
-
"List all exported symbols from a module with their resolved types. Gives an at-a-glance understanding of what a file provides.",
|
|
1461
|
+
"List all exported symbols from a module with their resolved types, including re-exports when possible. Gives an at-a-glance understanding of what a file provides.",
|
|
1229
1462
|
{
|
|
1230
1463
|
file: z.string().describe("File to inspect")
|
|
1231
1464
|
},
|
|
1232
1465
|
async ({ file }) => {
|
|
1233
|
-
const
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
}
|
|
1244
|
-
const moduleItem = bar.find((item) => item.kind === "module");
|
|
1245
|
-
const topItems = moduleItem?.childItems ?? bar;
|
|
1246
|
-
const exportKinds = /* @__PURE__ */ new Set([
|
|
1247
|
-
"function",
|
|
1248
|
-
"const",
|
|
1249
|
-
"class",
|
|
1250
|
-
"interface",
|
|
1251
|
-
"type",
|
|
1252
|
-
"enum",
|
|
1253
|
-
"var",
|
|
1254
|
-
"let",
|
|
1255
|
-
"method"
|
|
1256
|
-
]);
|
|
1257
|
-
const candidates = topItems.filter((item) => exportKinds.has(item.kind));
|
|
1258
|
-
const exports = [];
|
|
1259
|
-
for (const item of candidates) {
|
|
1260
|
-
if (!item.spans[0]) continue;
|
|
1261
|
-
const span = item.spans[0];
|
|
1262
|
-
const info = await client.quickinfo(file, span.start.line, span.start.offset);
|
|
1263
|
-
exports.push({
|
|
1264
|
-
symbol: item.text,
|
|
1265
|
-
kind: item.kind,
|
|
1266
|
-
line: span.start.line,
|
|
1267
|
-
type: info?.displayString ?? null
|
|
1268
|
-
});
|
|
1269
|
-
}
|
|
1466
|
+
const exports = await getModuleExports(file);
|
|
1467
|
+
const localCount = exports.filter((item) => item.source === "local").length;
|
|
1468
|
+
const reExportCount = exports.length - localCount;
|
|
1469
|
+
const typeOnlyCount = exports.filter((item) => item.isTypeOnly).length;
|
|
1470
|
+
const valueCount = exports.length - typeOnlyCount;
|
|
1471
|
+
const namespaceExportCount = exports.filter((item) => item.isNamespace).length;
|
|
1472
|
+
const hasLocalRuntimeExports = exports.some(
|
|
1473
|
+
(item) => item.source === "local" && !item.isTypeOnly
|
|
1474
|
+
);
|
|
1475
|
+
const isPrimarilyBarrel = exports.length > 0 && localCount < reExportCount;
|
|
1270
1476
|
return {
|
|
1271
1477
|
content: [
|
|
1272
1478
|
{
|
|
1273
1479
|
type: "text",
|
|
1274
|
-
text: JSON.stringify({
|
|
1480
|
+
text: JSON.stringify({
|
|
1481
|
+
file,
|
|
1482
|
+
exports,
|
|
1483
|
+
count: exports.length,
|
|
1484
|
+
localCount,
|
|
1485
|
+
reExportCount,
|
|
1486
|
+
typeOnlyCount,
|
|
1487
|
+
valueCount,
|
|
1488
|
+
namespaceExportCount,
|
|
1489
|
+
hasLocalRuntimeExports,
|
|
1490
|
+
isPrimarilyBarrel
|
|
1491
|
+
})
|
|
1275
1492
|
}
|
|
1276
1493
|
]
|
|
1277
1494
|
};
|
|
1278
1495
|
}
|
|
1279
1496
|
);
|
|
1280
1497
|
function relPath(absPath2) {
|
|
1281
|
-
return path5.relative(
|
|
1498
|
+
return path5.relative(normalizedProjectRoot, normalizeExistingPath(absPath2));
|
|
1282
1499
|
}
|
|
1283
1500
|
function absPath(file) {
|
|
1284
1501
|
return path5.isAbsolute(file) ? file : path5.resolve(projectRoot, file);
|
|
@@ -1459,6 +1676,7 @@ async function main() {
|
|
|
1459
1676
|
buildGraph(projectRoot, tsconfigPath)
|
|
1460
1677
|
]);
|
|
1461
1678
|
moduleGraph = graphResult.graph;
|
|
1679
|
+
moduleResolver = graphResult.resolver;
|
|
1462
1680
|
startWatcher(projectRoot, moduleGraph, graphResult.resolver);
|
|
1463
1681
|
const transport = new StdioServerTransport();
|
|
1464
1682
|
await mcpServer.connect(transport);
|
package/dist/smoke-test.js
CHANGED
|
@@ -386,7 +386,7 @@ function distToSource(resolvedPath, projectRoot) {
|
|
|
386
386
|
}
|
|
387
387
|
return resolvedPath;
|
|
388
388
|
}
|
|
389
|
-
function
|
|
389
|
+
function resolveProjectImport(resolver, fromDir, specifier, projectRoot) {
|
|
390
390
|
try {
|
|
391
391
|
const result = resolver.sync(fromDir, specifier);
|
|
392
392
|
if (result.path && !result.path.includes("node_modules")) {
|
|
@@ -437,7 +437,7 @@ function buildForwardEdges(files, resolver, projectRoot) {
|
|
|
437
437
|
const edges = [];
|
|
438
438
|
const fromDir = path2.dirname(filePath);
|
|
439
439
|
for (const raw of rawImports) {
|
|
440
|
-
const target =
|
|
440
|
+
const target = resolveProjectImport(resolver, fromDir, raw.specifier, projectRoot);
|
|
441
441
|
if (target) {
|
|
442
442
|
edges.push({
|
|
443
443
|
target,
|
|
@@ -0,0 +1,202 @@
|
|
|
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 ModuleExportRecord = {
|
|
12
|
+
symbol: string;
|
|
13
|
+
kind: string;
|
|
14
|
+
line: number;
|
|
15
|
+
type: string | null;
|
|
16
|
+
exportKind: "value" | "type";
|
|
17
|
+
isTypeOnly: boolean;
|
|
18
|
+
isNamespace: boolean;
|
|
19
|
+
source: "local" | "re-export" | "star-re-export";
|
|
20
|
+
from: string | null;
|
|
21
|
+
definedIn: string;
|
|
22
|
+
definedLine: number | null;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type ModuleExportsResult = {
|
|
26
|
+
file: string;
|
|
27
|
+
exports: ModuleExportRecord[];
|
|
28
|
+
count: number;
|
|
29
|
+
localCount: number;
|
|
30
|
+
reExportCount: number;
|
|
31
|
+
typeOnlyCount: number;
|
|
32
|
+
valueCount: number;
|
|
33
|
+
namespaceExportCount: number;
|
|
34
|
+
hasLocalRuntimeExports: boolean;
|
|
35
|
+
isPrimarilyBarrel: boolean;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function copyDir(src: string, dest: string): void {
|
|
39
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
40
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
41
|
+
const srcPath = path.join(src, entry.name);
|
|
42
|
+
const destPath = path.join(dest, entry.name);
|
|
43
|
+
if (entry.isDirectory()) {
|
|
44
|
+
copyDir(srcPath, destPath);
|
|
45
|
+
} else {
|
|
46
|
+
fs.copyFileSync(srcPath, destPath);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function findExport(
|
|
52
|
+
result: ModuleExportsResult,
|
|
53
|
+
symbol: string,
|
|
54
|
+
exportKind: "value" | "type"
|
|
55
|
+
): ModuleExportRecord {
|
|
56
|
+
const found = result.exports.find(
|
|
57
|
+
(item) => item.symbol === symbol && item.exportKind === exportKind
|
|
58
|
+
);
|
|
59
|
+
assert.ok(found, `Expected ${symbol}:${exportKind} in ${result.file}`);
|
|
60
|
+
return found;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function assertProjectPath(actual: string | null, expected: string): void {
|
|
64
|
+
assert.ok(actual, `Expected path ${expected}`);
|
|
65
|
+
const normalized = actual.replaceAll("\\", "/");
|
|
66
|
+
assert.equal(normalized, expected, `Expected ${normalized} to equal ${expected}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function main(): Promise<void> {
|
|
70
|
+
const repoRoot = import.meta.dirname;
|
|
71
|
+
const fixtureRoot = path.join(repoRoot, ".fixtures/export-surface");
|
|
72
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "typegraph-export-surface-"));
|
|
73
|
+
const projectRoot = path.join(tempRoot, "project");
|
|
74
|
+
|
|
75
|
+
copyDir(fixtureRoot, projectRoot);
|
|
76
|
+
fs.mkdirSync(path.join(projectRoot, "node_modules"), { recursive: true });
|
|
77
|
+
fs.symlinkSync(
|
|
78
|
+
path.join(repoRoot, "node_modules/typescript"),
|
|
79
|
+
path.join(projectRoot, "node_modules/typescript"),
|
|
80
|
+
"dir"
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const client = new Client({ name: "export-surface-test", version: "1.0.0" });
|
|
84
|
+
const transport = new StdioClientTransport({
|
|
85
|
+
command: path.join(repoRoot, "node_modules/.bin/tsx"),
|
|
86
|
+
args: [path.join(repoRoot, "server.ts")],
|
|
87
|
+
cwd: projectRoot,
|
|
88
|
+
env: {
|
|
89
|
+
TYPEGRAPH_PROJECT_ROOT: projectRoot,
|
|
90
|
+
TYPEGRAPH_TSCONFIG: path.join(projectRoot, "tsconfig.json"),
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
await client.connect(transport);
|
|
96
|
+
|
|
97
|
+
async function moduleExports(file: string): Promise<ModuleExportsResult> {
|
|
98
|
+
const result = await client.request(
|
|
99
|
+
{
|
|
100
|
+
method: "tools/call",
|
|
101
|
+
params: {
|
|
102
|
+
name: "ts_module_exports",
|
|
103
|
+
arguments: { file },
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
CallToolResultSchema
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const content = result.content[0];
|
|
110
|
+
assert.ok(content?.type === "text", `Expected text response for ${file}`);
|
|
111
|
+
return JSON.parse(content.text) as ModuleExportsResult;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const source = await moduleExports("src/source.ts");
|
|
115
|
+
const defaultExport = findExport(source, "default", "value");
|
|
116
|
+
assert.equal(defaultExport.source, "local");
|
|
117
|
+
assert.equal(defaultExport.kind, "function");
|
|
118
|
+
assert.equal(defaultExport.definedIn, "src/source.ts");
|
|
119
|
+
assert.equal(defaultExport.definedLine, 1);
|
|
120
|
+
|
|
121
|
+
const defaultExpression = await moduleExports("src/default-expression.ts");
|
|
122
|
+
const expressionDefault = findExport(defaultExpression, "default", "value");
|
|
123
|
+
assert.equal(expressionDefault.source, "local");
|
|
124
|
+
assert.equal(expressionDefault.definedIn, "src/default-expression.ts");
|
|
125
|
+
|
|
126
|
+
const barrel = await moduleExports("src/barrel.ts");
|
|
127
|
+
const barrelValue = findExport(barrel, "value", "value");
|
|
128
|
+
const barrelUserShape = findExport(barrel, "UserShape", "type");
|
|
129
|
+
assert.equal(barrelValue.source, "star-re-export");
|
|
130
|
+
assert.equal(barrelUserShape.source, "star-re-export");
|
|
131
|
+
assert.equal(barrel.exports.some((item) => item.symbol === "default"), false);
|
|
132
|
+
assert.equal(barrel.exports.some((item) => item.symbol === "buildUser"), false);
|
|
133
|
+
assert.equal(barrelUserShape.isTypeOnly, true);
|
|
134
|
+
assert.equal(barrel.typeOnlyCount, 2);
|
|
135
|
+
assert.equal(barrel.isPrimarilyBarrel, true);
|
|
136
|
+
assert.equal(barrel.hasLocalRuntimeExports, false);
|
|
137
|
+
|
|
138
|
+
const typeReExport = await moduleExports("src/reexport-type.ts");
|
|
139
|
+
const userShape = findExport(typeReExport, "UserShape", "type");
|
|
140
|
+
assert.equal(userShape.source, "re-export");
|
|
141
|
+
assert.equal(userShape.isTypeOnly, true);
|
|
142
|
+
assert.equal(userShape.exportKind, "type");
|
|
143
|
+
assert.equal(typeReExport.typeOnlyCount, 1);
|
|
144
|
+
assert.equal(typeReExport.valueCount, 0);
|
|
145
|
+
|
|
146
|
+
const namedReExport = await moduleExports("src/named-reexport.ts");
|
|
147
|
+
const aliasedValue = findExport(namedReExport, "aliasedValue", "value");
|
|
148
|
+
assert.equal(aliasedValue.source, "re-export");
|
|
149
|
+
assertProjectPath(aliasedValue.from, "src/source.ts");
|
|
150
|
+
assert.equal(aliasedValue.definedIn, "src/source.ts");
|
|
151
|
+
assert.equal(namedReExport.namespaceExportCount, 0);
|
|
152
|
+
|
|
153
|
+
const namespaceReExport = await moduleExports("src/namespace-reexport.ts");
|
|
154
|
+
const models = findExport(namespaceReExport, "Models", "value");
|
|
155
|
+
assert.equal(models.source, "re-export");
|
|
156
|
+
assert.equal(models.isNamespace, true);
|
|
157
|
+
assert.equal(namespaceReExport.namespaceExportCount, 1);
|
|
158
|
+
|
|
159
|
+
const mixed = await moduleExports("src/mixed.ts");
|
|
160
|
+
const localValue = findExport(mixed, "SessionId", "value");
|
|
161
|
+
const localType = findExport(mixed, "SessionId", "type");
|
|
162
|
+
const externalValue = findExport(mixed, "externalValue", "value");
|
|
163
|
+
const externalUserShape = findExport(mixed, "ExternalUserShape", "type");
|
|
164
|
+
assert.equal(localValue.source, "local");
|
|
165
|
+
assert.equal(localType.source, "local");
|
|
166
|
+
assert.equal(localType.kind, "type");
|
|
167
|
+
assert.equal(localType.definedLine, 2);
|
|
168
|
+
assert.equal(externalValue.source, "re-export");
|
|
169
|
+
assert.equal(externalUserShape.source, "re-export");
|
|
170
|
+
assert.equal(externalUserShape.isTypeOnly, true);
|
|
171
|
+
assert.equal(mixed.localCount, 2);
|
|
172
|
+
assert.equal(mixed.reExportCount, 2);
|
|
173
|
+
assert.equal(mixed.typeOnlyCount, 2);
|
|
174
|
+
assert.equal(mixed.hasLocalRuntimeExports, true);
|
|
175
|
+
assert.equal(mixed.isPrimarilyBarrel, false);
|
|
176
|
+
|
|
177
|
+
const collision = await moduleExports("src/collision-barrel.ts");
|
|
178
|
+
assert.equal(collision.exports.some((item) => item.symbol === "dup"), false);
|
|
179
|
+
assert.equal(collision.count, 0);
|
|
180
|
+
|
|
181
|
+
console.log("");
|
|
182
|
+
console.log("typegraph-mcp Export Surface Test");
|
|
183
|
+
console.log("=================================");
|
|
184
|
+
console.log(" ✓ local default exports are reported as default");
|
|
185
|
+
console.log(" ✓ anonymous default exports stay visible");
|
|
186
|
+
console.log(" ✓ barrel star re-exports");
|
|
187
|
+
console.log(" ✓ barrel star re-exports exclude default exports");
|
|
188
|
+
console.log(" ✓ type-only named re-exports");
|
|
189
|
+
console.log(" ✓ named alias re-exports");
|
|
190
|
+
console.log(" ✓ namespace re-exports");
|
|
191
|
+
console.log(" ✓ mixed local + re-export modules");
|
|
192
|
+
console.log(" ✓ conflicting star re-exports stay hidden");
|
|
193
|
+
} finally {
|
|
194
|
+
await transport.close().catch(() => {});
|
|
195
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
main().catch((err) => {
|
|
200
|
+
console.error(err);
|
|
201
|
+
process.exit(1);
|
|
202
|
+
});
|