@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.
- package/dist/changes/index.test.js +2 -6
- package/dist/changes/index.test.js.map +1 -1
- package/dist/cli/index.js +184 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/git/index.test.js +4 -6
- package/dist/git/index.test.js.map +1 -1
- package/dist/opencode/index.js +1 -1
- package/dist/opencode/index.js.map +1 -1
- package/dist/opencode/index.test.js +2 -2
- package/dist/opencode/index.test.js.map +1 -1
- package/dist/search/index.d.ts.map +1 -1
- package/dist/search/index.js +12 -4
- package/dist/search/index.js.map +1 -1
- package/package.json +16 -1
- package/AGENTS.md +0 -64
- package/TODOS.md +0 -61
- package/opencode.json +0 -24
- package/src/analysis/analysis.test.ts +0 -405
- package/src/analysis/complexity.ts +0 -107
- package/src/analysis/coupling.ts +0 -106
- package/src/analysis/hotspot.ts +0 -52
- package/src/analysis/index.ts +0 -17
- package/src/boundaries/index.test.ts +0 -335
- package/src/boundaries/index.ts +0 -137
- package/src/changes/index.test.ts +0 -114
- package/src/changes/index.ts +0 -95
- package/src/cli/index.ts +0 -736
- package/src/git/index.test.ts +0 -92
- package/src/git/index.ts +0 -86
- package/src/graph/types.test.ts +0 -383
- package/src/graph/types.ts +0 -353
- package/src/mcp/mcp.test.ts +0 -176
- package/src/mcp/server.ts +0 -217
- package/src/nextjs/index.ts +0 -23
- package/src/nextjs/nextjs.test.ts +0 -233
- package/src/nextjs/pages.ts +0 -43
- package/src/nextjs/react.ts +0 -100
- package/src/nextjs/router.ts +0 -102
- package/src/nextjs/routes.ts +0 -69
- package/src/opencode/index.test.ts +0 -90
- package/src/opencode/index.ts +0 -83
- package/src/parser/index.ts +0 -339
- package/src/parser/parser.test.ts +0 -282
- package/src/plan/index.test.ts +0 -162
- package/src/plan/index.ts +0 -161
- package/src/report/index.ts +0 -128
- package/src/scanner/index.ts +0 -97
- package/src/scanner/scanner.test.ts +0 -135
- package/src/search/index.ts +0 -163
- package/src/search/search.test.ts +0 -512
- package/src/traversal/index.ts +0 -5
- package/src/traversal/traversal.test.ts +0 -266
- package/src/traversal/traversal.ts +0 -185
- package/tsconfig.json +0 -20
- package/vitest.config.ts +0 -7
package/src/git/index.test.ts
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import fs from "node:fs";
|
|
4
|
-
import os from "node:os";
|
|
5
|
-
import { execSync } from "node:child_process";
|
|
6
|
-
import {
|
|
7
|
-
isGitRepo,
|
|
8
|
-
getCurrentBranch,
|
|
9
|
-
getDiffFiles,
|
|
10
|
-
getStaleFiles,
|
|
11
|
-
getCommitHistory,
|
|
12
|
-
} from "./index.js";
|
|
13
|
-
|
|
14
|
-
let tmpDir: string;
|
|
15
|
-
|
|
16
|
-
function run(cmd: string) {
|
|
17
|
-
execSync(cmd, { cwd: tmpDir, stdio: "pipe" });
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function writeFile(relativePath: string, content: string) {
|
|
21
|
-
const fullPath = path.join(tmpDir, relativePath);
|
|
22
|
-
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
23
|
-
fs.writeFileSync(fullPath, content, "utf-8");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
beforeAll(() => {
|
|
27
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tsgraph-git-test-"));
|
|
28
|
-
run("git init");
|
|
29
|
-
run('git config user.email "test@test.com"');
|
|
30
|
-
run('git config user.name "Test"');
|
|
31
|
-
writeFile(".gitkeep", "");
|
|
32
|
-
run("git add . && git commit -m 'initial'");
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
afterAll(() => {
|
|
36
|
-
fs.rmSync(tmpDir, { recursive: true });
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
describe("isGitRepo", () => {
|
|
40
|
-
it("returns true inside a git repo", () => {
|
|
41
|
-
expect(isGitRepo(tmpDir)).toBe(true);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("returns false outside a git repo", () => {
|
|
45
|
-
const nonRepo = fs.mkdtempSync(path.join(os.tmpdir(), "tsgraph-non-git-"));
|
|
46
|
-
expect(isGitRepo(nonRepo)).toBe(false);
|
|
47
|
-
fs.rmSync(nonRepo, { recursive: true });
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
describe("getCurrentBranch", () => {
|
|
52
|
-
it("returns current branch name", () => {
|
|
53
|
-
const branch = getCurrentBranch(tmpDir);
|
|
54
|
-
expect(branch).toBeTruthy();
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
describe("getDiffFiles", () => {
|
|
59
|
-
it("returns changed files vs base branch", () => {
|
|
60
|
-
writeFile("file1.ts", "content");
|
|
61
|
-
run("git add . && git commit -m 'initial'");
|
|
62
|
-
run("git checkout -b feature");
|
|
63
|
-
writeFile("file2.ts", "content");
|
|
64
|
-
run("git add . && git commit -m 'add file2'");
|
|
65
|
-
writeFile("file3.ts", "content");
|
|
66
|
-
run("git add . && git commit -m 'add file3'");
|
|
67
|
-
const results = getDiffFiles(tmpDir, "main");
|
|
68
|
-
expect(results.length).toBeGreaterThanOrEqual(2);
|
|
69
|
-
const paths = results.map((r) => r.path);
|
|
70
|
-
expect(paths).toContain("file2.ts");
|
|
71
|
-
expect(paths).toContain("file3.ts");
|
|
72
|
-
run("git checkout main");
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
describe("getCommitHistory", () => {
|
|
77
|
-
it("returns recent commit history", () => {
|
|
78
|
-
const history = getCommitHistory(tmpDir, 5);
|
|
79
|
-
expect(history.length).toBeGreaterThan(0);
|
|
80
|
-
expect(history[0]).toHaveProperty("hash");
|
|
81
|
-
expect(history[0]).toHaveProperty("message");
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
describe("getStaleFiles", () => {
|
|
86
|
-
it("does not return recently committed files", () => {
|
|
87
|
-
writeFile("fresh.ts", "fresh content");
|
|
88
|
-
run("git add . && git commit -m 'add fresh'");
|
|
89
|
-
const stale = getStaleFiles(tmpDir, 0);
|
|
90
|
-
expect(stale).not.toContain("fresh.ts");
|
|
91
|
-
});
|
|
92
|
-
});
|
package/src/git/index.ts
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { execSync } from "node:child_process";
|
|
2
|
-
|
|
3
|
-
export interface ChangedFile {
|
|
4
|
-
path: string;
|
|
5
|
-
status: "added" | "modified" | "deleted" | "renamed";
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface CommitInfo {
|
|
9
|
-
hash: string;
|
|
10
|
-
date: string;
|
|
11
|
-
message: string;
|
|
12
|
-
author: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function isGitRepo(root: string): boolean {
|
|
16
|
-
try {
|
|
17
|
-
execSync("git rev-parse --git-dir", { cwd: root, stdio: "ignore" });
|
|
18
|
-
return true;
|
|
19
|
-
} catch {
|
|
20
|
-
return false;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function getCurrentBranch(root: string): string {
|
|
25
|
-
return execSync("git rev-parse --abbrev-ref HEAD", {
|
|
26
|
-
cwd: root,
|
|
27
|
-
encoding: "utf-8",
|
|
28
|
-
}).trim();
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function getDiffFiles(root: string, base: string = "main"): ChangedFile[] {
|
|
32
|
-
const output = execSync(`git diff --name-status ${base}...HEAD`, {
|
|
33
|
-
cwd: root,
|
|
34
|
-
encoding: "utf-8",
|
|
35
|
-
}).trim();
|
|
36
|
-
if (!output) return [];
|
|
37
|
-
return output.split("\n").map((line) => {
|
|
38
|
-
const parts = line.split(/\s+/);
|
|
39
|
-
const status = parts[0];
|
|
40
|
-
let fileStatus: ChangedFile["status"];
|
|
41
|
-
let filePath = parts[1] ?? "";
|
|
42
|
-
if (status === "A") fileStatus = "added";
|
|
43
|
-
else if (status === "D") fileStatus = "deleted";
|
|
44
|
-
else if (status === "R" || status.startsWith("R")) fileStatus = "renamed";
|
|
45
|
-
else fileStatus = "modified";
|
|
46
|
-
if (fileStatus === "renamed" && parts[2]) filePath = parts[2];
|
|
47
|
-
return { path: filePath, status: fileStatus };
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function getStaleFiles(root: string, thresholdDays: number = 90): string[] {
|
|
52
|
-
const cutoff = new Date();
|
|
53
|
-
cutoff.setDate(cutoff.getDate() - thresholdDays);
|
|
54
|
-
const cutoffStr = cutoff.toISOString().split("T")[0];
|
|
55
|
-
try {
|
|
56
|
-
const output = execSync(
|
|
57
|
-
`git log --pretty=format: --name-only --diff-filter=AM --since=${cutoffStr}`,
|
|
58
|
-
{ cwd: root, encoding: "utf-8" },
|
|
59
|
-
).trim();
|
|
60
|
-
const recentFiles = new Set(output.split("\n").filter(Boolean));
|
|
61
|
-
const allFiles = execSync("git ls-files", {
|
|
62
|
-
cwd: root,
|
|
63
|
-
encoding: "utf-8",
|
|
64
|
-
})
|
|
65
|
-
.trim()
|
|
66
|
-
.split("\n")
|
|
67
|
-
.filter(Boolean);
|
|
68
|
-
return allFiles.filter((f) => !recentFiles.has(f));
|
|
69
|
-
} catch {
|
|
70
|
-
return [];
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function getCommitHistory(root: string, count: number = 10): CommitInfo[] {
|
|
75
|
-
const output = execSync(
|
|
76
|
-
`git log --max-count=${count} --format="%H|%ad|%s|%an" --date=short`,
|
|
77
|
-
{ cwd: root, encoding: "utf-8" },
|
|
78
|
-
).trim();
|
|
79
|
-
if (!output) return [];
|
|
80
|
-
return output.split("\n").map((line) => {
|
|
81
|
-
const [hash, date, ...msgParts] = line.split("|");
|
|
82
|
-
const message = msgParts.slice(0, -1).join("|");
|
|
83
|
-
const author = msgParts[msgParts.length - 1] ?? "";
|
|
84
|
-
return { hash, date, message, author };
|
|
85
|
-
});
|
|
86
|
-
}
|
package/src/graph/types.test.ts
DELETED
|
@@ -1,383 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
GRAPH_VERSION,
|
|
4
|
-
makeGraph,
|
|
5
|
-
makePackageNode,
|
|
6
|
-
makeFileNode,
|
|
7
|
-
makeSymbolNode,
|
|
8
|
-
makeCallEdge,
|
|
9
|
-
makeImportEdge,
|
|
10
|
-
makeDependency,
|
|
11
|
-
makeHTTPRoute,
|
|
12
|
-
makeEnvRead,
|
|
13
|
-
makeConcurrencyNode,
|
|
14
|
-
makeTestEdge,
|
|
15
|
-
makeImplementsEdge,
|
|
16
|
-
makeMutationEdge,
|
|
17
|
-
makeErrorEdge,
|
|
18
|
-
makeAppRouterNode,
|
|
19
|
-
makeStructField,
|
|
20
|
-
serialize,
|
|
21
|
-
deserialize,
|
|
22
|
-
} from "./types.js";
|
|
23
|
-
|
|
24
|
-
describe("GRAPH_VERSION", () => {
|
|
25
|
-
it("is 1", () => {
|
|
26
|
-
expect(GRAPH_VERSION).toBe("1");
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
describe("factory functions", () => {
|
|
31
|
-
describe("makeGraph", () => {
|
|
32
|
-
it("returns a graph with defaults", () => {
|
|
33
|
-
const g = makeGraph();
|
|
34
|
-
expect(g.version).toBe(GRAPH_VERSION);
|
|
35
|
-
expect(g.generatedAt).toBeTruthy();
|
|
36
|
-
expect(g.root).toBe("");
|
|
37
|
-
expect(g.packages).toEqual([]);
|
|
38
|
-
expect(g.files).toEqual([]);
|
|
39
|
-
expect(g.symbols).toEqual([]);
|
|
40
|
-
expect(g.imports).toEqual([]);
|
|
41
|
-
expect(g.calls).toEqual([]);
|
|
42
|
-
expect(g.envReads).toEqual([]);
|
|
43
|
-
expect(g.dependencies).toEqual([]);
|
|
44
|
-
expect(g.routes).toEqual([]);
|
|
45
|
-
expect(g.concurrency).toEqual([]);
|
|
46
|
-
expect(g.testEdges).toEqual([]);
|
|
47
|
-
expect(g.implements).toEqual([]);
|
|
48
|
-
expect(g.mutations).toEqual([]);
|
|
49
|
-
expect(g.errors).toEqual([]);
|
|
50
|
-
expect(g.appRouter).toBeUndefined();
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("merges overrides", () => {
|
|
54
|
-
const g = makeGraph({ root: "/project", version: "2" });
|
|
55
|
-
expect(g.root).toBe("/project");
|
|
56
|
-
expect(g.version).toBe("2");
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("generates an ISO timestamp", () => {
|
|
60
|
-
const g = makeGraph();
|
|
61
|
-
expect(() => new Date(g.generatedAt)).not.toThrow();
|
|
62
|
-
expect(new Date(g.generatedAt).toISOString()).toBe(g.generatedAt);
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
describe("makePackageNode", () => {
|
|
67
|
-
it("returns a package node with defaults", () => {
|
|
68
|
-
const p = makePackageNode();
|
|
69
|
-
expect(p.id).toBeTruthy();
|
|
70
|
-
expect(p.name).toBe("");
|
|
71
|
-
expect(p.importPathBestEffort).toBe("");
|
|
72
|
-
expect(p.dir).toBe("");
|
|
73
|
-
expect(p.files).toEqual([]);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("merges overrides", () => {
|
|
77
|
-
const p = makePackageNode({ name: "utils", dir: "./utils" });
|
|
78
|
-
expect(p.name).toBe("utils");
|
|
79
|
-
expect(p.dir).toBe("./utils");
|
|
80
|
-
expect(p.files).toEqual([]);
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
describe("makeFileNode", () => {
|
|
85
|
-
it("returns a file node with defaults", () => {
|
|
86
|
-
const f = makeFileNode();
|
|
87
|
-
expect(f.id).toBeTruthy();
|
|
88
|
-
expect(f.path).toBe("");
|
|
89
|
-
expect(f.packageName).toBe("");
|
|
90
|
-
expect(f.lines).toBe(0);
|
|
91
|
-
expect(f.generated).toBe(false);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("merges overrides", () => {
|
|
95
|
-
const f = makeFileNode({ path: "src/index.ts", generated: true, lines: 42 });
|
|
96
|
-
expect(f.path).toBe("src/index.ts");
|
|
97
|
-
expect(f.generated).toBe(true);
|
|
98
|
-
expect(f.lines).toBe(42);
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
describe("makeSymbolNode", () => {
|
|
103
|
-
it("returns a symbol node with defaults", () => {
|
|
104
|
-
const s = makeSymbolNode();
|
|
105
|
-
expect(s.id).toBeTruthy();
|
|
106
|
-
expect(s.kind).toBe("function");
|
|
107
|
-
expect(s.name).toBe("");
|
|
108
|
-
expect(s.packageName).toBe("");
|
|
109
|
-
expect(s.file).toBe("");
|
|
110
|
-
expect(s.line).toBe(0);
|
|
111
|
-
expect(s.endLine).toBe(0);
|
|
112
|
-
expect(s.isExported).toBe(false);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("merges overrides", () => {
|
|
116
|
-
const s = makeSymbolNode({ name: "Foo", kind: "class", isExported: true, line: 10, endLine: 30 });
|
|
117
|
-
expect(s.name).toBe("Foo");
|
|
118
|
-
expect(s.kind).toBe("class");
|
|
119
|
-
expect(s.isExported).toBe(true);
|
|
120
|
-
expect(s.line).toBe(10);
|
|
121
|
-
expect(s.endLine).toBe(30);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it("preserves optional fields", () => {
|
|
125
|
-
const s = makeSymbolNode({
|
|
126
|
-
doc: "does foo",
|
|
127
|
-
signature: "Foo() => void",
|
|
128
|
-
structFields: [{ name: "bar", type: "string" }],
|
|
129
|
-
embeddedTypes: ["Mixin"],
|
|
130
|
-
arity: 2,
|
|
131
|
-
isClientComponent: true,
|
|
132
|
-
isServerComponent: false,
|
|
133
|
-
});
|
|
134
|
-
expect(s.doc).toBe("does foo");
|
|
135
|
-
expect(s.signature).toBe("Foo() => void");
|
|
136
|
-
expect(s.structFields).toHaveLength(1);
|
|
137
|
-
expect(s.structFields![0].name).toBe("bar");
|
|
138
|
-
expect(s.embeddedTypes).toEqual(["Mixin"]);
|
|
139
|
-
expect(s.arity).toBe(2);
|
|
140
|
-
expect(s.isClientComponent).toBe(true);
|
|
141
|
-
expect(s.isServerComponent).toBe(false);
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
describe("makeCallEdge", () => {
|
|
146
|
-
it("returns defaults", () => {
|
|
147
|
-
const e = makeCallEdge();
|
|
148
|
-
expect(e.callerSymbolId).toBe("");
|
|
149
|
-
expect(e.callerName).toBe("");
|
|
150
|
-
expect(e.calleeRaw).toBe("");
|
|
151
|
-
expect(e.file).toBe("");
|
|
152
|
-
expect(e.line).toBe(0);
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
describe("makeImportEdge", () => {
|
|
157
|
-
it("returns defaults", () => {
|
|
158
|
-
const e = makeImportEdge();
|
|
159
|
-
expect(e.fromFile).toBe("");
|
|
160
|
-
expect(e.fromPackage).toBe("");
|
|
161
|
-
expect(e.importPath).toBe("");
|
|
162
|
-
expect(e.isDefault).toBe(false);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it("sets isDefault", () => {
|
|
166
|
-
const e = makeImportEdge({ isDefault: true });
|
|
167
|
-
expect(e.isDefault).toBe(true);
|
|
168
|
-
});
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
describe("makeDependency", () => {
|
|
172
|
-
it("returns defaults", () => {
|
|
173
|
-
const d = makeDependency();
|
|
174
|
-
expect(d.module).toBe("");
|
|
175
|
-
expect(d.version).toBe("");
|
|
176
|
-
});
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
describe("makeHTTPRoute", () => {
|
|
180
|
-
it("returns defaults", () => {
|
|
181
|
-
const r = makeHTTPRoute();
|
|
182
|
-
expect(r.method).toBe("");
|
|
183
|
-
expect(r.path).toBe("");
|
|
184
|
-
expect(r.handler).toBe("");
|
|
185
|
-
expect(r.file).toBe("");
|
|
186
|
-
expect(r.line).toBe(0);
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
describe("makeEnvRead", () => {
|
|
191
|
-
it("returns defaults", () => {
|
|
192
|
-
const e = makeEnvRead();
|
|
193
|
-
expect(e.key).toBe("");
|
|
194
|
-
expect(e.accessor).toBe("");
|
|
195
|
-
expect(e.file).toBe("");
|
|
196
|
-
expect(e.line).toBe(0);
|
|
197
|
-
});
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
describe("makeConcurrencyNode", () => {
|
|
201
|
-
it("returns defaults", () => {
|
|
202
|
-
const c = makeConcurrencyNode();
|
|
203
|
-
expect(c.kind).toBe("async_function");
|
|
204
|
-
expect(c.functionName).toBe("");
|
|
205
|
-
expect(c.file).toBe("");
|
|
206
|
-
expect(c.line).toBe(0);
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
describe("makeTestEdge", () => {
|
|
211
|
-
it("returns defaults", () => {
|
|
212
|
-
const t = makeTestEdge();
|
|
213
|
-
expect(t.testFunc).toBe("");
|
|
214
|
-
expect(t.target).toBe("");
|
|
215
|
-
expect(t.file).toBe("");
|
|
216
|
-
expect(t.line).toBe(0);
|
|
217
|
-
});
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
describe("makeImplementsEdge", () => {
|
|
221
|
-
it("returns defaults", () => {
|
|
222
|
-
const e = makeImplementsEdge();
|
|
223
|
-
expect(e.interface).toBe("");
|
|
224
|
-
expect(e.concrete).toBe("");
|
|
225
|
-
});
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
describe("makeMutationEdge", () => {
|
|
229
|
-
it("returns defaults", () => {
|
|
230
|
-
const m = makeMutationEdge();
|
|
231
|
-
expect(m.field).toBe("");
|
|
232
|
-
expect(m.functionName).toBe("");
|
|
233
|
-
expect(m.file).toBe("");
|
|
234
|
-
expect(m.line).toBe(0);
|
|
235
|
-
});
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
describe("makeErrorEdge", () => {
|
|
239
|
-
it("returns defaults", () => {
|
|
240
|
-
const e = makeErrorEdge();
|
|
241
|
-
expect(e.message).toBe("");
|
|
242
|
-
expect(e.functionName).toBe("");
|
|
243
|
-
expect(e.file).toBe("");
|
|
244
|
-
expect(e.line).toBe(0);
|
|
245
|
-
});
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
describe("makeAppRouterNode", () => {
|
|
249
|
-
it("returns defaults", () => {
|
|
250
|
-
const n = makeAppRouterNode();
|
|
251
|
-
expect(n.path).toBe("");
|
|
252
|
-
expect(n.dir).toBe("");
|
|
253
|
-
expect(n.files).toEqual({});
|
|
254
|
-
expect(n.children).toEqual([]);
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
it("supports nested children", () => {
|
|
258
|
-
const child = makeAppRouterNode({ path: "/child" });
|
|
259
|
-
const parent = makeAppRouterNode({ path: "/", children: [child] });
|
|
260
|
-
expect(parent.children).toHaveLength(1);
|
|
261
|
-
expect(parent.children[0].path).toBe("/child");
|
|
262
|
-
});
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
describe("makeStructField", () => {
|
|
266
|
-
it("returns defaults", () => {
|
|
267
|
-
const f = makeStructField();
|
|
268
|
-
expect(f.name).toBe("");
|
|
269
|
-
expect(f.type).toBe("");
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
it("merges overrides", () => {
|
|
273
|
-
const f = makeStructField({ name: "id", type: "string", tag: "json:\"id\"" });
|
|
274
|
-
expect(f.name).toBe("id");
|
|
275
|
-
expect(f.type).toBe("string");
|
|
276
|
-
expect(f.tag).toBe("json:\"id\"");
|
|
277
|
-
});
|
|
278
|
-
});
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
describe("serialize / deserialize", () => {
|
|
282
|
-
describe("serialize", () => {
|
|
283
|
-
it("produces valid JSON with 2-space indentation", () => {
|
|
284
|
-
const g = makeGraph();
|
|
285
|
-
const json = serialize(g);
|
|
286
|
-
expect(json).toBeTruthy();
|
|
287
|
-
expect(json.startsWith("{")).toBe(true);
|
|
288
|
-
const parsed = JSON.parse(json);
|
|
289
|
-
expect(parsed.version).toBe(GRAPH_VERSION);
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
it("throws on version mismatch", () => {
|
|
293
|
-
const g = makeGraph({ version: "999" });
|
|
294
|
-
expect(() => serialize(g)).toThrow("version mismatch");
|
|
295
|
-
});
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
describe("deserialize", () => {
|
|
299
|
-
it("round-trips a graph with all fields", () => {
|
|
300
|
-
const original = makeGraph({
|
|
301
|
-
root: "/test",
|
|
302
|
-
packages: [makePackageNode({ name: "main", dir: "." })],
|
|
303
|
-
files: [makeFileNode({ path: "index.ts", packageName: "main" })],
|
|
304
|
-
symbols: [makeSymbolNode({ name: "run", kind: "function", file: "index.ts", line: 1, endLine: 5 })],
|
|
305
|
-
calls: [makeCallEdge({ callerSymbolId: "s1", callerName: "run", calleeRaw: "log", file: "index.ts", line: 2 })],
|
|
306
|
-
imports: [makeImportEdge({ fromFile: "index.ts", importPath: "fs" })],
|
|
307
|
-
dependencies: [makeDependency({ module: "react", version: "^18" })],
|
|
308
|
-
routes: [makeHTTPRoute({ method: "GET", path: "/api", handler: "getHandler", file: "route.ts", line: 3 })],
|
|
309
|
-
envReads: [makeEnvRead({ key: "PORT", accessor: "process.env.PORT", file: "config.ts", line: 5 })],
|
|
310
|
-
concurrency: [makeConcurrencyNode({ kind: "promise_all", functionName: "fetchAll", file: "fetch.ts", line: 10 })],
|
|
311
|
-
testEdges: [makeTestEdge({ testFunc: "TestRun", target: "run", file: "index_test.ts", line: 1 })],
|
|
312
|
-
implements: [makeImplementsEdge({ interface: "Runner", concrete: "FastRunner" })],
|
|
313
|
-
mutations: [makeMutationEdge({ field: "User.name", functionName: "rename", file: "user.ts", line: 8 })],
|
|
314
|
-
errors: [makeErrorEdge({ message: "not found", functionName: "findUser", file: "user.ts", line: 12 })],
|
|
315
|
-
appRouter: makeAppRouterNode({
|
|
316
|
-
path: "/",
|
|
317
|
-
dir: "app",
|
|
318
|
-
files: { page: "page.tsx", layout: "layout.tsx" },
|
|
319
|
-
children: [makeAppRouterNode({ path: "/about", dir: "about", files: { page: "page.tsx" } })],
|
|
320
|
-
}),
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
const json = serialize(original);
|
|
324
|
-
const restored = deserialize(json);
|
|
325
|
-
|
|
326
|
-
expect(restored.version).toBe(original.version);
|
|
327
|
-
expect(restored.root).toBe(original.root);
|
|
328
|
-
expect(restored.packages).toHaveLength(1);
|
|
329
|
-
expect(restored.files).toHaveLength(1);
|
|
330
|
-
expect(restored.symbols).toHaveLength(1);
|
|
331
|
-
expect(restored.calls).toHaveLength(1);
|
|
332
|
-
expect(restored.imports).toHaveLength(1);
|
|
333
|
-
expect(restored.dependencies).toHaveLength(1);
|
|
334
|
-
expect(restored.routes).toHaveLength(1);
|
|
335
|
-
expect(restored.envReads).toHaveLength(1);
|
|
336
|
-
expect(restored.concurrency).toHaveLength(1);
|
|
337
|
-
expect(restored.testEdges).toHaveLength(1);
|
|
338
|
-
expect(restored.implements).toHaveLength(1);
|
|
339
|
-
expect(restored.mutations).toHaveLength(1);
|
|
340
|
-
expect(restored.errors).toHaveLength(1);
|
|
341
|
-
expect(restored.appRouter).toBeTruthy();
|
|
342
|
-
expect(restored.appRouter!.children).toHaveLength(1);
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
it("round-trips an empty graph", () => {
|
|
346
|
-
const original = makeGraph({ root: "/empty" });
|
|
347
|
-
const json = serialize(original);
|
|
348
|
-
const restored = deserialize(json);
|
|
349
|
-
expect(restored.root).toBe("/empty");
|
|
350
|
-
expect(restored.packages).toEqual([]);
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
it("rejects malformed JSON", () => {
|
|
354
|
-
expect(() => deserialize("not json")).toThrow("Invalid JSON");
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
it("rejects null", () => {
|
|
358
|
-
expect(() => deserialize("null")).toThrow("Invalid graph structure");
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
it("rejects missing version", () => {
|
|
362
|
-
const json = JSON.stringify({ root: "/x", generatedAt: "", packages: [], files: [], symbols: [], imports: [], calls: [], envReads: [], dependencies: [], routes: [], concurrency: [], testEdges: [], implements: [], mutations: [], errors: [] });
|
|
363
|
-
expect(() => deserialize(json)).toThrow("Invalid graph structure");
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
it("rejects version mismatch", () => {
|
|
367
|
-
const g = makeGraph({ version: "2" });
|
|
368
|
-
expect(() => deserialize(serialize(g))).toThrow("version mismatch");
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
it("rejects missing array fields", () => {
|
|
372
|
-
const json = JSON.stringify({ version: GRAPH_VERSION, generatedAt: "", root: "" });
|
|
373
|
-
expect(() => deserialize(json)).toThrow("Invalid graph structure");
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
it("accepts extra unknown fields (forward compat)", () => {
|
|
377
|
-
const g = makeGraph({ root: "/future" });
|
|
378
|
-
const json = JSON.stringify({ ...JSON.parse(serialize(g)), futureField: "hello" });
|
|
379
|
-
const restored = deserialize(json);
|
|
380
|
-
expect(restored.root).toBe("/future");
|
|
381
|
-
});
|
|
382
|
-
});
|
|
383
|
-
});
|