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,10 @@
|
|
|
1
|
+
import type { CodeNode } from "../types.js";
|
|
2
|
+
export interface LookupMaps {
|
|
3
|
+
nodesByName: Map<string, CodeNode[]>;
|
|
4
|
+
nodesByFile: Map<string, CodeNode[]>;
|
|
5
|
+
fileNodesByPath: Map<string, CodeNode>;
|
|
6
|
+
storeNodes: CodeNode[];
|
|
7
|
+
thirdPartyNodesByName: Map<string, CodeNode>;
|
|
8
|
+
thirdPartyImportAliases: Map<string, Map<string, string>>;
|
|
9
|
+
}
|
|
10
|
+
export declare function buildLookupMaps(codeNodes: CodeNode[]): LookupMaps;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function buildLookupMaps(codeNodes) {
|
|
2
|
+
const nodesByName = new Map();
|
|
3
|
+
const nodesByFile = new Map();
|
|
4
|
+
const fileNodesByPath = new Map();
|
|
5
|
+
const storeNodes = [];
|
|
6
|
+
const thirdPartyNodesByName = new Map();
|
|
7
|
+
const thirdPartyImportAliases = new Map();
|
|
8
|
+
for (const node of codeNodes) {
|
|
9
|
+
if (node.type === "THIRD_PARTY") {
|
|
10
|
+
thirdPartyNodesByName.set(node.name, node);
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
// FILE nodes go into their own dedicated map — kept separate so other
|
|
14
|
+
// detectors (guards, call edges, etc.) only see function/component nodes
|
|
15
|
+
if (["FILE", "TEST", "STORY"].includes(node.type)) {
|
|
16
|
+
fileNodesByPath.set(node.filePath, node);
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (!nodesByName.has(node.name)) {
|
|
20
|
+
nodesByName.set(node.name, []);
|
|
21
|
+
}
|
|
22
|
+
nodesByName.get(node.name).push(node);
|
|
23
|
+
if (!nodesByFile.has(node.filePath)) {
|
|
24
|
+
nodesByFile.set(node.filePath, []);
|
|
25
|
+
}
|
|
26
|
+
nodesByFile.get(node.filePath).push(node);
|
|
27
|
+
if (node.type === "STATE_STORE") {
|
|
28
|
+
storeNodes.push(node);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return { nodesByName, nodesByFile, fileNodesByPath, storeNodes, thirdPartyNodesByName, thirdPartyImportAliases };
|
|
32
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CodeEdge, CodeNode } from "../../types.js";
|
|
2
|
+
import type { LookupMaps } from "../buildLookup.js";
|
|
3
|
+
export interface CallEdgeResult {
|
|
4
|
+
edges: CodeEdge[];
|
|
5
|
+
newThirdPartyNodes: CodeNode[];
|
|
6
|
+
}
|
|
7
|
+
export declare function detectCallEdges(nodes: CodeNode[], lookupMp: LookupMaps): CallEdgeResult;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { closestByPath } from "./utils.js";
|
|
2
|
+
export function detectCallEdges(nodes, lookupMp) {
|
|
3
|
+
const edges = [];
|
|
4
|
+
// Dedup for THIRD_PARTY CALLS edges — one per (caller, target) pair
|
|
5
|
+
const createdThirdPartyEdges = new Set();
|
|
6
|
+
// Accumulate resolved calls per node — written back to metadata after edge loop
|
|
7
|
+
const resolvedCallsMap = new Map();
|
|
8
|
+
// Lazily-created method nodes for default/namespace import member-access calls
|
|
9
|
+
const createdMethodNodes = new Map();
|
|
10
|
+
for (const node of nodes) {
|
|
11
|
+
// Only functions, hooks, and components make direct calls
|
|
12
|
+
if (node.type !== "FUNCTION" &&
|
|
13
|
+
node.type !== "HOOK" &&
|
|
14
|
+
node.type !== "COMPONENT")
|
|
15
|
+
continue;
|
|
16
|
+
const calls = node.metadata.calls;
|
|
17
|
+
const uses = node.metadata.uses;
|
|
18
|
+
const hookCalls = node.metadata.hookCalls;
|
|
19
|
+
const dependencies = node.metadata.dependencies;
|
|
20
|
+
const hooks = node.metadata.hooks;
|
|
21
|
+
// Primary call list determines the edge type
|
|
22
|
+
const primaryNames = calls ?? uses;
|
|
23
|
+
const edgeType = calls ? "CALLS" : "USES";
|
|
24
|
+
// Hook / dependency names are checked for third-party edges only —
|
|
25
|
+
// local hook-to-hook edges are already handled by hookEdges.ts.
|
|
26
|
+
const hookNames = [
|
|
27
|
+
...(hookCalls ?? []),
|
|
28
|
+
...(dependencies ?? []),
|
|
29
|
+
...(hooks ?? []),
|
|
30
|
+
];
|
|
31
|
+
const hasPrimary = primaryNames && primaryNames.length > 0;
|
|
32
|
+
const hasHooks = hookNames.length > 0;
|
|
33
|
+
if (!hasPrimary && !hasHooks)
|
|
34
|
+
continue;
|
|
35
|
+
// ── Primary names: both third-party and local edges ──────────────
|
|
36
|
+
for (const calledName of (primaryNames ?? [])) {
|
|
37
|
+
// ── Third-party guard ─────────────────────────────────────────
|
|
38
|
+
// The alias map is keyed by node.filePath (relative) and populated
|
|
39
|
+
// by importEdges.ts (which runs first).
|
|
40
|
+
const fileAliasMap = lookupMp.thirdPartyImportAliases.get(node.filePath);
|
|
41
|
+
if (fileAliasMap) {
|
|
42
|
+
const rootName = calledName.split(".")[0];
|
|
43
|
+
let tpNodeId = fileAliasMap.get(calledName) ?? fileAliasMap.get(rootName);
|
|
44
|
+
if (tpNodeId) {
|
|
45
|
+
// When the alias resolved to a package node (default/namespace import)
|
|
46
|
+
// AND the calledName is a member-access expression like "axios.get",
|
|
47
|
+
// create a more granular per-method node.
|
|
48
|
+
const isPackageNode = !tpNodeId.includes("::");
|
|
49
|
+
const hasMemberAccess = calledName.includes(".");
|
|
50
|
+
if (isPackageNode && hasMemberAccess) {
|
|
51
|
+
const methodSuffix = calledName.slice(rootName.length + 1); // "get" from "axios.get"
|
|
52
|
+
const methodNodeId = `${tpNodeId}::${methodSuffix}`;
|
|
53
|
+
if (!createdMethodNodes.has(methodNodeId)) {
|
|
54
|
+
const pkgName = tpNodeId.replace(/^\[npm\]\//, "");
|
|
55
|
+
const pkgNode = lookupMp.thirdPartyNodesByName.get(pkgName);
|
|
56
|
+
createdMethodNodes.set(methodNodeId, {
|
|
57
|
+
id: methodNodeId,
|
|
58
|
+
name: `${pkgName}.${methodSuffix}`,
|
|
59
|
+
type: "THIRD_PARTY",
|
|
60
|
+
filePath: tpNodeId,
|
|
61
|
+
startLine: 0,
|
|
62
|
+
endLine: 0,
|
|
63
|
+
rawCode: undefined,
|
|
64
|
+
codeHash: undefined,
|
|
65
|
+
metadata: {
|
|
66
|
+
isThirdParty: true,
|
|
67
|
+
packageVersion: pkgNode?.metadata.packageVersion ?? "unknown",
|
|
68
|
+
category: pkgNode?.metadata.category ?? "unknown",
|
|
69
|
+
parentPackageId: tpNodeId,
|
|
70
|
+
methodName: methodSuffix,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
// Cache in the alias map so subsequent lookups for the same
|
|
75
|
+
// expression skip re-creation.
|
|
76
|
+
fileAliasMap.set(calledName, methodNodeId);
|
|
77
|
+
tpNodeId = methodNodeId;
|
|
78
|
+
}
|
|
79
|
+
const edgeKey = `${node.id}→${tpNodeId}:CALLS`;
|
|
80
|
+
if (!createdThirdPartyEdges.has(edgeKey)) {
|
|
81
|
+
createdThirdPartyEdges.add(edgeKey);
|
|
82
|
+
edges.push({
|
|
83
|
+
from: node.id,
|
|
84
|
+
to: tpNodeId,
|
|
85
|
+
type: "CALLS",
|
|
86
|
+
metadata: { calledName, isThirdParty: true },
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Skip non-third-party member-access calls (console.log, Math.round, etc.)
|
|
93
|
+
if (calledName.includes("."))
|
|
94
|
+
continue;
|
|
95
|
+
// ── Local node lookup ─────────────────────────────────────────
|
|
96
|
+
const targets = lookupMp.nodesByName.get(calledName);
|
|
97
|
+
if (!targets || targets.length === 0)
|
|
98
|
+
continue;
|
|
99
|
+
const target = targets.length === 1
|
|
100
|
+
? targets[0]
|
|
101
|
+
: closestByPath(targets, node.filePath);
|
|
102
|
+
if (target.id === node.id)
|
|
103
|
+
continue; // skip self-reference
|
|
104
|
+
edges.push({
|
|
105
|
+
from: node.id,
|
|
106
|
+
to: target.id,
|
|
107
|
+
type: edgeType,
|
|
108
|
+
metadata: { calledName },
|
|
109
|
+
});
|
|
110
|
+
if (!resolvedCallsMap.has(node.id))
|
|
111
|
+
resolvedCallsMap.set(node.id, []);
|
|
112
|
+
const existing = resolvedCallsMap.get(node.id);
|
|
113
|
+
if (!existing.some(r => r.nodeId === target.id)) {
|
|
114
|
+
existing.push({ name: calledName, nodeId: target.id });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// ── Hook / dependency names: third-party edges only ──────────────
|
|
118
|
+
if (hookNames.length > 0) {
|
|
119
|
+
const fileAliasMap = lookupMp.thirdPartyImportAliases.get(node.filePath);
|
|
120
|
+
if (fileAliasMap) {
|
|
121
|
+
for (const hookName of hookNames) {
|
|
122
|
+
const rootName = hookName.split(".")[0];
|
|
123
|
+
const tpNodeId = fileAliasMap.get(hookName) ?? fileAliasMap.get(rootName);
|
|
124
|
+
if (!tpNodeId)
|
|
125
|
+
continue;
|
|
126
|
+
const edgeKey = `${node.id}→${tpNodeId}:CALLS`;
|
|
127
|
+
if (!createdThirdPartyEdges.has(edgeKey)) {
|
|
128
|
+
createdThirdPartyEdges.add(edgeKey);
|
|
129
|
+
edges.push({
|
|
130
|
+
from: node.id,
|
|
131
|
+
to: tpNodeId,
|
|
132
|
+
type: "CALLS",
|
|
133
|
+
metadata: { calledName: hookName, isThirdParty: true },
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Write resolved calls back onto nodes
|
|
141
|
+
for (const node of nodes) {
|
|
142
|
+
node.metadata.resolvedCalls = resolvedCallsMap.get(node.id) ?? [];
|
|
143
|
+
}
|
|
144
|
+
return { edges, newThirdPartyNodes: [...createdMethodNodes.values()] };
|
|
145
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CodeNode, CodeEdge } from "../../types.js";
|
|
2
|
+
import type { LookupMaps } from "../buildLookup.js";
|
|
3
|
+
export interface EventEdgeResult {
|
|
4
|
+
edges: CodeEdge[];
|
|
5
|
+
ghostNodes: CodeNode[];
|
|
6
|
+
}
|
|
7
|
+
export declare function detectEventEdges(lookup: LookupMaps, repoPath: string): EventEdgeResult;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { Project, SyntaxKind } from "ts-morph";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
// Patterns that indicate an event is being emitted
|
|
5
|
+
const EMITTER_PATTERNS = [
|
|
6
|
+
"dispatchEvent",
|
|
7
|
+
".emit",
|
|
8
|
+
];
|
|
9
|
+
// Patterns that indicate an event is being listened to
|
|
10
|
+
const LISTENER_PATTERNS = [
|
|
11
|
+
"addEventListener",
|
|
12
|
+
".on",
|
|
13
|
+
".once",
|
|
14
|
+
];
|
|
15
|
+
// Recursively walk directory and add files to project
|
|
16
|
+
function addFilesRecursively(dir, project) {
|
|
17
|
+
const IGNORE_DIRS = [
|
|
18
|
+
"node_modules", "dist", "build",
|
|
19
|
+
".next", "coverage", ".git",
|
|
20
|
+
];
|
|
21
|
+
let entries;
|
|
22
|
+
try {
|
|
23
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
const fullPath = path.join(dir, entry.name);
|
|
30
|
+
if (entry.isDirectory()) {
|
|
31
|
+
if (IGNORE_DIRS.includes(entry.name))
|
|
32
|
+
continue;
|
|
33
|
+
addFilesRecursively(fullPath, project);
|
|
34
|
+
}
|
|
35
|
+
else if (entry.isFile()) {
|
|
36
|
+
if (!/\.(ts|tsx|js|jsx)$/.test(entry.name))
|
|
37
|
+
continue;
|
|
38
|
+
if (/\.(test|spec)\.(ts|tsx|js|jsx)$/.test(entry.name))
|
|
39
|
+
continue;
|
|
40
|
+
if (/\.d\.ts$/.test(entry.name))
|
|
41
|
+
continue;
|
|
42
|
+
project.addSourceFileAtPath(fullPath);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Creates a ghost node for an event
|
|
47
|
+
function createGhostNode(eventName) {
|
|
48
|
+
return {
|
|
49
|
+
id: `ghost::event:${eventName}`,
|
|
50
|
+
name: `event:${eventName}`,
|
|
51
|
+
type: "GHOST",
|
|
52
|
+
filePath: "",
|
|
53
|
+
startLine: 0,
|
|
54
|
+
endLine: 0,
|
|
55
|
+
metadata: {
|
|
56
|
+
ghostType: "event",
|
|
57
|
+
eventName,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
// Extracts event name from a dispatchEvent call
|
|
62
|
+
// window.dispatchEvent(new CustomEvent('payment-complete'))
|
|
63
|
+
// We need to go one level deeper into CustomEvent arguments
|
|
64
|
+
function extractDispatchEventName(call) {
|
|
65
|
+
const args = call.getArguments?.();
|
|
66
|
+
if (!args || args.length === 0)
|
|
67
|
+
return null;
|
|
68
|
+
const firstArg = args[0];
|
|
69
|
+
const firstArgText = firstArg.getText();
|
|
70
|
+
// Must be new CustomEvent('...')
|
|
71
|
+
if (!firstArgText.startsWith("new CustomEvent"))
|
|
72
|
+
return null;
|
|
73
|
+
// Get arguments of CustomEvent constructor
|
|
74
|
+
const newExpression = firstArg.asKind(SyntaxKind.NewExpression);
|
|
75
|
+
if (!newExpression)
|
|
76
|
+
return null;
|
|
77
|
+
const customEventArgs = newExpression.getArguments();
|
|
78
|
+
if (customEventArgs.length === 0)
|
|
79
|
+
return null;
|
|
80
|
+
const eventNameArg = customEventArgs[0].getText();
|
|
81
|
+
// Remove surrounding quotes
|
|
82
|
+
return eventNameArg.replace(/^['"`]|['"`]$/g, "");
|
|
83
|
+
}
|
|
84
|
+
// Extracts event name from emit/addEventListener/on/once calls
|
|
85
|
+
// eventEmitter.emit('payment-complete', data)
|
|
86
|
+
// window.addEventListener('payment-complete', handler)
|
|
87
|
+
// First argument is always the event name
|
|
88
|
+
function extractEventName(call) {
|
|
89
|
+
const args = call.getArguments?.();
|
|
90
|
+
if (!args || args.length === 0)
|
|
91
|
+
return null;
|
|
92
|
+
const firstArg = args[0].getText();
|
|
93
|
+
// Must be a string literal
|
|
94
|
+
if (!firstArg.startsWith("'") &&
|
|
95
|
+
!firstArg.startsWith('"') &&
|
|
96
|
+
!firstArg.startsWith("`"))
|
|
97
|
+
return null;
|
|
98
|
+
return firstArg.replace(/^['"`]|['"`]$/g, "");
|
|
99
|
+
}
|
|
100
|
+
// Walks up the AST from a node to find the containing function name
|
|
101
|
+
// Returns the function name or null if not found
|
|
102
|
+
function findContainingFunctionName(node) {
|
|
103
|
+
let current = node.getParent();
|
|
104
|
+
while (current) {
|
|
105
|
+
// Check if current node is any kind of function
|
|
106
|
+
if (current.getKind() === SyntaxKind.FunctionDeclaration ||
|
|
107
|
+
current.getKind() === SyntaxKind.FunctionExpression ||
|
|
108
|
+
current.getKind() === SyntaxKind.ArrowFunction ||
|
|
109
|
+
current.getKind() === SyntaxKind.MethodDeclaration) {
|
|
110
|
+
// Try to get the name
|
|
111
|
+
const asFuncDecl = current.asKind(SyntaxKind.FunctionDeclaration);
|
|
112
|
+
if (asFuncDecl)
|
|
113
|
+
return asFuncDecl.getName() ?? null;
|
|
114
|
+
const asMethod = current.asKind(SyntaxKind.MethodDeclaration);
|
|
115
|
+
if (asMethod)
|
|
116
|
+
return asMethod.getName() ?? null;
|
|
117
|
+
// For arrow functions and function expressions
|
|
118
|
+
// the name comes from the variable they are assigned to
|
|
119
|
+
const parent = current.getParent();
|
|
120
|
+
if (parent?.getKind() === SyntaxKind.VariableDeclaration) {
|
|
121
|
+
return parent.getName?.() ?? null;
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
current = current.getParent();
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
//MAIN FUNCTION
|
|
130
|
+
export function detectEventEdges(lookup, repoPath) {
|
|
131
|
+
const edges = [];
|
|
132
|
+
const ghostNodes = [];
|
|
133
|
+
// Track ghost nodes by event name to avoid duplicates
|
|
134
|
+
const ghostsByEventName = new Map();
|
|
135
|
+
const project = new Project({
|
|
136
|
+
compilerOptions: {
|
|
137
|
+
allowJs: true,
|
|
138
|
+
checkJs: false,
|
|
139
|
+
jsx: 4,
|
|
140
|
+
strict: false,
|
|
141
|
+
},
|
|
142
|
+
skipAddingFilesFromTsConfig: true,
|
|
143
|
+
});
|
|
144
|
+
addFilesRecursively(repoPath, project);
|
|
145
|
+
for (const file of project.getSourceFiles()) {
|
|
146
|
+
const callExpressions = file.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
147
|
+
for (const call of callExpressions) {
|
|
148
|
+
const expressionText = call.getExpression().getText();
|
|
149
|
+
// ─── Determine if emitter or listener ─────────────────────────────────
|
|
150
|
+
const isEmitter = EMITTER_PATTERNS.some((p) => expressionText.includes(p));
|
|
151
|
+
const isListener = LISTENER_PATTERNS.some((p) => expressionText.includes(p));
|
|
152
|
+
if (!isEmitter && !isListener)
|
|
153
|
+
continue;
|
|
154
|
+
// ─── Extract event name ────────────────────────────────────────────────
|
|
155
|
+
let eventName = null;
|
|
156
|
+
if (expressionText.includes("dispatchEvent")) {
|
|
157
|
+
// dispatchEvent wraps CustomEvent — need to go deeper
|
|
158
|
+
eventName = extractDispatchEventName(call);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
// emit / addEventListener / on / once — first arg is event name
|
|
162
|
+
eventName = extractEventName(call);
|
|
163
|
+
}
|
|
164
|
+
if (!eventName)
|
|
165
|
+
continue;
|
|
166
|
+
// ─── Find containing function ──────────────────────────────────────────
|
|
167
|
+
const containingName = findContainingFunctionName(call);
|
|
168
|
+
if (!containingName)
|
|
169
|
+
continue;
|
|
170
|
+
// Look up containing function in our extracted nodes
|
|
171
|
+
const containingNodes = lookup.nodesByName.get(containingName);
|
|
172
|
+
if (!containingNodes || containingNodes.length === 0)
|
|
173
|
+
continue;
|
|
174
|
+
// Use first match — same name in multiple files is rare for event handlers
|
|
175
|
+
const containingNode = containingNodes[0];
|
|
176
|
+
// ─── Get or create ghost node ──────────────────────────────────────────
|
|
177
|
+
let ghostNode = ghostsByEventName.get(eventName);
|
|
178
|
+
if (!ghostNode) {
|
|
179
|
+
ghostNode = createGhostNode(eventName);
|
|
180
|
+
ghostsByEventName.set(eventName, ghostNode);
|
|
181
|
+
ghostNodes.push(ghostNode);
|
|
182
|
+
}
|
|
183
|
+
// ─── Create edge ───────────────────────────────────────────────────────
|
|
184
|
+
if (isEmitter) {
|
|
185
|
+
edges.push({
|
|
186
|
+
from: containingNode.id,
|
|
187
|
+
to: ghostNode.id,
|
|
188
|
+
type: "EMITS",
|
|
189
|
+
metadata: { eventName },
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
if (isListener) {
|
|
193
|
+
edges.push({
|
|
194
|
+
from: ghostNode.id,
|
|
195
|
+
to: containingNode.id,
|
|
196
|
+
type: "LISTENS",
|
|
197
|
+
metadata: { eventName },
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return { edges, ghostNodes };
|
|
203
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { CodeNode, CodeEdge, RouteNode, BackendRouteNode, ProjectFingerprint } from "../../types.js";
|
|
2
|
+
import type { LookupMaps } from "../buildLookup.js";
|
|
3
|
+
export declare function detectGuardEdges(nodes: CodeNode[], lookup: LookupMaps, routeNodes: (RouteNode | BackendRouteNode)[], repoPath: string, fingerprint: ProjectFingerprint): CodeEdge[];
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { Project, SyntaxKind } from "ts-morph";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
// Recursively walk directory and add files to project
|
|
5
|
+
function addFilesRecursively(dir, project) {
|
|
6
|
+
const IGNORE_DIRS = [
|
|
7
|
+
"node_modules", "dist", "build",
|
|
8
|
+
".next", "coverage", ".git",
|
|
9
|
+
];
|
|
10
|
+
let entries;
|
|
11
|
+
try {
|
|
12
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
const fullPath = path.join(dir, entry.name);
|
|
19
|
+
if (entry.isDirectory()) {
|
|
20
|
+
if (IGNORE_DIRS.includes(entry.name))
|
|
21
|
+
continue;
|
|
22
|
+
addFilesRecursively(fullPath, project);
|
|
23
|
+
}
|
|
24
|
+
else if (entry.isFile()) {
|
|
25
|
+
if (!/\.(ts|tsx|js|jsx)$/.test(entry.name))
|
|
26
|
+
continue;
|
|
27
|
+
if (/\.(test|spec)\.(ts|tsx|js|jsx)$/.test(entry.name))
|
|
28
|
+
continue;
|
|
29
|
+
if (/\.d\.ts$/.test(entry.name))
|
|
30
|
+
continue;
|
|
31
|
+
project.addSourceFileAtPath(fullPath);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Cleans a Next.js matcher pattern to a base path for matching
|
|
36
|
+
// "/dashboard/:path*" → "/dashboard"
|
|
37
|
+
// "/admin" → "/admin"
|
|
38
|
+
function cleanMatcherPattern(pattern) {
|
|
39
|
+
return pattern
|
|
40
|
+
.replace(/\/:path\*/g, "") // remove /:path*
|
|
41
|
+
.replace(/\/:[^/]+/g, "") // remove any :param segments
|
|
42
|
+
.replace(/\*/g, "") // remove wildcards
|
|
43
|
+
.replace(/\/$/, "") // remove trailing slash
|
|
44
|
+
|| "/"; // fallback to root
|
|
45
|
+
}
|
|
46
|
+
// Checks if a route path is protected by a guard pattern
|
|
47
|
+
function routeMatchesPattern(routePath, guardPattern) {
|
|
48
|
+
if (guardPattern === "/")
|
|
49
|
+
return true; // guards all routes
|
|
50
|
+
return routePath === guardPattern || routePath.startsWith(guardPattern + "/");
|
|
51
|
+
}
|
|
52
|
+
// ─── Next.js Guard Detection ──────────────────────────────────────────────────
|
|
53
|
+
function detectNextjsGuards(lookup, routeNodes, repoPath, project) {
|
|
54
|
+
const edges = [];
|
|
55
|
+
// Find middleware file — could be at root or inside src/
|
|
56
|
+
const possiblePaths = [
|
|
57
|
+
path.join(repoPath, "middleware.ts"),
|
|
58
|
+
path.join(repoPath, "middleware.js"),
|
|
59
|
+
path.join(repoPath, "src", "middleware.ts"),
|
|
60
|
+
path.join(repoPath, "src", "middleware.js"),
|
|
61
|
+
];
|
|
62
|
+
const middlewarePath = possiblePaths.find((p) => fs.existsSync(p));
|
|
63
|
+
if (!middlewarePath)
|
|
64
|
+
return edges;
|
|
65
|
+
// Find the middleware node in our extracted nodes.
|
|
66
|
+
// nodesByFile is now keyed by relative forward-slash paths (same as node.filePath).
|
|
67
|
+
const relativeMiddlewarePath = path.relative(repoPath, middlewarePath).replace(/\\/g, "/");
|
|
68
|
+
const middlewareNodes = lookup.nodesByFile.get(relativeMiddlewarePath);
|
|
69
|
+
if (!middlewareNodes || middlewareNodes.length === 0)
|
|
70
|
+
return edges;
|
|
71
|
+
// Use the first node from middleware file as the source
|
|
72
|
+
const middlewareNode = middlewareNodes[0];
|
|
73
|
+
// Open middleware file with ts-morph
|
|
74
|
+
const sourceFile = project.getSourceFile(middlewarePath);
|
|
75
|
+
if (!sourceFile)
|
|
76
|
+
return edges;
|
|
77
|
+
// Find the config variable declaration
|
|
78
|
+
// export const config = { matcher: [...] }
|
|
79
|
+
const configVariable = sourceFile
|
|
80
|
+
.getVariableDeclarations()
|
|
81
|
+
.find((v) => v.getName() === "config");
|
|
82
|
+
if (!configVariable)
|
|
83
|
+
return edges;
|
|
84
|
+
const initializer = configVariable.getInitializer();
|
|
85
|
+
if (!initializer)
|
|
86
|
+
return edges;
|
|
87
|
+
// Find the matcher array inside config object
|
|
88
|
+
const objLiteral = initializer.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
89
|
+
if (!objLiteral)
|
|
90
|
+
return edges;
|
|
91
|
+
for (const prop of objLiteral.getProperties()) {
|
|
92
|
+
const propName = prop.getName?.();
|
|
93
|
+
if (propName !== "matcher")
|
|
94
|
+
continue;
|
|
95
|
+
const propInitializer = prop.getInitializer?.();
|
|
96
|
+
if (!propInitializer)
|
|
97
|
+
continue;
|
|
98
|
+
// matcher can be a single string or an array
|
|
99
|
+
// matcher: '/dashboard'
|
|
100
|
+
// matcher: ['/dashboard', '/admin']
|
|
101
|
+
const patterns = [];
|
|
102
|
+
if (propInitializer.getKind() === SyntaxKind.StringLiteral) {
|
|
103
|
+
// Single string pattern
|
|
104
|
+
patterns.push(propInitializer.getText().replace(/^['"`]|['"`]$/g, ""));
|
|
105
|
+
}
|
|
106
|
+
else if (propInitializer.getKind() === SyntaxKind.ArrayLiteralExpression) {
|
|
107
|
+
// Array of patterns
|
|
108
|
+
const arrayLiteral = propInitializer.asKind(SyntaxKind.ArrayLiteralExpression);
|
|
109
|
+
if (!arrayLiteral)
|
|
110
|
+
continue;
|
|
111
|
+
for (const element of arrayLiteral.getElements()) {
|
|
112
|
+
const text = element.getText().replace(/^['"`]|['"`]$/g, "");
|
|
113
|
+
patterns.push(text);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// For each pattern find matching route nodes
|
|
117
|
+
for (const pattern of patterns) {
|
|
118
|
+
const basePath = cleanMatcherPattern(pattern);
|
|
119
|
+
for (const routeNode of routeNodes) {
|
|
120
|
+
if (routeNode.type === "MIDDLEWARE")
|
|
121
|
+
continue;
|
|
122
|
+
if (routeMatchesPattern(routeNode.urlPath, basePath)) {
|
|
123
|
+
edges.push({
|
|
124
|
+
from: middlewareNode.id,
|
|
125
|
+
to: routeNode.urlPath,
|
|
126
|
+
type: "GUARDS",
|
|
127
|
+
metadata: {
|
|
128
|
+
pattern,
|
|
129
|
+
guardedPath: routeNode.urlPath,
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return edges;
|
|
137
|
+
}
|
|
138
|
+
// ─── Express/Fastify/Koa Guard Detection ──────────────────────────────────────
|
|
139
|
+
function detectBackendGuards(lookup, routeNodes, project) {
|
|
140
|
+
const edges = [];
|
|
141
|
+
// Only look at backend route nodes
|
|
142
|
+
const backendRoutes = routeNodes.filter((r) => r.type === "BACKEND_ROUTE");
|
|
143
|
+
for (const file of project.getSourceFiles()) {
|
|
144
|
+
const callExpressions = file.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
145
|
+
for (const call of callExpressions) {
|
|
146
|
+
const expressionText = call.getExpression().getText();
|
|
147
|
+
// Only look for app.use() / router.use() calls
|
|
148
|
+
if (!expressionText.endsWith(".use"))
|
|
149
|
+
continue;
|
|
150
|
+
const args = call.getArguments();
|
|
151
|
+
if (args.length === 0)
|
|
152
|
+
continue;
|
|
153
|
+
let guardPath = "/"; // default — guards all routes
|
|
154
|
+
let middlewareName;
|
|
155
|
+
if (args.length === 1) {
|
|
156
|
+
// app.use(middlewareName) — no path, guards all routes
|
|
157
|
+
const argText = args[0].getText();
|
|
158
|
+
if (!argText.includes("=>") &&
|
|
159
|
+
!argText.includes("function") &&
|
|
160
|
+
/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(argText)) {
|
|
161
|
+
middlewareName = argText;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
else if (args.length >= 2) {
|
|
165
|
+
// app.use('/admin', middlewareName)
|
|
166
|
+
const firstArgText = args[0].getText();
|
|
167
|
+
// First arg must be a string path
|
|
168
|
+
if (firstArgText.startsWith("'") ||
|
|
169
|
+
firstArgText.startsWith('"') ||
|
|
170
|
+
firstArgText.startsWith("`")) {
|
|
171
|
+
guardPath = firstArgText.replace(/^['"`]|['"`]$/g, "");
|
|
172
|
+
// Last arg is the middleware function
|
|
173
|
+
const lastArgText = args[args.length - 1].getText();
|
|
174
|
+
if (!lastArgText.includes("=>") &&
|
|
175
|
+
!lastArgText.includes("function") &&
|
|
176
|
+
/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(lastArgText)) {
|
|
177
|
+
middlewareName = lastArgText;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (!middlewareName)
|
|
182
|
+
continue;
|
|
183
|
+
// Look up middleware in our extracted nodes
|
|
184
|
+
const middlewareNodes = lookup.nodesByName.get(middlewareName);
|
|
185
|
+
if (!middlewareNodes || middlewareNodes.length === 0)
|
|
186
|
+
continue;
|
|
187
|
+
const middlewareNode = middlewareNodes[0];
|
|
188
|
+
// Find matching backend routes
|
|
189
|
+
for (const route of backendRoutes) {
|
|
190
|
+
if (routeMatchesPattern(route.urlPath, guardPath)) {
|
|
191
|
+
edges.push({
|
|
192
|
+
from: middlewareNode.id,
|
|
193
|
+
to: route.urlPath,
|
|
194
|
+
type: "GUARDS",
|
|
195
|
+
metadata: {
|
|
196
|
+
guardedPath: route.urlPath,
|
|
197
|
+
httpMethod: route.httpMethod,
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return edges;
|
|
205
|
+
}
|
|
206
|
+
// ─── Main Export ──────────────────────────────────────────────────────────────
|
|
207
|
+
export function detectGuardEdges(nodes, lookup, routeNodes, repoPath, fingerprint) {
|
|
208
|
+
const project = new Project({
|
|
209
|
+
compilerOptions: {
|
|
210
|
+
allowJs: true,
|
|
211
|
+
checkJs: false,
|
|
212
|
+
jsx: 4,
|
|
213
|
+
strict: false,
|
|
214
|
+
},
|
|
215
|
+
skipAddingFilesFromTsConfig: true,
|
|
216
|
+
});
|
|
217
|
+
addFilesRecursively(repoPath, project);
|
|
218
|
+
const edges = [];
|
|
219
|
+
// Next.js — middleware.ts with matcher config
|
|
220
|
+
if (fingerprint.framework === "nextjs" ||
|
|
221
|
+
fingerprint.projectType === "fullstack") {
|
|
222
|
+
edges.push(...detectNextjsGuards(lookup, routeNodes, repoPath, project));
|
|
223
|
+
}
|
|
224
|
+
// Express / Fastify / Koa — app.use() calls
|
|
225
|
+
if (fingerprint.framework === "express" ||
|
|
226
|
+
fingerprint.framework === "fastify" ||
|
|
227
|
+
fingerprint.framework === "koa" ||
|
|
228
|
+
fingerprint.projectType === "fullstack") {
|
|
229
|
+
edges.push(...detectBackendGuards(lookup, routeNodes, project));
|
|
230
|
+
}
|
|
231
|
+
return edges;
|
|
232
|
+
}
|