@shvmgyl15/tsgraph 0.1.0 → 0.2.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.
Files changed (55) hide show
  1. package/dist/changes/index.test.js +2 -6
  2. package/dist/changes/index.test.js.map +1 -1
  3. package/dist/cli/index.js +184 -4
  4. package/dist/cli/index.js.map +1 -1
  5. package/dist/git/index.test.js +4 -6
  6. package/dist/git/index.test.js.map +1 -1
  7. package/dist/opencode/index.js +1 -1
  8. package/dist/opencode/index.js.map +1 -1
  9. package/dist/opencode/index.test.js +2 -2
  10. package/dist/opencode/index.test.js.map +1 -1
  11. package/dist/search/index.d.ts.map +1 -1
  12. package/dist/search/index.js +12 -4
  13. package/dist/search/index.js.map +1 -1
  14. package/package.json +16 -1
  15. package/AGENTS.md +0 -64
  16. package/TODOS.md +0 -61
  17. package/opencode.json +0 -24
  18. package/src/analysis/analysis.test.ts +0 -405
  19. package/src/analysis/complexity.ts +0 -107
  20. package/src/analysis/coupling.ts +0 -106
  21. package/src/analysis/hotspot.ts +0 -52
  22. package/src/analysis/index.ts +0 -17
  23. package/src/boundaries/index.test.ts +0 -335
  24. package/src/boundaries/index.ts +0 -137
  25. package/src/changes/index.test.ts +0 -114
  26. package/src/changes/index.ts +0 -95
  27. package/src/cli/index.ts +0 -736
  28. package/src/git/index.test.ts +0 -92
  29. package/src/git/index.ts +0 -86
  30. package/src/graph/types.test.ts +0 -383
  31. package/src/graph/types.ts +0 -353
  32. package/src/mcp/mcp.test.ts +0 -176
  33. package/src/mcp/server.ts +0 -217
  34. package/src/nextjs/index.ts +0 -23
  35. package/src/nextjs/nextjs.test.ts +0 -233
  36. package/src/nextjs/pages.ts +0 -43
  37. package/src/nextjs/react.ts +0 -100
  38. package/src/nextjs/router.ts +0 -102
  39. package/src/nextjs/routes.ts +0 -69
  40. package/src/opencode/index.test.ts +0 -90
  41. package/src/opencode/index.ts +0 -83
  42. package/src/parser/index.ts +0 -339
  43. package/src/parser/parser.test.ts +0 -282
  44. package/src/plan/index.test.ts +0 -162
  45. package/src/plan/index.ts +0 -161
  46. package/src/report/index.ts +0 -128
  47. package/src/scanner/index.ts +0 -97
  48. package/src/scanner/scanner.test.ts +0 -135
  49. package/src/search/index.ts +0 -163
  50. package/src/search/search.test.ts +0 -512
  51. package/src/traversal/index.ts +0 -5
  52. package/src/traversal/traversal.test.ts +0 -266
  53. package/src/traversal/traversal.ts +0 -185
  54. package/tsconfig.json +0 -20
  55. package/vitest.config.ts +0 -7
@@ -1,282 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import path from "node:path";
3
- import fs from "node:fs";
4
- import os from "node:os";
5
- import { scanFiles } from "../scanner/index.js";
6
- import { parseProject } from "./index.js";
7
-
8
- function createTempDir(): string {
9
- return fs.mkdtempSync(path.join(os.tmpdir(), "tsgraph-test-"));
10
- }
11
-
12
- function writeFile(dir: string, relativePath: string, content: string) {
13
- const fullPath = path.join(dir, relativePath);
14
- fs.mkdirSync(path.dirname(fullPath), { recursive: true });
15
- fs.writeFileSync(fullPath, content, "utf-8");
16
- }
17
-
18
- describe("parseProject", () => {
19
- it("extracts functions from a TypeScript file", () => {
20
- const dir = createTempDir();
21
- writeFile(dir, "index.ts", `
22
- export function greet(name: string): string {
23
- return "Hello " + name;
24
- }
25
-
26
- function helper() {
27
- return 42;
28
- }
29
- `);
30
-
31
- const { files } = scanFiles(dir);
32
- const graph = parseProject(dir, files);
33
-
34
- expect(graph.symbols).toHaveLength(2);
35
-
36
- const greet = graph.symbols.find((s) => s.name === "greet");
37
- expect(greet).toBeTruthy();
38
- expect(greet!.kind).toBe("function");
39
- expect(greet!.isExported).toBe(true);
40
- expect(greet!.file).toBe("index.ts");
41
-
42
- const helper = graph.symbols.find((s) => s.name === "helper");
43
- expect(helper).toBeTruthy();
44
- expect(helper!.kind).toBe("function");
45
- expect(helper!.isExported).toBe(false);
46
- fs.rmSync(dir, { recursive: true });
47
- });
48
-
49
- it("extracts classes with methods", () => {
50
- const dir = createTempDir();
51
- writeFile(dir, "user.ts", `
52
- export class User {
53
- name: string;
54
-
55
- constructor(name: string) {
56
- this.name = name;
57
- }
58
-
59
- greet(): string {
60
- return "Hi " + this.name;
61
- }
62
- }
63
- `);
64
-
65
- const { files } = scanFiles(dir);
66
- const graph = parseProject(dir, files);
67
-
68
- const cls = graph.symbols.find((s) => s.name === "User");
69
- expect(cls).toBeTruthy();
70
- expect(cls!.kind).toBe("class");
71
- expect(cls!.isExported).toBe(true);
72
-
73
- const greet = graph.symbols.find((s) => s.name === "greet");
74
- expect(greet).toBeTruthy();
75
- expect(greet!.kind).toBe("method");
76
- expect(greet!.receiver).toBe("User");
77
-
78
- fs.rmSync(dir, { recursive: true });
79
- });
80
-
81
- it("extracts interfaces and type aliases", () => {
82
- const dir = createTempDir();
83
- writeFile(dir, "types.ts", `
84
- export interface Props {
85
- name: string;
86
- age?: number;
87
- }
88
-
89
- export type Status = "active" | "inactive";
90
-
91
- type Internal = number;
92
- `);
93
-
94
- const { files } = scanFiles(dir);
95
- const graph = parseProject(dir, files);
96
-
97
- expect(graph.symbols).toHaveLength(3);
98
-
99
- const props = graph.symbols.find((s) => s.name === "Props");
100
- expect(props).toBeTruthy();
101
- expect(props!.kind).toBe("interface");
102
- expect(props!.isExported).toBe(true);
103
-
104
- const status = graph.symbols.find((s) => s.name === "Status");
105
- expect(status).toBeTruthy();
106
- expect(status!.kind).toBe("type_alias");
107
- expect(status!.isExported).toBe(true);
108
-
109
- const internal = graph.symbols.find((s) => s.name === "Internal");
110
- expect(internal).toBeTruthy();
111
- expect(internal!.kind).toBe("type_alias");
112
- expect(internal!.isExported).toBe(false);
113
-
114
- fs.rmSync(dir, { recursive: true });
115
- });
116
-
117
- it("extracts enums", () => {
118
- const dir = createTempDir();
119
- writeFile(dir, "enums.ts", `
120
- export enum Color {
121
- Red,
122
- Green,
123
- Blue,
124
- }
125
- `);
126
-
127
- const { files } = scanFiles(dir);
128
- const graph = parseProject(dir, files);
129
-
130
- const color = graph.symbols.find((s) => s.name === "Color");
131
- expect(color).toBeTruthy();
132
- expect(color!.kind).toBe("enum");
133
- expect(color!.isExported).toBe(true);
134
-
135
- fs.rmSync(dir, { recursive: true });
136
- });
137
-
138
- it("extracts const and var declarations", () => {
139
- const dir = createTempDir();
140
- writeFile(dir, "vars.ts", `
141
- export const MAX = 100;
142
- const TIMEOUT = 5000;
143
- var name = "test";
144
- `);
145
-
146
- const { files } = scanFiles(dir);
147
- const graph = parseProject(dir, files);
148
-
149
- const max = graph.symbols.find((s) => s.name === "MAX");
150
- expect(max).toBeTruthy();
151
- expect(max!.kind).toBe("const");
152
- expect(max!.isExported).toBe(true);
153
-
154
- const timeout = graph.symbols.find((s) => s.name === "TIMEOUT");
155
- expect(timeout).toBeTruthy();
156
- expect(timeout!.kind).toBe("const");
157
- expect(timeout!.isExported).toBe(false);
158
-
159
- const n = graph.symbols.find((s) => s.name === "name");
160
- expect(n).toBeTruthy();
161
- expect(n!.kind).toBe("var");
162
-
163
- fs.rmSync(dir, { recursive: true });
164
- });
165
-
166
- it("extracts call expressions within function bodies", () => {
167
- const dir = createTempDir();
168
- writeFile(dir, "lib.ts", `
169
- export function log(msg: string) {
170
- console.log(msg);
171
- }
172
-
173
- export function process() {
174
- log("starting");
175
- Math.random();
176
- }
177
- `);
178
-
179
- const { files } = scanFiles(dir);
180
- const graph = parseProject(dir, files);
181
-
182
- // process calls log and Math.random
183
- const processCalls = graph.calls.filter((c) => c.callerName === "process");
184
- expect(processCalls.length).toBeGreaterThanOrEqual(2);
185
- expect(processCalls.some((c) => c.calleeRaw === "log")).toBe(true);
186
- expect(processCalls.some((c) => c.calleeRaw === "Math.random")).toBe(true);
187
-
188
- fs.rmSync(dir, { recursive: true });
189
- });
190
-
191
- it("extracts imports", () => {
192
- const dir = createTempDir();
193
- writeFile(dir, "main.ts", `
194
- import React, { useState } from "react";
195
- import { z } from "zod";
196
- import fs from "node:fs";
197
- `);
198
- writeFile(dir, "package.json", JSON.stringify({
199
- name: "test-project",
200
- dependencies: { react: "^18", zod: "^3" },
201
- }));
202
-
203
- const { files } = scanFiles(dir);
204
- const graph = parseProject(dir, files);
205
-
206
- expect(graph.imports.length).toBeGreaterThanOrEqual(3);
207
-
208
- const reactDefault = graph.imports.find(
209
- (i) => i.importPath === "react" && i.alias === "React",
210
- );
211
- expect(reactDefault).toBeTruthy();
212
- expect(reactDefault!.isDefault).toBe(true);
213
-
214
- const useState = graph.imports.find(
215
- (i) => i.importPath === "react" && i.alias === "useState",
216
- );
217
- expect(useState).toBeTruthy();
218
- expect(useState!.isDefault).toBe(false);
219
-
220
- const zodImport = graph.imports.find(
221
- (i) => i.importPath === "zod" && i.alias === "z",
222
- );
223
- expect(zodImport).toBeTruthy();
224
-
225
- fs.rmSync(dir, { recursive: true });
226
- });
227
-
228
- it("reads dependencies from package.json", () => {
229
- const dir = createTempDir();
230
- writeFile(dir, "index.ts", `export const x = 1;`);
231
- writeFile(dir, "package.json", JSON.stringify({
232
- name: "my-app",
233
- dependencies: { react: "^18" },
234
- devDependencies: { vitest: "^1" },
235
- }));
236
-
237
- const { files } = scanFiles(dir);
238
- const graph = parseProject(dir, files);
239
-
240
- expect(graph.dependencies).toHaveLength(2);
241
- expect(graph.dependencies.some((d) => d.module === "react")).toBe(true);
242
- expect(graph.dependencies.some((d) => d.module === "vitest")).toBe(true);
243
-
244
- fs.rmSync(dir, { recursive: true });
245
- });
246
-
247
- it("creates file nodes for all scanned files", () => {
248
- const dir = createTempDir();
249
- writeFile(dir, "src/index.ts", "");
250
- writeFile(dir, "src/util.ts", "");
251
- writeFile(dir, "data.json", "{}");
252
- writeFile(dir, "style.css", "body{}");
253
-
254
- const { files } = scanFiles(dir);
255
- const graph = parseProject(dir, files);
256
-
257
- expect(graph.files).toHaveLength(4);
258
- const paths = graph.files.map((f) => f.path).sort();
259
- expect(paths).toContain("data.json");
260
- expect(paths).toContain("style.css");
261
- expect(paths).toContain("src/index.ts");
262
- expect(paths).toContain("src/util.ts");
263
-
264
- fs.rmSync(dir, { recursive: true });
265
- });
266
-
267
- it("handles empty project with no .ts files", () => {
268
- const dir = createTempDir();
269
- writeFile(dir, "README.md", "# Hello");
270
-
271
- const { files } = scanFiles(dir);
272
- const graph = parseProject(dir, files);
273
-
274
- expect(graph.symbols).toHaveLength(0);
275
- expect(graph.calls).toHaveLength(0);
276
- expect(graph.imports).toHaveLength(0);
277
- expect(graph.files).toHaveLength(1);
278
- expect(graph.packages).toHaveLength(1);
279
-
280
- fs.rmSync(dir, { recursive: true });
281
- });
282
- });
@@ -1,162 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import {
3
- makeGraph,
4
- makeSymbolNode,
5
- makeFileNode,
6
- makeCallEdge,
7
- makeImportEdge,
8
- makePackageNode,
9
- } from "../graph/types.js";
10
- import { generatePlan, generateReview } from "./index.js";
11
- import path from "node:path";
12
- import fs from "node:fs";
13
- import os from "node:os";
14
- import { execSync } from "node:child_process";
15
-
16
- function createGitRepo(): string {
17
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "tsgraph-plan-"));
18
- execSync("git init", { cwd: dir, stdio: "pipe" });
19
- execSync('git config user.email "test@test.com"', { cwd: dir, stdio: "pipe" });
20
- execSync('git config user.name "Test"', { cwd: dir, stdio: "pipe" });
21
- return dir;
22
- }
23
-
24
- function writeFile(root: string, relPath: string, content: string) {
25
- const fullPath = path.join(root, relPath);
26
- fs.mkdirSync(path.dirname(fullPath), { recursive: true });
27
- fs.writeFileSync(fullPath, content, "utf-8");
28
- }
29
-
30
- describe("generatePlan", () => {
31
- it("returns summary for given files and symbols", () => {
32
- const graph = makeGraph({
33
- files: [
34
- makeFileNode({ path: "src/util.ts", packageName: "app" }),
35
- makeFileNode({ path: "src/index.ts", packageName: "app" }),
36
- ],
37
- symbols: [
38
- makeSymbolNode({
39
- id: "src/util.ts::helper",
40
- name: "helper",
41
- kind: "function",
42
- file: "src/util.ts",
43
- packageName: "app",
44
- line: 1,
45
- endLine: 5,
46
- isExported: true,
47
- }),
48
- makeSymbolNode({
49
- id: "src/index.ts::main",
50
- name: "main",
51
- kind: "function",
52
- file: "src/index.ts",
53
- packageName: "app",
54
- line: 10,
55
- endLine: 15,
56
- isExported: true,
57
- }),
58
- ],
59
- calls: [
60
- makeCallEdge({
61
- callerSymbolId: "src/index.ts::main",
62
- callerName: "main",
63
- calleeRaw: "helper",
64
- file: "src/index.ts",
65
- line: 11,
66
- }),
67
- ],
68
- imports: [],
69
- });
70
-
71
- const result = generatePlan(graph, ["src/util.ts"], []);
72
- expect(result.changes.files).toContain("src/util.ts");
73
- expect(result.affectedFiles).toContain("src/util.ts");
74
- expect(result.affectedFiles).toContain("src/index.ts");
75
- expect(result.summary).toContain("1 symbol(s) changed");
76
- });
77
-
78
- it("handles explicit symbols", () => {
79
- const graph = makeGraph({
80
- files: [makeFileNode({ path: "src/lib.ts", packageName: "app" })],
81
- symbols: [
82
- makeSymbolNode({
83
- id: "src/lib.ts::foo",
84
- name: "foo",
85
- kind: "function",
86
- file: "src/lib.ts",
87
- packageName: "app",
88
- line: 1,
89
- endLine: 3,
90
- isExported: true,
91
- }),
92
- ],
93
- calls: [],
94
- imports: [],
95
- });
96
-
97
- const result = generatePlan(graph, [], ["foo"]);
98
- expect(result.changes.symbols).toContain("foo");
99
- expect(result.summary).toContain("1 symbol(s) changed");
100
- });
101
- });
102
-
103
- describe("generateReview", () => {
104
- it("returns empty result if not a git repo", () => {
105
- const graph = makeGraph({ files: [], symbols: [], imports: [] });
106
- const result = generateReview(graph, "/tmp/nonexistent");
107
- expect(result.totalChanges).toBe(0);
108
- expect(result.summary).toContain("Not a git repository");
109
- });
110
-
111
- it("finds findings for changed files", () => {
112
- const dir = createGitRepo();
113
- writeFile(dir, "src/index.ts", 'export const x = 1;\n');
114
- execSync("git add . && git commit -m 'initial'", { cwd: dir, stdio: "pipe" });
115
- writeFile(dir, "src/new.ts", 'export const y = 2;\n');
116
- execSync("git add . && git commit -m 'add new'", { cwd: dir, stdio: "pipe" });
117
-
118
- const graph = makeGraph({
119
- files: [
120
- makeFileNode({ path: "src/index.ts", packageName: "app" }),
121
- makeFileNode({ path: "src/new.ts", packageName: "app" }),
122
- ],
123
- symbols: [
124
- makeSymbolNode({
125
- id: "src/index.ts::x",
126
- name: "x",
127
- kind: "const",
128
- file: "src/index.ts",
129
- packageName: "app",
130
- line: 1,
131
- endLine: 1,
132
- isExported: true,
133
- }),
134
- makeSymbolNode({
135
- id: "src/new.ts::y",
136
- name: "y",
137
- kind: "const",
138
- file: "src/new.ts",
139
- packageName: "app",
140
- line: 1,
141
- endLine: 1,
142
- isExported: true,
143
- }),
144
- ],
145
- calls: [],
146
- imports: [],
147
- });
148
-
149
- const result = generateReview(graph, dir, "HEAD~1");
150
- expect(result.totalChanges).toBeGreaterThanOrEqual(1);
151
- expect(result.findings.length).toBeGreaterThanOrEqual(1);
152
- const orphan = result.findings.find((f) => f.type === "orphan");
153
- expect(orphan).toBeTruthy();
154
- expect(orphan!.detail).toContain("y");
155
-
156
- const changedExport = result.findings.find(
157
- (f) => f.type === "changed_export",
158
- );
159
- expect(changedExport).toBeTruthy();
160
- fs.rmSync(dir, { recursive: true });
161
- });
162
- });
package/src/plan/index.ts DELETED
@@ -1,161 +0,0 @@
1
- import type { Graph, SymbolNode } from "../graph/types.js";
2
- import { findCallers } from "../search/index.js";
3
- import { getDiffFiles, isGitRepo } from "../git/index.js";
4
- import type { ChangedFile } from "../git/index.js";
5
- import { checkBoundaries, loadBoundariesConfig } from "../boundaries/index.js";
6
-
7
- export interface PlannedChange {
8
- files: string[];
9
- symbols: string[];
10
- }
11
-
12
- export interface PlanResult {
13
- changes: PlannedChange;
14
- affectedCallers: { symbol: SymbolNode; callerCount: number }[];
15
- affectedFiles: string[];
16
- summary: string;
17
- }
18
-
19
- export interface ReviewFinding {
20
- type: "orphan" | "boundary" | "changed_export";
21
- detail: string;
22
- }
23
-
24
- export interface ReviewResult {
25
- changes: { path: string; status: ChangedFile["status"] }[];
26
- findings: ReviewFinding[];
27
- totalChanges: number;
28
- totalFindings: number;
29
- summary: string;
30
- }
31
-
32
- function symbolsInFile(graph: Graph, filePath: string): SymbolNode[] {
33
- return graph.symbols.filter((s) => s.file === filePath);
34
- }
35
-
36
- export function generatePlan(
37
- graph: Graph,
38
- files: string[],
39
- symbols: string[] = [],
40
- ): PlanResult {
41
- const allSymbolNames = new Set(symbols);
42
-
43
- for (const f of files) {
44
- for (const sym of symbolsInFile(graph, f)) {
45
- allSymbolNames.add(sym.name);
46
- }
47
- }
48
-
49
- const callersMap = new Map<string, { symbol: SymbolNode; callerCount: number }>();
50
-
51
- for (const symName of allSymbolNames) {
52
- const sym = graph.symbols.find((s) => s.name === symName);
53
- if (!sym) continue;
54
- const callers = findCallers(graph, sym.name);
55
- callersMap.set(symName, {
56
- symbol: sym,
57
- callerCount: callers.length,
58
- });
59
- }
60
-
61
- const affectedFiles = new Set<string>();
62
- for (const [_, info] of callersMap) {
63
- affectedFiles.add(info.symbol.file);
64
- const callers = findCallers(graph, info.symbol.name);
65
- for (const c of callers) {
66
- affectedFiles.add(c.callerSymbol.file);
67
- }
68
- }
69
- for (const f of files) {
70
- affectedFiles.add(f);
71
- }
72
-
73
- const totalAffected = callersMap.size;
74
- const totalCallers = [...callersMap.values()].reduce(
75
- (sum, c) => sum + c.callerCount,
76
- 0,
77
- );
78
-
79
- const summary =
80
- `Plan: ${files.length} file(s), ${allSymbolNames.size} symbol(s) changed. ` +
81
- `Affects ${totalAffected} symbol(s) with ${totalCallers} total caller(s) across ${affectedFiles.size} file(s).`;
82
-
83
- return {
84
- changes: { files, symbols: [...allSymbolNames] },
85
- affectedCallers: [...callersMap.values()],
86
- affectedFiles: [...affectedFiles],
87
- summary,
88
- };
89
- }
90
-
91
- export function generateReview(
92
- graph: Graph,
93
- rootDir: string,
94
- base: string = "main",
95
- ): ReviewResult {
96
- const findings: ReviewFinding[] = [];
97
-
98
- if (!isGitRepo(rootDir)) {
99
- return {
100
- changes: [],
101
- findings: [],
102
- totalChanges: 0,
103
- totalFindings: 0,
104
- summary: "Not a git repository — cannot review.",
105
- };
106
- }
107
-
108
- const diffFiles = getDiffFiles(rootDir, base);
109
- const changedPaths = new Set(diffFiles.map((d) => d.path));
110
-
111
- for (const f of diffFiles) {
112
- const fileSyms = symbolsInFile(graph, f.path);
113
- for (const sym of fileSyms) {
114
- if (f.status === "added") {
115
- const callers = findCallers(graph, sym.id);
116
- if (callers.length === 0 && !sym.name.startsWith("_")) {
117
- findings.push({
118
- type: "orphan",
119
- detail: `${sym.kind} ${sym.name} in ${sym.file}:${sym.line} — new symbol has no callers`,
120
- });
121
- }
122
- }
123
- if (f.status === "modified" || f.status === "added") {
124
- if (sym.isExported) {
125
- findings.push({
126
- type: "changed_export",
127
- detail: `${sym.isExported ? "exported " : ""}${sym.kind} ${sym.name} in ${sym.file}:${sym.line} — public API change`,
128
- });
129
- }
130
- }
131
- }
132
- }
133
-
134
- const boundariesConfig = loadBoundariesConfig(rootDir);
135
- if (boundariesConfig) {
136
- const result = checkBoundaries(graph, boundariesConfig);
137
- for (const v of result.violations) {
138
- if (changedPaths.has(v.fromFile) || changedPaths.has(v.toFile)) {
139
- findings.push({
140
- type: "boundary",
141
- detail: `${v.fromFile} → ${v.toFile}: ${v.rule}`,
142
- });
143
- }
144
- }
145
- }
146
-
147
- const summary =
148
- `Review: ${diffFiles.length} file(s) changed vs "${base}". ` +
149
- `${findings.length} finding(s): ` +
150
- `${findings.filter((f) => f.type === "orphan").length} orphan(s), ` +
151
- `${findings.filter((f) => f.type === "changed_export").length} changed export(s), ` +
152
- `${findings.filter((f) => f.type === "boundary").length} boundary violation(s).`;
153
-
154
- return {
155
- changes: diffFiles,
156
- findings,
157
- totalChanges: diffFiles.length,
158
- totalFindings: findings.length,
159
- summary,
160
- };
161
- }