devlensio 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/LICENSE +674 -0
- package/dist/clustering/index.d.ts +27 -0
- package/dist/clustering/index.js +149 -0
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.js +78 -0
- package/dist/config/providers/file.d.ts +19 -0
- package/dist/config/providers/file.js +215 -0
- package/dist/config/providers/request.d.ts +2 -0
- package/dist/config/providers/request.js +72 -0
- package/dist/config/types.d.ts +46 -0
- package/dist/config/types.js +81 -0
- package/dist/config/writer.d.ts +29 -0
- package/dist/config/writer.js +103 -0
- package/dist/filesystem/appRouter.d.ts +2 -0
- package/dist/filesystem/appRouter.js +126 -0
- package/dist/filesystem/backendRoutes.d.ts +2 -0
- package/dist/filesystem/backendRoutes.js +161 -0
- package/dist/filesystem/index.d.ts +2 -0
- package/dist/filesystem/index.js +28 -0
- package/dist/filesystem/index.test.d.ts +1 -0
- package/dist/filesystem/index.test.js +178 -0
- package/dist/filesystem/pagesRouter.d.ts +2 -0
- package/dist/filesystem/pagesRouter.js +109 -0
- package/dist/fingerprint/detectors.d.ts +8 -0
- package/dist/fingerprint/detectors.js +174 -0
- package/dist/fingerprint/index.d.ts +2 -0
- package/dist/fingerprint/index.js +41 -0
- package/dist/fingerprint/index.test.d.ts +1 -0
- package/dist/fingerprint/index.test.js +148 -0
- package/dist/graph/buildLookup.d.ts +10 -0
- package/dist/graph/buildLookup.js +32 -0
- package/dist/graph/edges/callEdges.d.ts +7 -0
- package/dist/graph/edges/callEdges.js +145 -0
- package/dist/graph/edges/eventEdges.d.ts +7 -0
- package/dist/graph/edges/eventEdges.js +203 -0
- package/dist/graph/edges/guardEdges.d.ts +3 -0
- package/dist/graph/edges/guardEdges.js +232 -0
- package/dist/graph/edges/hookEdges.d.ts +3 -0
- package/dist/graph/edges/hookEdges.js +54 -0
- package/dist/graph/edges/importEdges.d.ts +8 -0
- package/dist/graph/edges/importEdges.js +224 -0
- package/dist/graph/edges/propEdges.d.ts +3 -0
- package/dist/graph/edges/propEdges.js +142 -0
- package/dist/graph/edges/routeEdge.d.ts +3 -0
- package/dist/graph/edges/routeEdge.js +124 -0
- package/dist/graph/edges/stateEdges.d.ts +3 -0
- package/dist/graph/edges/stateEdges.js +206 -0
- package/dist/graph/edges/testEdges.d.ts +3 -0
- package/dist/graph/edges/testEdges.js +143 -0
- package/dist/graph/edges/utils.d.ts +2 -0
- package/dist/graph/edges/utils.js +25 -0
- package/dist/graph/index.d.ts +6 -0
- package/dist/graph/index.js +65 -0
- package/dist/graph/index.test.d.ts +1 -0
- package/dist/graph/index.test.js +542 -0
- package/dist/graph/thirdPartyLibs.d.ts +8 -0
- package/dist/graph/thirdPartyLibs.js +162 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +15 -0
- package/dist/jobs/index.d.ts +5 -0
- package/dist/jobs/index.js +11 -0
- package/dist/jobs/queue/interface.d.ts +13 -0
- package/dist/jobs/queue/interface.js +1 -0
- package/dist/jobs/queue/memory.d.ts +24 -0
- package/dist/jobs/queue/memory.js +291 -0
- package/dist/jobs/runner.d.ts +3 -0
- package/dist/jobs/runner.js +136 -0
- package/dist/jobs/types.d.ts +112 -0
- package/dist/jobs/types.js +33 -0
- package/dist/parser/directives.d.ts +4 -0
- package/dist/parser/directives.js +31 -0
- package/dist/parser/extractors/components.d.ts +5 -0
- package/dist/parser/extractors/components.js +240 -0
- package/dist/parser/extractors/functions.d.ts +4 -0
- package/dist/parser/extractors/functions.js +240 -0
- package/dist/parser/extractors/hooks.d.ts +4 -0
- package/dist/parser/extractors/hooks.js +128 -0
- package/dist/parser/extractors/stores.d.ts +3 -0
- package/dist/parser/extractors/stores.js +181 -0
- package/dist/parser/index.d.ts +14 -0
- package/dist/parser/index.js +168 -0
- package/dist/parser/index.test.d.ts +1 -0
- package/dist/parser/index.test.js +319 -0
- package/dist/parser/typeUtils.d.ts +9 -0
- package/dist/parser/typeUtils.js +46 -0
- package/dist/pipeline/index.d.ts +50 -0
- package/dist/pipeline/index.js +249 -0
- package/dist/scoring/connectionCounter.d.ts +28 -0
- package/dist/scoring/connectionCounter.js +134 -0
- package/dist/scoring/fileScorer.d.ts +2 -0
- package/dist/scoring/fileScorer.js +44 -0
- package/dist/scoring/index.d.ts +22 -0
- package/dist/scoring/index.js +130 -0
- package/dist/scoring/index.test.d.ts +1 -0
- package/dist/scoring/index.test.js +453 -0
- package/dist/scoring/nodeScorer.d.ts +3 -0
- package/dist/scoring/nodeScorer.js +108 -0
- package/dist/scoring/noiseFilter.d.ts +18 -0
- package/dist/scoring/noiseFilter.js +92 -0
- package/dist/storage/fileStorage.d.ts +117 -0
- package/dist/storage/fileStorage.js +616 -0
- package/dist/storage/index.d.ts +4 -0
- package/dist/storage/index.js +2 -0
- package/dist/storage/interface.d.ts +27 -0
- package/dist/storage/interface.js +1 -0
- package/dist/summarizer/checkpoint.d.ts +15 -0
- package/dist/summarizer/checkpoint.js +110 -0
- package/dist/summarizer/index.d.ts +2 -0
- package/dist/summarizer/index.js +281 -0
- package/dist/summarizer/mapreduce.d.ts +4 -0
- package/dist/summarizer/mapreduce.js +87 -0
- package/dist/summarizer/prompts.d.ts +22 -0
- package/dist/summarizer/prompts.js +205 -0
- package/dist/summarizer/providers/anthropic.d.ts +9 -0
- package/dist/summarizer/providers/anthropic.js +78 -0
- package/dist/summarizer/providers/gemini.d.ts +9 -0
- package/dist/summarizer/providers/gemini.js +79 -0
- package/dist/summarizer/providers/index.d.ts +3 -0
- package/dist/summarizer/providers/index.js +43 -0
- package/dist/summarizer/providers/ollama.d.ts +9 -0
- package/dist/summarizer/providers/ollama.js +23 -0
- package/dist/summarizer/providers/openRouter.d.ts +9 -0
- package/dist/summarizer/providers/openRouter.js +19 -0
- package/dist/summarizer/providers/openai.d.ts +9 -0
- package/dist/summarizer/providers/openai.js +72 -0
- package/dist/summarizer/providers/types.d.ts +32 -0
- package/dist/summarizer/providers/types.js +1 -0
- package/dist/summarizer/retry.d.ts +7 -0
- package/dist/summarizer/retry.js +51 -0
- package/dist/summarizer/topological.d.ts +3 -0
- package/dist/summarizer/topological.js +105 -0
- package/dist/summarizer/types.d.ts +57 -0
- package/dist/summarizer/types.js +17 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.js +1 -0
- package/package.json +48 -0
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import { parseRepo } from "../parser/index.js";
|
|
5
|
+
import { buildLookupMaps } from "./buildLookup.js";
|
|
6
|
+
import { detectCallEdges } from "./edges/callEdges.js";
|
|
7
|
+
import { detectImportEdges } from "./edges/importEdges.js";
|
|
8
|
+
import { detectStateEdges } from "./edges/stateEdges.js";
|
|
9
|
+
import { detectPropEdges } from "./edges/propEdges.js";
|
|
10
|
+
import { detectEventEdges } from "./edges/eventEdges.js";
|
|
11
|
+
import { detectGuardEdges } from "./edges/guardEdges.js";
|
|
12
|
+
import { detectEdges } from "./index.js";
|
|
13
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
14
|
+
function createFakeRepo(files) {
|
|
15
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "devlens-graph-test-"));
|
|
16
|
+
for (const [filePath, content] of Object.entries(files)) {
|
|
17
|
+
const fullPath = path.join(tmpDir, filePath);
|
|
18
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
19
|
+
fs.writeFileSync(fullPath, content);
|
|
20
|
+
}
|
|
21
|
+
return tmpDir;
|
|
22
|
+
}
|
|
23
|
+
function deleteFakeRepo(repoPath) {
|
|
24
|
+
fs.rmSync(repoPath, { recursive: true, force: true });
|
|
25
|
+
}
|
|
26
|
+
function makeFingerprint(overrides = {}) {
|
|
27
|
+
return {
|
|
28
|
+
language: "typescript",
|
|
29
|
+
projectType: "frontend",
|
|
30
|
+
framework: "nextjs",
|
|
31
|
+
router: "app",
|
|
32
|
+
stateManagement: ["zustand"],
|
|
33
|
+
dataFetching: ["fetch"],
|
|
34
|
+
databases: [],
|
|
35
|
+
rawDependencies: {},
|
|
36
|
+
...overrides,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
// Debug helper — prints all nodes found by parser
|
|
40
|
+
// Useful when a test is failing and you want to see what was extracted
|
|
41
|
+
function debugNodes(repoPath) {
|
|
42
|
+
const { nodes } = parseRepo(repoPath);
|
|
43
|
+
console.log("=== DEBUG NODES ===");
|
|
44
|
+
for (const n of nodes) {
|
|
45
|
+
console.log(` ${n.type} | ${n.name} | ${n.filePath}`);
|
|
46
|
+
}
|
|
47
|
+
return nodes;
|
|
48
|
+
}
|
|
49
|
+
// ─── CALLS edges ──────────────────────────────────────────────────────────────
|
|
50
|
+
describe("detectCallEdges", () => {
|
|
51
|
+
it("should detect a direct function call", () => {
|
|
52
|
+
const repoPath = createFakeRepo({
|
|
53
|
+
"src/payment.ts": `
|
|
54
|
+
export function processPayment() {
|
|
55
|
+
validateCard();
|
|
56
|
+
chargeCard();
|
|
57
|
+
}
|
|
58
|
+
export function validateCard() { return true; }
|
|
59
|
+
export function chargeCard() { return true; }
|
|
60
|
+
`,
|
|
61
|
+
});
|
|
62
|
+
const { nodes } = parseRepo(repoPath);
|
|
63
|
+
const lookup = buildLookupMaps(nodes);
|
|
64
|
+
const { edges } = detectCallEdges(nodes, lookup);
|
|
65
|
+
const callsValidate = edges.find((e) => e.type === "CALLS" &&
|
|
66
|
+
e.from.includes("processPayment") &&
|
|
67
|
+
e.to.includes("validateCard"));
|
|
68
|
+
const callsCharge = edges.find((e) => e.type === "CALLS" &&
|
|
69
|
+
e.from.includes("processPayment") &&
|
|
70
|
+
e.to.includes("chargeCard"));
|
|
71
|
+
expect(callsValidate).toBeDefined();
|
|
72
|
+
expect(callsCharge).toBeDefined();
|
|
73
|
+
deleteFakeRepo(repoPath);
|
|
74
|
+
});
|
|
75
|
+
it("should not create edges for external calls", () => {
|
|
76
|
+
const repoPath = createFakeRepo({
|
|
77
|
+
"src/api.ts": `
|
|
78
|
+
export async function fetchData() {
|
|
79
|
+
const res = await fetch('/api/data');
|
|
80
|
+
console.log(res);
|
|
81
|
+
return res.json();
|
|
82
|
+
}
|
|
83
|
+
`,
|
|
84
|
+
});
|
|
85
|
+
const { nodes } = parseRepo(repoPath);
|
|
86
|
+
const lookup = buildLookupMaps(nodes);
|
|
87
|
+
const { edges } = detectCallEdges(nodes, lookup);
|
|
88
|
+
// fetch and console.log are external — no edges expected
|
|
89
|
+
const externalEdge = edges.find((e) => e.to.includes("fetch") || e.to.includes("console"));
|
|
90
|
+
expect(externalEdge).toBeUndefined();
|
|
91
|
+
deleteFakeRepo(repoPath);
|
|
92
|
+
});
|
|
93
|
+
it("should not create self referencing edges", () => {
|
|
94
|
+
const repoPath = createFakeRepo({
|
|
95
|
+
"src/utils.ts": `
|
|
96
|
+
export function factorial(n: number): number {
|
|
97
|
+
if (n <= 1) return 1;
|
|
98
|
+
return n * factorial(n - 1);
|
|
99
|
+
}
|
|
100
|
+
`,
|
|
101
|
+
});
|
|
102
|
+
const { nodes } = parseRepo(repoPath);
|
|
103
|
+
const lookup = buildLookupMaps(nodes);
|
|
104
|
+
const { edges } = detectCallEdges(nodes, lookup);
|
|
105
|
+
const selfEdge = edges.find((e) => e.from === e.to);
|
|
106
|
+
expect(selfEdge).toBeUndefined();
|
|
107
|
+
deleteFakeRepo(repoPath);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
// ─── IMPORTS edges ────────────────────────────────────────────────────────────
|
|
111
|
+
describe("detectImportEdges", () => {
|
|
112
|
+
it("should detect relative imports between files", () => {
|
|
113
|
+
// Both files in same src/ folder so relative import resolves cleanly
|
|
114
|
+
const repoPath = createFakeRepo({
|
|
115
|
+
"src/CheckoutButton.tsx": `
|
|
116
|
+
import { processPayment } from "./PaymentService.js";
|
|
117
|
+
export function CheckoutButton() {
|
|
118
|
+
return <button onClick={processPayment}>Pay</button>;
|
|
119
|
+
}
|
|
120
|
+
`,
|
|
121
|
+
"src/PaymentService.ts": `
|
|
122
|
+
export function processPayment() {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
`,
|
|
126
|
+
});
|
|
127
|
+
const { nodes } = parseRepo(repoPath);
|
|
128
|
+
const lookup = buildLookupMaps(nodes);
|
|
129
|
+
const { edges } = detectImportEdges(lookup, repoPath);
|
|
130
|
+
const importEdge = edges.find((e) => e.type === "IMPORTS" &&
|
|
131
|
+
e.from.includes("CheckoutButton") &&
|
|
132
|
+
e.to.includes("PaymentService"));
|
|
133
|
+
expect(importEdge).toBeDefined();
|
|
134
|
+
deleteFakeRepo(repoPath);
|
|
135
|
+
});
|
|
136
|
+
it("should not create edges for third party imports", () => {
|
|
137
|
+
const repoPath = createFakeRepo({
|
|
138
|
+
"src/Component.tsx": `
|
|
139
|
+
import React from "react";
|
|
140
|
+
import axios from "axios";
|
|
141
|
+
export function Component() {
|
|
142
|
+
return <div />;
|
|
143
|
+
}
|
|
144
|
+
`,
|
|
145
|
+
});
|
|
146
|
+
const { nodes } = parseRepo(repoPath);
|
|
147
|
+
const lookup = buildLookupMaps(nodes);
|
|
148
|
+
const { edges } = detectImportEdges(lookup, repoPath);
|
|
149
|
+
const thirdPartyEdge = edges.find((e) => e.metadata?.importPath === "react" ||
|
|
150
|
+
e.metadata?.importPath === "axios");
|
|
151
|
+
expect(thirdPartyEdge).toBeUndefined();
|
|
152
|
+
deleteFakeRepo(repoPath);
|
|
153
|
+
});
|
|
154
|
+
it("should not create duplicate edges for multiple imports from same file", () => {
|
|
155
|
+
// Both files in same folder — simple relative imports
|
|
156
|
+
const repoPath = createFakeRepo({
|
|
157
|
+
"src/Checkout.tsx": `
|
|
158
|
+
import { processPayment } from "./PaymentService.js";
|
|
159
|
+
import { validateCard } from "./PaymentService.js";
|
|
160
|
+
export function Checkout() {
|
|
161
|
+
return <div />;
|
|
162
|
+
}
|
|
163
|
+
`,
|
|
164
|
+
"src/PaymentService.ts": `
|
|
165
|
+
export function processPayment() { return true; }
|
|
166
|
+
export function validateCard() { return true; }
|
|
167
|
+
`,
|
|
168
|
+
});
|
|
169
|
+
const { nodes } = parseRepo(repoPath);
|
|
170
|
+
const lookup = buildLookupMaps(nodes);
|
|
171
|
+
const { edges } = detectImportEdges(lookup, repoPath);
|
|
172
|
+
// Count edges from Checkout to processPayment — should be exactly 1
|
|
173
|
+
const duplicateEdges = edges.filter((e) => e.type === "IMPORTS" &&
|
|
174
|
+
e.from.includes("Checkout") &&
|
|
175
|
+
e.to.includes("PaymentService"));
|
|
176
|
+
expect(duplicateEdges.length).toBe(1);
|
|
177
|
+
deleteFakeRepo(repoPath);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
// ─── STATE edges ──────────────────────────────────────────────────────────────
|
|
181
|
+
describe("detectStateEdges", () => {
|
|
182
|
+
it("should detect zustand store usage in a component", () => {
|
|
183
|
+
const repoPath = createFakeRepo({
|
|
184
|
+
"src/store.ts": `
|
|
185
|
+
const useCartStore = create((set) => ({
|
|
186
|
+
items: [],
|
|
187
|
+
addItem: (item) => set((state) => ({
|
|
188
|
+
items: [...state.items, item]
|
|
189
|
+
})),
|
|
190
|
+
}));
|
|
191
|
+
`,
|
|
192
|
+
"src/CheckoutButton.tsx": `
|
|
193
|
+
export function CheckoutButton() {
|
|
194
|
+
const items = useCartStore(state => state.items);
|
|
195
|
+
return <div>{items.length}</div>;
|
|
196
|
+
}
|
|
197
|
+
`,
|
|
198
|
+
});
|
|
199
|
+
const { nodes } = parseRepo(repoPath);
|
|
200
|
+
const lookup = buildLookupMaps(nodes);
|
|
201
|
+
const edges = detectStateEdges(nodes, lookup);
|
|
202
|
+
const readsEdge = edges.find((e) => e.type === "READS_FROM" &&
|
|
203
|
+
e.from.includes("CheckoutButton") &&
|
|
204
|
+
e.to.includes("useCartStore"));
|
|
205
|
+
expect(readsEdge).toBeDefined();
|
|
206
|
+
deleteFakeRepo(repoPath);
|
|
207
|
+
});
|
|
208
|
+
it("should detect redux useSelector as READS_FROM", () => {
|
|
209
|
+
const repoPath = createFakeRepo({
|
|
210
|
+
"src/cartSlice.ts": `
|
|
211
|
+
const cartSlice = createSlice({
|
|
212
|
+
name: 'cart',
|
|
213
|
+
initialState: { items: [] },
|
|
214
|
+
reducers: {
|
|
215
|
+
addItem: (state, action) => {
|
|
216
|
+
state.items.push(action.payload);
|
|
217
|
+
},
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
`,
|
|
221
|
+
"src/CartPage.tsx": `
|
|
222
|
+
export function CartPage() {
|
|
223
|
+
const items = useSelector(state => state.cart.items);
|
|
224
|
+
return <div>{items.length}</div>;
|
|
225
|
+
}
|
|
226
|
+
`,
|
|
227
|
+
});
|
|
228
|
+
const { nodes } = parseRepo(repoPath);
|
|
229
|
+
const lookup = buildLookupMaps(nodes);
|
|
230
|
+
const edges = detectStateEdges(nodes, lookup);
|
|
231
|
+
const readsEdge = edges.find((e) => e.type === "READS_FROM" &&
|
|
232
|
+
e.from.includes("CartPage") &&
|
|
233
|
+
e.to.includes("cartSlice"));
|
|
234
|
+
expect(readsEdge).toBeDefined();
|
|
235
|
+
deleteFakeRepo(repoPath);
|
|
236
|
+
});
|
|
237
|
+
it("should detect redux useDispatch as WRITES_TO", () => {
|
|
238
|
+
const repoPath = createFakeRepo({
|
|
239
|
+
"src/cartSlice.ts": `
|
|
240
|
+
const cartSlice = createSlice({
|
|
241
|
+
name: 'cart',
|
|
242
|
+
initialState: { items: [] },
|
|
243
|
+
reducers: {
|
|
244
|
+
addItem: (state, action) => {
|
|
245
|
+
state.items.push(action.payload);
|
|
246
|
+
},
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
`,
|
|
250
|
+
"src/AddToCart.tsx": `
|
|
251
|
+
export function AddToCart() {
|
|
252
|
+
const dispatch = useDispatch();
|
|
253
|
+
return <button onClick={() => dispatch(addItem())}>Add</button>;
|
|
254
|
+
}
|
|
255
|
+
`,
|
|
256
|
+
});
|
|
257
|
+
const { nodes } = parseRepo(repoPath);
|
|
258
|
+
const lookup = buildLookupMaps(nodes);
|
|
259
|
+
const edges = detectStateEdges(nodes, lookup);
|
|
260
|
+
const writesEdge = edges.find((e) => e.type === "WRITES_TO" &&
|
|
261
|
+
e.from.includes("AddToCart") &&
|
|
262
|
+
e.to.includes("cartSlice"));
|
|
263
|
+
expect(writesEdge).toBeDefined();
|
|
264
|
+
deleteFakeRepo(repoPath);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
// ─── PROP_PASS edges ──────────────────────────────────────────────────────────
|
|
268
|
+
describe("detectPropEdges", () => {
|
|
269
|
+
it("should detect prop passing from parent to child", () => {
|
|
270
|
+
const repoPath = createFakeRepo({
|
|
271
|
+
"src/OrderSummary.tsx": `
|
|
272
|
+
export function OrderSummary() {
|
|
273
|
+
const item = { name: "Product" };
|
|
274
|
+
return <CartItem item={item} />;
|
|
275
|
+
}
|
|
276
|
+
`,
|
|
277
|
+
"src/CartItem.tsx": `
|
|
278
|
+
export function CartItem({ item }) {
|
|
279
|
+
return <div>{item.name}</div>;
|
|
280
|
+
}
|
|
281
|
+
`,
|
|
282
|
+
});
|
|
283
|
+
const { nodes } = parseRepo(repoPath);
|
|
284
|
+
const lookup = buildLookupMaps(nodes);
|
|
285
|
+
const edges = detectPropEdges(nodes, lookup, repoPath);
|
|
286
|
+
const propEdge = edges.find((e) => e.type === "PROP_PASS" &&
|
|
287
|
+
e.from.includes("OrderSummary") &&
|
|
288
|
+
e.to.includes("CartItem"));
|
|
289
|
+
expect(propEdge).toBeDefined();
|
|
290
|
+
expect(propEdge?.metadata?.props).toContain("item");
|
|
291
|
+
deleteFakeRepo(repoPath);
|
|
292
|
+
});
|
|
293
|
+
it("should track renderCount when same child rendered multiple times", () => {
|
|
294
|
+
const repoPath = createFakeRepo({
|
|
295
|
+
"src/ProductList.tsx": `
|
|
296
|
+
export function ProductList() {
|
|
297
|
+
return (
|
|
298
|
+
<div>
|
|
299
|
+
<ProductCard product={products[0]} />
|
|
300
|
+
<ProductCard product={products[1]} />
|
|
301
|
+
<ProductCard product={products[2]} />
|
|
302
|
+
</div>
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
`,
|
|
306
|
+
"src/ProductCard.tsx": `
|
|
307
|
+
export function ProductCard({ product }) {
|
|
308
|
+
return <div>{product.name}</div>;
|
|
309
|
+
}
|
|
310
|
+
`,
|
|
311
|
+
});
|
|
312
|
+
const { nodes } = parseRepo(repoPath);
|
|
313
|
+
// Only pass ProductList node — not ProductCard
|
|
314
|
+
// ProductCard doesn't render any JSX children so it won't
|
|
315
|
+
// create any PROP_PASS edges anyway, but filtering here
|
|
316
|
+
// ensures renderCount is only counted from ProductList's body
|
|
317
|
+
const productListOnly = nodes.filter((n) => n.name === "ProductList");
|
|
318
|
+
const lookup = buildLookupMaps(nodes); // full lookup so ProductCard is findable
|
|
319
|
+
const edges = detectPropEdges(productListOnly, lookup, repoPath);
|
|
320
|
+
const propEdge = edges.find((e) => e.type === "PROP_PASS" &&
|
|
321
|
+
e.from.includes("ProductList") &&
|
|
322
|
+
e.to.includes("ProductCard"));
|
|
323
|
+
expect(propEdge?.metadata?.renderCount).toBe(3);
|
|
324
|
+
deleteFakeRepo(repoPath);
|
|
325
|
+
});
|
|
326
|
+
it("should skip HTML native elements", () => {
|
|
327
|
+
const repoPath = createFakeRepo({
|
|
328
|
+
"src/Form.tsx": `
|
|
329
|
+
export function Form() {
|
|
330
|
+
return (
|
|
331
|
+
<div>
|
|
332
|
+
<input type="text" />
|
|
333
|
+
<button>Submit</button>
|
|
334
|
+
</div>
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
`,
|
|
338
|
+
});
|
|
339
|
+
const { nodes } = parseRepo(repoPath);
|
|
340
|
+
const lookup = buildLookupMaps(nodes);
|
|
341
|
+
const edges = detectPropEdges(nodes, lookup, repoPath);
|
|
342
|
+
const nativeEdge = edges.find((e) => e.to.includes("input") ||
|
|
343
|
+
e.to.includes("button") ||
|
|
344
|
+
e.to.includes("div"));
|
|
345
|
+
expect(nativeEdge).toBeUndefined();
|
|
346
|
+
deleteFakeRepo(repoPath);
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
// ─── EVENT edges ──────────────────────────────────────────────────────────────
|
|
350
|
+
describe("detectEventEdges", () => {
|
|
351
|
+
it("should detect custom event emitter and create ghost node", () => {
|
|
352
|
+
const repoPath = createFakeRepo({
|
|
353
|
+
"src/payment.ts": `
|
|
354
|
+
export function processPayment() {
|
|
355
|
+
window.dispatchEvent(new CustomEvent('payment-complete'));
|
|
356
|
+
}
|
|
357
|
+
`,
|
|
358
|
+
});
|
|
359
|
+
const { nodes } = parseRepo(repoPath);
|
|
360
|
+
const lookup = buildLookupMaps(nodes);
|
|
361
|
+
const result = detectEventEdges(lookup, repoPath);
|
|
362
|
+
const emitEdge = result.edges.find((e) => e.type === "EMITS" &&
|
|
363
|
+
e.from.includes("processPayment"));
|
|
364
|
+
const ghostNode = result.ghostNodes.find((n) => n.name === "event:payment-complete");
|
|
365
|
+
expect(emitEdge).toBeDefined();
|
|
366
|
+
expect(ghostNode).toBeDefined();
|
|
367
|
+
deleteFakeRepo(repoPath);
|
|
368
|
+
});
|
|
369
|
+
it("should detect event listener", () => {
|
|
370
|
+
const repoPath = createFakeRepo({
|
|
371
|
+
"src/notifications.ts": `
|
|
372
|
+
export function setupListeners() {
|
|
373
|
+
window.addEventListener('payment-complete', showConfirmation);
|
|
374
|
+
}
|
|
375
|
+
export function showConfirmation() {
|
|
376
|
+
console.log('Payment complete');
|
|
377
|
+
}
|
|
378
|
+
`,
|
|
379
|
+
});
|
|
380
|
+
const { nodes } = parseRepo(repoPath);
|
|
381
|
+
const lookup = buildLookupMaps(nodes);
|
|
382
|
+
const result = detectEventEdges(lookup, repoPath);
|
|
383
|
+
const listenEdge = result.edges.find((e) => e.type === "LISTENS" &&
|
|
384
|
+
e.to.includes("setupListeners"));
|
|
385
|
+
expect(listenEdge).toBeDefined();
|
|
386
|
+
deleteFakeRepo(repoPath);
|
|
387
|
+
});
|
|
388
|
+
it("should reuse ghost node for same event name", () => {
|
|
389
|
+
const repoPath = createFakeRepo({
|
|
390
|
+
"src/payment.ts": `
|
|
391
|
+
export function processPayment() {
|
|
392
|
+
window.dispatchEvent(new CustomEvent('payment-complete'));
|
|
393
|
+
}
|
|
394
|
+
export function setupListeners() {
|
|
395
|
+
window.addEventListener('payment-complete', handlePayment);
|
|
396
|
+
}
|
|
397
|
+
export function handlePayment() {}
|
|
398
|
+
`,
|
|
399
|
+
});
|
|
400
|
+
const { nodes } = parseRepo(repoPath);
|
|
401
|
+
const lookup = buildLookupMaps(nodes);
|
|
402
|
+
const result = detectEventEdges(lookup, repoPath);
|
|
403
|
+
const ghostNodes = result.ghostNodes.filter((n) => n.name === "event:payment-complete");
|
|
404
|
+
// Same event name → exactly one ghost node
|
|
405
|
+
expect(ghostNodes.length).toBe(1);
|
|
406
|
+
deleteFakeRepo(repoPath);
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
// ─── GUARDS edges ─────────────────────────────────────────────────────────────
|
|
410
|
+
describe("detectGuardEdges", () => {
|
|
411
|
+
it("should detect Next.js middleware guards", () => {
|
|
412
|
+
const repoPath = createFakeRepo({
|
|
413
|
+
"middleware.ts": `
|
|
414
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
415
|
+
|
|
416
|
+
export function middleware(request: NextRequest) {
|
|
417
|
+
return NextResponse.next();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export const config = {
|
|
421
|
+
matcher: ['/dashboard/:path*', '/admin/:path*']
|
|
422
|
+
};
|
|
423
|
+
`,
|
|
424
|
+
"app/dashboard/page.tsx": `
|
|
425
|
+
export default function DashboardPage() {
|
|
426
|
+
return <div>Dashboard</div>;
|
|
427
|
+
}
|
|
428
|
+
`,
|
|
429
|
+
"app/admin/page.tsx": `
|
|
430
|
+
export default function AdminPage() {
|
|
431
|
+
return <div>Admin</div>;
|
|
432
|
+
}
|
|
433
|
+
`,
|
|
434
|
+
});
|
|
435
|
+
const { nodes } = parseRepo(repoPath);
|
|
436
|
+
const lookup = buildLookupMaps(nodes);
|
|
437
|
+
const fingerprint = makeFingerprint({ framework: "nextjs" });
|
|
438
|
+
const routeNodes = [
|
|
439
|
+
{
|
|
440
|
+
type: "PAGE",
|
|
441
|
+
urlPath: "/dashboard",
|
|
442
|
+
filePath: path.join(repoPath, "app/dashboard/page.tsx"),
|
|
443
|
+
isDynamic: false,
|
|
444
|
+
isCatchAll: false,
|
|
445
|
+
isGroupRoute: false,
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
type: "PAGE",
|
|
449
|
+
urlPath: "/admin",
|
|
450
|
+
filePath: path.join(repoPath, "app/admin/page.tsx"),
|
|
451
|
+
isDynamic: false,
|
|
452
|
+
isCatchAll: false,
|
|
453
|
+
isGroupRoute: false,
|
|
454
|
+
},
|
|
455
|
+
];
|
|
456
|
+
const edges = detectGuardEdges(nodes, lookup, routeNodes, repoPath, fingerprint);
|
|
457
|
+
const guardsDashboard = edges.find((e) => e.type === "GUARDS" && e.to === "/dashboard");
|
|
458
|
+
const guardsAdmin = edges.find((e) => e.type === "GUARDS" && e.to === "/admin");
|
|
459
|
+
expect(guardsDashboard).toBeDefined();
|
|
460
|
+
expect(guardsAdmin).toBeDefined();
|
|
461
|
+
deleteFakeRepo(repoPath);
|
|
462
|
+
});
|
|
463
|
+
it("should detect Express middleware guards", () => {
|
|
464
|
+
const repoPath = createFakeRepo({
|
|
465
|
+
"src/server.ts": `
|
|
466
|
+
import express from 'express';
|
|
467
|
+
const app = express();
|
|
468
|
+
|
|
469
|
+
export function requireAdmin(req: any, res: any, next: any) {
|
|
470
|
+
next();
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
app.use('/admin', requireAdmin);
|
|
474
|
+
app.get('/admin/users', getUsers);
|
|
475
|
+
`,
|
|
476
|
+
});
|
|
477
|
+
const { nodes } = parseRepo(repoPath);
|
|
478
|
+
const lookup = buildLookupMaps(nodes);
|
|
479
|
+
const fingerprint = makeFingerprint({
|
|
480
|
+
framework: "express",
|
|
481
|
+
projectType: "backend",
|
|
482
|
+
router: "none",
|
|
483
|
+
stateManagement: [],
|
|
484
|
+
dataFetching: [],
|
|
485
|
+
});
|
|
486
|
+
const routeNodes = [
|
|
487
|
+
{
|
|
488
|
+
type: "BACKEND_ROUTE",
|
|
489
|
+
urlPath: "/admin/users",
|
|
490
|
+
filePath: path.join(repoPath, "src/server.ts"),
|
|
491
|
+
httpMethod: "GET",
|
|
492
|
+
framework: "express",
|
|
493
|
+
isDynamic: false,
|
|
494
|
+
handlerName: "getUsers",
|
|
495
|
+
params: [],
|
|
496
|
+
},
|
|
497
|
+
];
|
|
498
|
+
const edges = detectGuardEdges(nodes, lookup, routeNodes, repoPath, fingerprint);
|
|
499
|
+
const guardsAdmin = edges.find((e) => e.type === "GUARDS" && e.to === "/admin/users");
|
|
500
|
+
expect(guardsAdmin).toBeDefined();
|
|
501
|
+
deleteFakeRepo(repoPath);
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
// ─── Full pipeline ────────────────────────────────────────────────────────────
|
|
505
|
+
describe("detectEdges", () => {
|
|
506
|
+
it("should return edges and ghost nodes from full pipeline", () => {
|
|
507
|
+
const repoPath = createFakeRepo({
|
|
508
|
+
"src/payment.ts": `
|
|
509
|
+
export function processPayment() {
|
|
510
|
+
validateCard();
|
|
511
|
+
}
|
|
512
|
+
export function validateCard() { return true; }
|
|
513
|
+
`,
|
|
514
|
+
});
|
|
515
|
+
const { nodes } = parseRepo(repoPath);
|
|
516
|
+
const fingerprint = makeFingerprint();
|
|
517
|
+
const result = detectEdges(nodes, [], repoPath, fingerprint);
|
|
518
|
+
expect(result.edges).toBeDefined();
|
|
519
|
+
expect(result.ghostNodes).toBeDefined();
|
|
520
|
+
expect(Array.isArray(result.edges)).toBe(true);
|
|
521
|
+
expect(Array.isArray(result.ghostNodes)).toBe(true);
|
|
522
|
+
deleteFakeRepo(repoPath);
|
|
523
|
+
});
|
|
524
|
+
it("should detect calls edges in full pipeline", () => {
|
|
525
|
+
const repoPath = createFakeRepo({
|
|
526
|
+
"src/payment.ts": `
|
|
527
|
+
export function processPayment() {
|
|
528
|
+
validateCard();
|
|
529
|
+
}
|
|
530
|
+
export function validateCard() { return true; }
|
|
531
|
+
`,
|
|
532
|
+
});
|
|
533
|
+
const { nodes } = parseRepo(repoPath);
|
|
534
|
+
const fingerprint = makeFingerprint();
|
|
535
|
+
const result = detectEdges(nodes, [], repoPath, fingerprint);
|
|
536
|
+
const callEdge = result.edges.find((e) => e.type === "CALLS" &&
|
|
537
|
+
e.from.includes("processPayment") &&
|
|
538
|
+
e.to.includes("validateCard"));
|
|
539
|
+
expect(callEdge).toBeDefined();
|
|
540
|
+
deleteFakeRepo(repoPath);
|
|
541
|
+
});
|
|
542
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CodeNode } from "../types.js";
|
|
2
|
+
export declare function readPackageDependencies(repoPath: string): {
|
|
3
|
+
dependencies: Record<string, string>;
|
|
4
|
+
devDependencies: Record<string, string>;
|
|
5
|
+
};
|
|
6
|
+
export declare function categorizeLibrary(packageName: string, isDev: boolean): "runtime" | "ui" | "devtool" | "unknown";
|
|
7
|
+
export declare function extractPackageName(importSpecifier: string): string;
|
|
8
|
+
export declare function buildThirdPartyNodes(repoPath: string, includedLibs: string[]): CodeNode[];
|