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.
Files changed (136) hide show
  1. package/LICENSE +674 -0
  2. package/dist/clustering/index.d.ts +27 -0
  3. package/dist/clustering/index.js +149 -0
  4. package/dist/config/index.d.ts +10 -0
  5. package/dist/config/index.js +78 -0
  6. package/dist/config/providers/file.d.ts +19 -0
  7. package/dist/config/providers/file.js +215 -0
  8. package/dist/config/providers/request.d.ts +2 -0
  9. package/dist/config/providers/request.js +72 -0
  10. package/dist/config/types.d.ts +46 -0
  11. package/dist/config/types.js +81 -0
  12. package/dist/config/writer.d.ts +29 -0
  13. package/dist/config/writer.js +103 -0
  14. package/dist/filesystem/appRouter.d.ts +2 -0
  15. package/dist/filesystem/appRouter.js +126 -0
  16. package/dist/filesystem/backendRoutes.d.ts +2 -0
  17. package/dist/filesystem/backendRoutes.js +161 -0
  18. package/dist/filesystem/index.d.ts +2 -0
  19. package/dist/filesystem/index.js +28 -0
  20. package/dist/filesystem/index.test.d.ts +1 -0
  21. package/dist/filesystem/index.test.js +178 -0
  22. package/dist/filesystem/pagesRouter.d.ts +2 -0
  23. package/dist/filesystem/pagesRouter.js +109 -0
  24. package/dist/fingerprint/detectors.d.ts +8 -0
  25. package/dist/fingerprint/detectors.js +174 -0
  26. package/dist/fingerprint/index.d.ts +2 -0
  27. package/dist/fingerprint/index.js +41 -0
  28. package/dist/fingerprint/index.test.d.ts +1 -0
  29. package/dist/fingerprint/index.test.js +148 -0
  30. package/dist/graph/buildLookup.d.ts +10 -0
  31. package/dist/graph/buildLookup.js +32 -0
  32. package/dist/graph/edges/callEdges.d.ts +7 -0
  33. package/dist/graph/edges/callEdges.js +145 -0
  34. package/dist/graph/edges/eventEdges.d.ts +7 -0
  35. package/dist/graph/edges/eventEdges.js +203 -0
  36. package/dist/graph/edges/guardEdges.d.ts +3 -0
  37. package/dist/graph/edges/guardEdges.js +232 -0
  38. package/dist/graph/edges/hookEdges.d.ts +3 -0
  39. package/dist/graph/edges/hookEdges.js +54 -0
  40. package/dist/graph/edges/importEdges.d.ts +8 -0
  41. package/dist/graph/edges/importEdges.js +224 -0
  42. package/dist/graph/edges/propEdges.d.ts +3 -0
  43. package/dist/graph/edges/propEdges.js +142 -0
  44. package/dist/graph/edges/routeEdge.d.ts +3 -0
  45. package/dist/graph/edges/routeEdge.js +124 -0
  46. package/dist/graph/edges/stateEdges.d.ts +3 -0
  47. package/dist/graph/edges/stateEdges.js +206 -0
  48. package/dist/graph/edges/testEdges.d.ts +3 -0
  49. package/dist/graph/edges/testEdges.js +143 -0
  50. package/dist/graph/edges/utils.d.ts +2 -0
  51. package/dist/graph/edges/utils.js +25 -0
  52. package/dist/graph/index.d.ts +6 -0
  53. package/dist/graph/index.js +65 -0
  54. package/dist/graph/index.test.d.ts +1 -0
  55. package/dist/graph/index.test.js +542 -0
  56. package/dist/graph/thirdPartyLibs.d.ts +8 -0
  57. package/dist/graph/thirdPartyLibs.js +162 -0
  58. package/dist/index.d.ts +15 -0
  59. package/dist/index.js +15 -0
  60. package/dist/jobs/index.d.ts +5 -0
  61. package/dist/jobs/index.js +11 -0
  62. package/dist/jobs/queue/interface.d.ts +13 -0
  63. package/dist/jobs/queue/interface.js +1 -0
  64. package/dist/jobs/queue/memory.d.ts +24 -0
  65. package/dist/jobs/queue/memory.js +291 -0
  66. package/dist/jobs/runner.d.ts +3 -0
  67. package/dist/jobs/runner.js +136 -0
  68. package/dist/jobs/types.d.ts +112 -0
  69. package/dist/jobs/types.js +33 -0
  70. package/dist/parser/directives.d.ts +4 -0
  71. package/dist/parser/directives.js +31 -0
  72. package/dist/parser/extractors/components.d.ts +5 -0
  73. package/dist/parser/extractors/components.js +240 -0
  74. package/dist/parser/extractors/functions.d.ts +4 -0
  75. package/dist/parser/extractors/functions.js +240 -0
  76. package/dist/parser/extractors/hooks.d.ts +4 -0
  77. package/dist/parser/extractors/hooks.js +128 -0
  78. package/dist/parser/extractors/stores.d.ts +3 -0
  79. package/dist/parser/extractors/stores.js +181 -0
  80. package/dist/parser/index.d.ts +14 -0
  81. package/dist/parser/index.js +168 -0
  82. package/dist/parser/index.test.d.ts +1 -0
  83. package/dist/parser/index.test.js +319 -0
  84. package/dist/parser/typeUtils.d.ts +9 -0
  85. package/dist/parser/typeUtils.js +46 -0
  86. package/dist/pipeline/index.d.ts +50 -0
  87. package/dist/pipeline/index.js +249 -0
  88. package/dist/scoring/connectionCounter.d.ts +28 -0
  89. package/dist/scoring/connectionCounter.js +134 -0
  90. package/dist/scoring/fileScorer.d.ts +2 -0
  91. package/dist/scoring/fileScorer.js +44 -0
  92. package/dist/scoring/index.d.ts +22 -0
  93. package/dist/scoring/index.js +130 -0
  94. package/dist/scoring/index.test.d.ts +1 -0
  95. package/dist/scoring/index.test.js +453 -0
  96. package/dist/scoring/nodeScorer.d.ts +3 -0
  97. package/dist/scoring/nodeScorer.js +108 -0
  98. package/dist/scoring/noiseFilter.d.ts +18 -0
  99. package/dist/scoring/noiseFilter.js +92 -0
  100. package/dist/storage/fileStorage.d.ts +117 -0
  101. package/dist/storage/fileStorage.js +616 -0
  102. package/dist/storage/index.d.ts +4 -0
  103. package/dist/storage/index.js +2 -0
  104. package/dist/storage/interface.d.ts +27 -0
  105. package/dist/storage/interface.js +1 -0
  106. package/dist/summarizer/checkpoint.d.ts +15 -0
  107. package/dist/summarizer/checkpoint.js +110 -0
  108. package/dist/summarizer/index.d.ts +2 -0
  109. package/dist/summarizer/index.js +281 -0
  110. package/dist/summarizer/mapreduce.d.ts +4 -0
  111. package/dist/summarizer/mapreduce.js +87 -0
  112. package/dist/summarizer/prompts.d.ts +22 -0
  113. package/dist/summarizer/prompts.js +205 -0
  114. package/dist/summarizer/providers/anthropic.d.ts +9 -0
  115. package/dist/summarizer/providers/anthropic.js +78 -0
  116. package/dist/summarizer/providers/gemini.d.ts +9 -0
  117. package/dist/summarizer/providers/gemini.js +79 -0
  118. package/dist/summarizer/providers/index.d.ts +3 -0
  119. package/dist/summarizer/providers/index.js +43 -0
  120. package/dist/summarizer/providers/ollama.d.ts +9 -0
  121. package/dist/summarizer/providers/ollama.js +23 -0
  122. package/dist/summarizer/providers/openRouter.d.ts +9 -0
  123. package/dist/summarizer/providers/openRouter.js +19 -0
  124. package/dist/summarizer/providers/openai.d.ts +9 -0
  125. package/dist/summarizer/providers/openai.js +72 -0
  126. package/dist/summarizer/providers/types.d.ts +32 -0
  127. package/dist/summarizer/providers/types.js +1 -0
  128. package/dist/summarizer/retry.d.ts +7 -0
  129. package/dist/summarizer/retry.js +51 -0
  130. package/dist/summarizer/topological.d.ts +3 -0
  131. package/dist/summarizer/topological.js +105 -0
  132. package/dist/summarizer/types.d.ts +57 -0
  133. package/dist/summarizer/types.js +17 -0
  134. package/dist/types.d.ts +78 -0
  135. package/dist/types.js +1 -0
  136. 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
+ }
@@ -0,0 +1,3 @@
1
+ import type { CodeEdge, CodeNode } from "../../types.js";
2
+ import type { LookupMaps } from "../buildLookup.js";
3
+ export declare function detectHookEdges(nodes: CodeNode[], lookup: LookupMaps): CodeEdge[];