@shvmgyl15/tsgraph 0.1.0
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/AGENTS.md +64 -0
- package/README.md +128 -0
- package/TODOS.md +61 -0
- package/dist/analysis/analysis.test.d.ts +2 -0
- package/dist/analysis/analysis.test.d.ts.map +1 -0
- package/dist/analysis/analysis.test.js +359 -0
- package/dist/analysis/analysis.test.js.map +1 -0
- package/dist/analysis/complexity.d.ts +8 -0
- package/dist/analysis/complexity.d.ts.map +1 -0
- package/dist/analysis/complexity.js +88 -0
- package/dist/analysis/complexity.js.map +1 -0
- package/dist/analysis/coupling.d.ts +17 -0
- package/dist/analysis/coupling.d.ts.map +1 -0
- package/dist/analysis/coupling.js +71 -0
- package/dist/analysis/coupling.js.map +1 -0
- package/dist/analysis/hotspot.d.ts +10 -0
- package/dist/analysis/hotspot.d.ts.map +1 -0
- package/dist/analysis/hotspot.js +33 -0
- package/dist/analysis/hotspot.js.map +1 -0
- package/dist/analysis/index.d.ts +9 -0
- package/dist/analysis/index.d.ts.map +1 -0
- package/dist/analysis/index.js +5 -0
- package/dist/analysis/index.js.map +1 -0
- package/dist/boundaries/index.d.ts +25 -0
- package/dist/boundaries/index.d.ts.map +1 -0
- package/dist/boundaries/index.js +103 -0
- package/dist/boundaries/index.js.map +1 -0
- package/dist/boundaries/index.test.d.ts +2 -0
- package/dist/boundaries/index.test.d.ts.map +1 -0
- package/dist/boundaries/index.test.js +293 -0
- package/dist/boundaries/index.test.js.map +1 -0
- package/dist/changes/index.d.ts +28 -0
- package/dist/changes/index.d.ts.map +1 -0
- package/dist/changes/index.js +48 -0
- package/dist/changes/index.js.map +1 -0
- package/dist/changes/index.test.d.ts +2 -0
- package/dist/changes/index.test.d.ts.map +1 -0
- package/dist/changes/index.test.js +104 -0
- package/dist/changes/index.test.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +659 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/git/index.d.ts +16 -0
- package/dist/git/index.d.ts.map +1 -0
- package/dist/git/index.js +73 -0
- package/dist/git/index.js.map +1 -0
- package/dist/git/index.test.d.ts +2 -0
- package/dist/git/index.test.d.ts.map +1 -0
- package/dist/git/index.test.js +78 -0
- package/dist/git/index.test.js.map +1 -0
- package/dist/graph/types.d.ts +156 -0
- package/dist/graph/types.d.ts.map +1 -0
- package/dist/graph/types.js +166 -0
- package/dist/graph/types.js.map +1 -0
- package/dist/graph/types.test.d.ts +2 -0
- package/dist/graph/types.test.d.ts.map +1 -0
- package/dist/graph/types.test.js +326 -0
- package/dist/graph/types.test.js.map +1 -0
- package/dist/mcp/mcp.test.d.ts +2 -0
- package/dist/mcp/mcp.test.d.ts.map +1 -0
- package/dist/mcp/mcp.test.js +151 -0
- package/dist/mcp/mcp.test.js.map +1 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +209 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/nextjs/index.d.ts +8 -0
- package/dist/nextjs/index.d.ts.map +1 -0
- package/dist/nextjs/index.js +16 -0
- package/dist/nextjs/index.js.map +1 -0
- package/dist/nextjs/nextjs.test.d.ts +2 -0
- package/dist/nextjs/nextjs.test.d.ts.map +1 -0
- package/dist/nextjs/nextjs.test.js +190 -0
- package/dist/nextjs/nextjs.test.js.map +1 -0
- package/dist/nextjs/pages.d.ts +4 -0
- package/dist/nextjs/pages.d.ts.map +1 -0
- package/dist/nextjs/pages.js +36 -0
- package/dist/nextjs/pages.js.map +1 -0
- package/dist/nextjs/react.d.ts +3 -0
- package/dist/nextjs/react.d.ts.map +1 -0
- package/dist/nextjs/react.js +86 -0
- package/dist/nextjs/react.js.map +1 -0
- package/dist/nextjs/router.d.ts +4 -0
- package/dist/nextjs/router.d.ts.map +1 -0
- package/dist/nextjs/router.js +86 -0
- package/dist/nextjs/router.js.map +1 -0
- package/dist/nextjs/routes.d.ts +4 -0
- package/dist/nextjs/routes.d.ts.map +1 -0
- package/dist/nextjs/routes.js +58 -0
- package/dist/nextjs/routes.js.map +1 -0
- package/dist/opencode/index.d.ts +7 -0
- package/dist/opencode/index.d.ts.map +1 -0
- package/dist/opencode/index.js +71 -0
- package/dist/opencode/index.js.map +1 -0
- package/dist/opencode/index.test.d.ts +2 -0
- package/dist/opencode/index.test.d.ts.map +1 -0
- package/dist/opencode/index.test.js +71 -0
- package/dist/opencode/index.test.js.map +1 -0
- package/dist/parser/index.d.ts +4 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/index.js +282 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/parser/parser.test.d.ts +2 -0
- package/dist/parser/parser.test.d.ts.map +1 -0
- package/dist/parser/parser.test.js +225 -0
- package/dist/parser/parser.test.js.map +1 -0
- package/dist/plan/index.d.ts +32 -0
- package/dist/plan/index.d.ts.map +1 -0
- package/dist/plan/index.js +107 -0
- package/dist/plan/index.js.map +1 -0
- package/dist/plan/index.test.d.ts +2 -0
- package/dist/plan/index.test.d.ts.map +1 -0
- package/dist/plan/index.test.js +143 -0
- package/dist/plan/index.test.js.map +1 -0
- package/dist/report/index.d.ts +9 -0
- package/dist/report/index.d.ts.map +1 -0
- package/dist/report/index.js +108 -0
- package/dist/report/index.js.map +1 -0
- package/dist/scanner/index.d.ts +13 -0
- package/dist/scanner/index.d.ts.map +1 -0
- package/dist/scanner/index.js +78 -0
- package/dist/scanner/index.js.map +1 -0
- package/dist/scanner/scanner.test.d.ts +2 -0
- package/dist/scanner/scanner.test.d.ts.map +1 -0
- package/dist/scanner/scanner.test.js +113 -0
- package/dist/scanner/scanner.test.js.map +1 -0
- package/dist/search/index.d.ts +32 -0
- package/dist/search/index.d.ts.map +1 -0
- package/dist/search/index.js +97 -0
- package/dist/search/index.js.map +1 -0
- package/dist/search/search.test.d.ts +2 -0
- package/dist/search/search.test.d.ts.map +1 -0
- package/dist/search/search.test.js +446 -0
- package/dist/search/search.test.js.map +1 -0
- package/dist/traversal/index.d.ts +5 -0
- package/dist/traversal/index.d.ts.map +1 -0
- package/dist/traversal/index.js +3 -0
- package/dist/traversal/index.js.map +1 -0
- package/dist/traversal/traversal.d.ts +31 -0
- package/dist/traversal/traversal.d.ts.map +1 -0
- package/dist/traversal/traversal.js +130 -0
- package/dist/traversal/traversal.js.map +1 -0
- package/dist/traversal/traversal.test.d.ts +2 -0
- package/dist/traversal/traversal.test.d.ts.map +1 -0
- package/dist/traversal/traversal.test.js +224 -0
- package/dist/traversal/traversal.test.js.map +1 -0
- package/opencode.json +24 -0
- package/package.json +29 -0
- package/src/analysis/analysis.test.ts +405 -0
- package/src/analysis/complexity.ts +107 -0
- package/src/analysis/coupling.ts +106 -0
- package/src/analysis/hotspot.ts +52 -0
- package/src/analysis/index.ts +17 -0
- package/src/boundaries/index.test.ts +335 -0
- package/src/boundaries/index.ts +137 -0
- package/src/changes/index.test.ts +114 -0
- package/src/changes/index.ts +95 -0
- package/src/cli/index.ts +736 -0
- package/src/git/index.test.ts +92 -0
- package/src/git/index.ts +86 -0
- package/src/graph/types.test.ts +383 -0
- package/src/graph/types.ts +353 -0
- package/src/mcp/mcp.test.ts +176 -0
- package/src/mcp/server.ts +217 -0
- package/src/nextjs/index.ts +23 -0
- package/src/nextjs/nextjs.test.ts +233 -0
- package/src/nextjs/pages.ts +43 -0
- package/src/nextjs/react.ts +100 -0
- package/src/nextjs/router.ts +102 -0
- package/src/nextjs/routes.ts +69 -0
- package/src/opencode/index.test.ts +90 -0
- package/src/opencode/index.ts +83 -0
- package/src/parser/index.ts +339 -0
- package/src/parser/parser.test.ts +282 -0
- package/src/plan/index.test.ts +162 -0
- package/src/plan/index.ts +161 -0
- package/src/report/index.ts +128 -0
- package/src/scanner/index.ts +97 -0
- package/src/scanner/scanner.test.ts +135 -0
- package/src/search/index.ts +163 -0
- package/src/search/search.test.ts +512 -0
- package/src/traversal/index.ts +5 -0
- package/src/traversal/traversal.test.ts +266 -0
- package/src/traversal/traversal.ts +185 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
export const GRAPH_VERSION = "1";
|
|
2
|
+
|
|
3
|
+
export type SymbolKind =
|
|
4
|
+
| "function"
|
|
5
|
+
| "method"
|
|
6
|
+
| "class"
|
|
7
|
+
| "interface"
|
|
8
|
+
| "type_alias"
|
|
9
|
+
| "enum"
|
|
10
|
+
| "var"
|
|
11
|
+
| "const";
|
|
12
|
+
|
|
13
|
+
export type ConcurrencyKind =
|
|
14
|
+
| "promise_all"
|
|
15
|
+
| "promise_settled"
|
|
16
|
+
| "set_timeout"
|
|
17
|
+
| "set_interval"
|
|
18
|
+
| "abort_signal"
|
|
19
|
+
| "async_function";
|
|
20
|
+
|
|
21
|
+
export interface StructField {
|
|
22
|
+
name: string;
|
|
23
|
+
type: string;
|
|
24
|
+
tag?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SymbolNode {
|
|
28
|
+
id: string;
|
|
29
|
+
kind: SymbolKind;
|
|
30
|
+
name: string;
|
|
31
|
+
receiver?: string;
|
|
32
|
+
packageName: string;
|
|
33
|
+
file: string;
|
|
34
|
+
line: number;
|
|
35
|
+
endLine: number;
|
|
36
|
+
doc?: string;
|
|
37
|
+
signature?: string;
|
|
38
|
+
methodSignature?: string;
|
|
39
|
+
interfaceMethods?: Record<string, string>;
|
|
40
|
+
structFields?: StructField[];
|
|
41
|
+
embeddedTypes?: string[];
|
|
42
|
+
arity?: number;
|
|
43
|
+
isExported: boolean;
|
|
44
|
+
isClientComponent?: boolean;
|
|
45
|
+
isServerComponent?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface PackageNode {
|
|
49
|
+
id: string;
|
|
50
|
+
name: string;
|
|
51
|
+
importPathBestEffort: string;
|
|
52
|
+
dir: string;
|
|
53
|
+
files: string[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface FileNode {
|
|
57
|
+
id: string;
|
|
58
|
+
path: string;
|
|
59
|
+
packageName: string;
|
|
60
|
+
lines: number;
|
|
61
|
+
generated: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface CallEdge {
|
|
65
|
+
callerSymbolId: string;
|
|
66
|
+
callerName: string;
|
|
67
|
+
calleeRaw: string;
|
|
68
|
+
file: string;
|
|
69
|
+
line: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface ImportEdge {
|
|
73
|
+
fromFile: string;
|
|
74
|
+
fromPackage: string;
|
|
75
|
+
importPath: string;
|
|
76
|
+
alias?: string;
|
|
77
|
+
isDefault: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface Dependency {
|
|
81
|
+
module: string;
|
|
82
|
+
version: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface HTTPRoute {
|
|
86
|
+
method: string;
|
|
87
|
+
path: string;
|
|
88
|
+
handler: string;
|
|
89
|
+
file: string;
|
|
90
|
+
line: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface EnvRead {
|
|
94
|
+
key: string;
|
|
95
|
+
accessor: string;
|
|
96
|
+
file: string;
|
|
97
|
+
line: number;
|
|
98
|
+
functionName?: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface ConcurrencyNode {
|
|
102
|
+
kind: ConcurrencyKind;
|
|
103
|
+
functionName: string;
|
|
104
|
+
file: string;
|
|
105
|
+
line: number;
|
|
106
|
+
detail?: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface TestEdge {
|
|
110
|
+
testFunc: string;
|
|
111
|
+
target: string;
|
|
112
|
+
file: string;
|
|
113
|
+
line: number;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface ImplementsEdge {
|
|
117
|
+
interface: string;
|
|
118
|
+
concrete: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface MutationEdge {
|
|
122
|
+
field: string;
|
|
123
|
+
functionName: string;
|
|
124
|
+
file: string;
|
|
125
|
+
line: number;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface ErrorEdge {
|
|
129
|
+
message: string;
|
|
130
|
+
functionName: string;
|
|
131
|
+
file: string;
|
|
132
|
+
line: number;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface AppRouterNode {
|
|
136
|
+
path: string;
|
|
137
|
+
dir: string;
|
|
138
|
+
files: {
|
|
139
|
+
page?: string;
|
|
140
|
+
layout?: string;
|
|
141
|
+
loading?: string;
|
|
142
|
+
error?: string;
|
|
143
|
+
notFound?: string;
|
|
144
|
+
route?: string;
|
|
145
|
+
template?: string;
|
|
146
|
+
default?: string;
|
|
147
|
+
};
|
|
148
|
+
children: AppRouterNode[];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface Graph {
|
|
152
|
+
version: string;
|
|
153
|
+
generatedAt: string;
|
|
154
|
+
root: string;
|
|
155
|
+
packages: PackageNode[];
|
|
156
|
+
files: FileNode[];
|
|
157
|
+
symbols: SymbolNode[];
|
|
158
|
+
imports: ImportEdge[];
|
|
159
|
+
calls: CallEdge[];
|
|
160
|
+
envReads: EnvRead[];
|
|
161
|
+
dependencies: Dependency[];
|
|
162
|
+
routes: HTTPRoute[];
|
|
163
|
+
concurrency: ConcurrencyNode[];
|
|
164
|
+
testEdges: TestEdge[];
|
|
165
|
+
implements: ImplementsEdge[];
|
|
166
|
+
mutations: MutationEdge[];
|
|
167
|
+
errors: ErrorEdge[];
|
|
168
|
+
appRouter?: AppRouterNode;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let _nextId = 0;
|
|
172
|
+
function nextId(): string {
|
|
173
|
+
return `gen_${++_nextId}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function makeStructField(overrides?: Partial<StructField>): StructField {
|
|
177
|
+
return { name: "", type: "", ...overrides };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function makeSymbolNode(overrides?: Partial<SymbolNode>): SymbolNode {
|
|
181
|
+
return {
|
|
182
|
+
id: nextId(),
|
|
183
|
+
kind: "function",
|
|
184
|
+
name: "",
|
|
185
|
+
packageName: "",
|
|
186
|
+
file: "",
|
|
187
|
+
line: 0,
|
|
188
|
+
endLine: 0,
|
|
189
|
+
isExported: false,
|
|
190
|
+
...overrides,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function makePackageNode(overrides?: Partial<PackageNode>): PackageNode {
|
|
195
|
+
return {
|
|
196
|
+
id: nextId(),
|
|
197
|
+
name: "",
|
|
198
|
+
importPathBestEffort: "",
|
|
199
|
+
dir: "",
|
|
200
|
+
files: [],
|
|
201
|
+
...overrides,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function makeFileNode(overrides?: Partial<FileNode>): FileNode {
|
|
206
|
+
return {
|
|
207
|
+
id: nextId(),
|
|
208
|
+
path: "",
|
|
209
|
+
packageName: "",
|
|
210
|
+
lines: 0,
|
|
211
|
+
generated: false,
|
|
212
|
+
...overrides,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function makeCallEdge(overrides?: Partial<CallEdge>): CallEdge {
|
|
217
|
+
return {
|
|
218
|
+
callerSymbolId: "",
|
|
219
|
+
callerName: "",
|
|
220
|
+
calleeRaw: "",
|
|
221
|
+
file: "",
|
|
222
|
+
line: 0,
|
|
223
|
+
...overrides,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function makeImportEdge(overrides?: Partial<ImportEdge>): ImportEdge {
|
|
228
|
+
return {
|
|
229
|
+
fromFile: "",
|
|
230
|
+
fromPackage: "",
|
|
231
|
+
importPath: "",
|
|
232
|
+
isDefault: false,
|
|
233
|
+
...overrides,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function makeDependency(overrides?: Partial<Dependency>): Dependency {
|
|
238
|
+
return { module: "", version: "", ...overrides };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function makeHTTPRoute(overrides?: Partial<HTTPRoute>): HTTPRoute {
|
|
242
|
+
return { method: "", path: "", handler: "", file: "", line: 0, ...overrides };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function makeEnvRead(overrides?: Partial<EnvRead>): EnvRead {
|
|
246
|
+
return { key: "", accessor: "", file: "", line: 0, ...overrides };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function makeConcurrencyNode(overrides?: Partial<ConcurrencyNode>): ConcurrencyNode {
|
|
250
|
+
return {
|
|
251
|
+
kind: "async_function",
|
|
252
|
+
functionName: "",
|
|
253
|
+
file: "",
|
|
254
|
+
line: 0,
|
|
255
|
+
...overrides,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function makeTestEdge(overrides?: Partial<TestEdge>): TestEdge {
|
|
260
|
+
return { testFunc: "", target: "", file: "", line: 0, ...overrides };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function makeImplementsEdge(overrides?: Partial<ImplementsEdge>): ImplementsEdge {
|
|
264
|
+
return { interface: "", concrete: "", ...overrides };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function makeMutationEdge(overrides?: Partial<MutationEdge>): MutationEdge {
|
|
268
|
+
return { field: "", functionName: "", file: "", line: 0, ...overrides };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function makeErrorEdge(overrides?: Partial<ErrorEdge>): ErrorEdge {
|
|
272
|
+
return { message: "", functionName: "", file: "", line: 0, ...overrides };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function makeAppRouterNode(overrides?: Partial<AppRouterNode>): AppRouterNode {
|
|
276
|
+
return {
|
|
277
|
+
path: "",
|
|
278
|
+
dir: "",
|
|
279
|
+
files: {},
|
|
280
|
+
children: [],
|
|
281
|
+
...overrides,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function makeGraph(overrides?: Partial<Graph>): Graph {
|
|
286
|
+
return {
|
|
287
|
+
version: GRAPH_VERSION,
|
|
288
|
+
generatedAt: new Date().toISOString(),
|
|
289
|
+
root: "",
|
|
290
|
+
packages: [],
|
|
291
|
+
files: [],
|
|
292
|
+
symbols: [],
|
|
293
|
+
imports: [],
|
|
294
|
+
calls: [],
|
|
295
|
+
envReads: [],
|
|
296
|
+
dependencies: [],
|
|
297
|
+
routes: [],
|
|
298
|
+
concurrency: [],
|
|
299
|
+
testEdges: [],
|
|
300
|
+
implements: [],
|
|
301
|
+
mutations: [],
|
|
302
|
+
errors: [],
|
|
303
|
+
...overrides,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const ARRAY_KEYS: (keyof Graph)[] = [
|
|
308
|
+
"packages", "files", "symbols", "imports", "calls",
|
|
309
|
+
"envReads", "dependencies", "routes", "concurrency",
|
|
310
|
+
"testEdges", "implements", "mutations", "errors",
|
|
311
|
+
];
|
|
312
|
+
|
|
313
|
+
function isGraph(value: unknown): value is Graph {
|
|
314
|
+
if (value === null || value === undefined) return false;
|
|
315
|
+
if (typeof value !== "object") return false;
|
|
316
|
+
const obj = value as Record<string, unknown>;
|
|
317
|
+
if (typeof obj.version !== "string") return false;
|
|
318
|
+
if (typeof obj.generatedAt !== "string") return false;
|
|
319
|
+
if (typeof obj.root !== "string") return false;
|
|
320
|
+
for (const key of ARRAY_KEYS) {
|
|
321
|
+
if (!Array.isArray(obj[key])) return false;
|
|
322
|
+
}
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function serialize(graph: Graph): string {
|
|
327
|
+
if (graph.version !== GRAPH_VERSION) {
|
|
328
|
+
throw new Error(
|
|
329
|
+
`Graph version mismatch: expected ${GRAPH_VERSION}, got ${graph.version}`,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
return JSON.stringify(graph, null, 2);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function deserialize(json: string): Graph {
|
|
336
|
+
let parsed: unknown;
|
|
337
|
+
try {
|
|
338
|
+
parsed = JSON.parse(json);
|
|
339
|
+
} catch (e) {
|
|
340
|
+
throw new Error(`Invalid JSON: ${(e as Error).message}`);
|
|
341
|
+
}
|
|
342
|
+
if (!isGraph(parsed)) {
|
|
343
|
+
throw new Error(
|
|
344
|
+
"Invalid graph structure: missing or invalid required fields",
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
if (parsed.version !== GRAPH_VERSION) {
|
|
348
|
+
throw new Error(
|
|
349
|
+
`Graph version mismatch: expected ${GRAPH_VERSION}, got ${parsed.version}`,
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
return parsed;
|
|
353
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const projectRoot = path.resolve(__dirname, "../..");
|
|
10
|
+
const cliEntry = path.join(projectRoot, "src/cli/index.ts");
|
|
11
|
+
|
|
12
|
+
function createTempDir(): string {
|
|
13
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "tsgraph-test-"));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function writeFile(dir: string, relativePath: string, content: string) {
|
|
17
|
+
const fullPath = path.join(dir, relativePath);
|
|
18
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
19
|
+
fs.writeFileSync(fullPath, content, "utf-8");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let server: ChildProcess | null = null;
|
|
23
|
+
let requestId = 0;
|
|
24
|
+
let stderrBuf = "";
|
|
25
|
+
|
|
26
|
+
function sendRequest(proc: ChildProcess, method: string, params?: unknown): Promise<unknown> {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const id = ++requestId;
|
|
29
|
+
const msg = JSON.stringify({ jsonrpc: "2.0", id, method, params: params ?? {} }) + "\n";
|
|
30
|
+
|
|
31
|
+
const responses: string[] = [];
|
|
32
|
+
|
|
33
|
+
const onData = (chunk: Buffer) => {
|
|
34
|
+
const text = chunk.toString("utf-8");
|
|
35
|
+
responses.push(text);
|
|
36
|
+
const full = responses.join("");
|
|
37
|
+
const lines = full.split("\n").filter(Boolean);
|
|
38
|
+
// Keep incomplete last chunk
|
|
39
|
+
if (!full.endsWith("\n")) {
|
|
40
|
+
responses.length = 0;
|
|
41
|
+
responses.push(lines.pop() ?? "");
|
|
42
|
+
}
|
|
43
|
+
for (const line of lines) {
|
|
44
|
+
try {
|
|
45
|
+
const resp = JSON.parse(line);
|
|
46
|
+
if (resp.id === id) {
|
|
47
|
+
proc.stdout?.off("data", onData);
|
|
48
|
+
resolve(resp);
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
// partial or notification, keep waiting
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const onStderr = (chunk: Buffer) => {
|
|
57
|
+
stderrBuf += chunk.toString("utf-8");
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
proc.stdout?.on("data", onData);
|
|
61
|
+
proc.stderr?.on("data", onStderr);
|
|
62
|
+
proc.stdin?.write(msg);
|
|
63
|
+
|
|
64
|
+
setTimeout(() => {
|
|
65
|
+
proc.stdout?.off("data", onData);
|
|
66
|
+
reject(new Error(`Timeout. stderr: ${stderrBuf.slice(-500)}`));
|
|
67
|
+
}, 10000);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function mcpHandshake(proc: ChildProcess): Promise<void> {
|
|
72
|
+
const initResp = await sendRequest(proc, "initialize", {
|
|
73
|
+
protocolVersion: "2024-11-05",
|
|
74
|
+
capabilities: {},
|
|
75
|
+
clientInfo: { name: "tsgraph-test", version: "1.0.0" },
|
|
76
|
+
}) as Record<string, unknown>;
|
|
77
|
+
expect((initResp as { result?: Record<string, unknown> }).result?.protocolVersion).toBeTruthy();
|
|
78
|
+
// Send initialized notification
|
|
79
|
+
proc.stdin?.write(JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }) + "\n");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
describe("MCP server", () => {
|
|
83
|
+
afterEach(() => {
|
|
84
|
+
if (server && !server.killed) {
|
|
85
|
+
server.kill();
|
|
86
|
+
server = null;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("lists all registered tools", async () => {
|
|
91
|
+
const dir = createTempDir();
|
|
92
|
+
writeFile(dir, "package.json", JSON.stringify({ name: "test" }));
|
|
93
|
+
writeFile(dir, "index.ts", "export function foo() { return 1; }");
|
|
94
|
+
fs.mkdirSync(path.join(dir, ".tsgraph"), { recursive: true });
|
|
95
|
+
|
|
96
|
+
// Build graph first
|
|
97
|
+
const { scanFiles } = await import("../scanner/index.js");
|
|
98
|
+
const { parseProject } = await import("../parser/index.js");
|
|
99
|
+
const { serialize } = await import("../graph/types.js");
|
|
100
|
+
const scanned = scanFiles(dir);
|
|
101
|
+
const graph = parseProject(dir, scanned.files);
|
|
102
|
+
fs.writeFileSync(path.join(dir, ".tsgraph", "graph.json"), serialize(graph), "utf-8");
|
|
103
|
+
|
|
104
|
+
server = spawn("npx", ["tsx", cliEntry, "mcp"], {
|
|
105
|
+
cwd: dir,
|
|
106
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
110
|
+
|
|
111
|
+
await mcpHandshake(server);
|
|
112
|
+
|
|
113
|
+
const resp = await sendRequest(server, "tools/list") as Record<string, unknown>;
|
|
114
|
+
expect(resp).toBeTruthy();
|
|
115
|
+
expect((resp as { result?: { tools?: unknown[] } }).result?.tools).toBeTruthy();
|
|
116
|
+
const tools = (resp as { result: { tools: { name: string }[] } }).result.tools;
|
|
117
|
+
const toolNames = tools.map((t) => t.name).sort();
|
|
118
|
+
expect(toolNames).toContain("callers");
|
|
119
|
+
expect(toolNames).toContain("callees");
|
|
120
|
+
expect(toolNames).toContain("node");
|
|
121
|
+
expect(toolNames).toContain("query");
|
|
122
|
+
expect(toolNames).toContain("context");
|
|
123
|
+
expect(toolNames).toContain("imports");
|
|
124
|
+
expect(toolNames).toContain("public");
|
|
125
|
+
expect(toolNames).toContain("impact");
|
|
126
|
+
expect(toolNames).toContain("path");
|
|
127
|
+
expect(toolNames).toContain("orphans");
|
|
128
|
+
expect(toolNames).toContain("trace");
|
|
129
|
+
expect(toolNames).toContain("complexity");
|
|
130
|
+
expect(toolNames).toContain("hotspot");
|
|
131
|
+
expect(toolNames).toContain("coupling");
|
|
132
|
+
expect(toolNames).toHaveLength(14);
|
|
133
|
+
|
|
134
|
+
server.kill();
|
|
135
|
+
fs.rmSync(dir, { recursive: true });
|
|
136
|
+
}, 30000);
|
|
137
|
+
|
|
138
|
+
it("calls orphans tool and returns results", async () => {
|
|
139
|
+
const dir = createTempDir();
|
|
140
|
+
writeFile(dir, "package.json", JSON.stringify({ name: "test" }));
|
|
141
|
+
writeFile(dir, "index.ts", "export function foo() { return 1; }");
|
|
142
|
+
fs.mkdirSync(path.join(dir, ".tsgraph"), { recursive: true });
|
|
143
|
+
|
|
144
|
+
const { scanFiles } = await import("../scanner/index.js");
|
|
145
|
+
const { parseProject } = await import("../parser/index.js");
|
|
146
|
+
const { serialize } = await import("../graph/types.js");
|
|
147
|
+
const scanned = scanFiles(dir);
|
|
148
|
+
const graph = parseProject(dir, scanned.files);
|
|
149
|
+
fs.writeFileSync(path.join(dir, ".tsgraph", "graph.json"), serialize(graph), "utf-8");
|
|
150
|
+
|
|
151
|
+
server = spawn("npx", ["tsx", cliEntry, "mcp"], {
|
|
152
|
+
cwd: dir,
|
|
153
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
157
|
+
|
|
158
|
+
await mcpHandshake(server);
|
|
159
|
+
|
|
160
|
+
const resp = await sendRequest(server, "tools/call", {
|
|
161
|
+
name: "orphans",
|
|
162
|
+
arguments: {},
|
|
163
|
+
}) as Record<string, unknown>;
|
|
164
|
+
|
|
165
|
+
expect(resp).toBeTruthy();
|
|
166
|
+
const result = (resp as { result?: { content?: { text?: string }[] } }).result;
|
|
167
|
+
expect(result).toBeTruthy();
|
|
168
|
+
const textContent = result?.content?.[0]?.text;
|
|
169
|
+
expect(textContent).toBeTruthy();
|
|
170
|
+
const parsed = JSON.parse(textContent!);
|
|
171
|
+
expect(Array.isArray(parsed)).toBe(true);
|
|
172
|
+
|
|
173
|
+
server.kill();
|
|
174
|
+
fs.rmSync(dir, { recursive: true });
|
|
175
|
+
}, 30000);
|
|
176
|
+
});
|