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,54 @@
1
+ // ─── detectHookEdges ──────────────────────────────────────────────────────────
2
+ //
3
+ // Creates CALLS edges for hook usage relationships:
4
+ //
5
+ // COMPONENT → HOOK (component uses a hook) — reads metadata.hooks
6
+ // HOOK → HOOK (hook uses another hook) — reads metadata.dependencies
7
+ // FUNCTION → HOOK (function uses a hook) — reads metadata.hookCalls
8
+ //
9
+ // callEdges.ts explicitly skips hook calls so these would otherwise be missed.
10
+ // We reuse CALLS edge type with isHookCall: true in metadata for distinction.
11
+ export function detectHookEdges(nodes, lookup) {
12
+ const edges = [];
13
+ for (const node of nodes) {
14
+ if (node.type !== "COMPONENT" &&
15
+ node.type !== "HOOK" &&
16
+ node.type !== "FUNCTION")
17
+ continue;
18
+ const hookNames = (node.type === "COMPONENT" ? node.metadata.hooks :
19
+ node.type === "HOOK" ? node.metadata.dependencies :
20
+ node.metadata.hookCalls // FUNCTION
21
+ );
22
+ if (!hookNames || hookNames.length === 0)
23
+ continue;
24
+ for (const hookName of hookNames) {
25
+ if (!hookName.startsWith("use"))
26
+ continue;
27
+ const targets = lookup.nodesByName.get(hookName);
28
+ if (!targets || targets.length === 0)
29
+ continue;
30
+ for (const target of targets) {
31
+ // Only connect to HOOK nodes
32
+ if (target.type !== "HOOK")
33
+ continue;
34
+ // No self-loops
35
+ if (target.id === node.id)
36
+ continue;
37
+ // No duplicates
38
+ const alreadyExists = edges.some(e => e.from === node.id && e.to === target.id && e.type === "CALLS");
39
+ if (alreadyExists)
40
+ continue;
41
+ edges.push({
42
+ from: node.id,
43
+ to: target.id,
44
+ type: "CALLS",
45
+ metadata: {
46
+ calledName: hookName,
47
+ isHookCall: true,
48
+ },
49
+ });
50
+ }
51
+ }
52
+ }
53
+ return edges;
54
+ }
@@ -0,0 +1,8 @@
1
+ import type { CodeEdge, CodeNode } from "../../types.js";
2
+ import type { LookupMaps } from "../buildLookup.js";
3
+ export declare function isLocalImport(importPath: string): boolean;
4
+ export interface ImportEdgeResult {
5
+ edges: CodeEdge[];
6
+ thirdPartyMethodNodes: CodeNode[];
7
+ }
8
+ export declare function detectImportEdges(lookupMp: LookupMaps, repoPath: string): ImportEdgeResult;
@@ -0,0 +1,224 @@
1
+ import path from "path";
2
+ import fs from "fs";
3
+ import { extractPackageName } from "../thirdPartyLibs.js";
4
+ import { Project } from "ts-morph";
5
+ // Aliases we consider as local imports
6
+ const LOCAL_PREFIXES = ["./", "../", "@/", "~/", "#/"];
7
+ // Check if an import is local (not third party)
8
+ export function isLocalImport(importPath) {
9
+ return LOCAL_PREFIXES.some(prefix => importPath.startsWith(prefix));
10
+ }
11
+ //Get config file path - tsconfig first, jsconfig as fallback
12
+ function getConfigPath(repoPath) {
13
+ const tsconfig = path.join(repoPath, "tsconfig.json");
14
+ const jsconfig = path.join(repoPath, "jsconfig.json");
15
+ if (fs.existsSync(tsconfig))
16
+ return tsconfig;
17
+ if (fs.existsSync(jsconfig))
18
+ return jsconfig;
19
+ return undefined;
20
+ }
21
+ // Recursively walk directory and add files to project
22
+ // Same approach as parser — reliable on Windows
23
+ function addFilesRecursively(dir, project) {
24
+ const IGNORE_DIRS = [
25
+ "node_modules", "dist", "build",
26
+ ".next", "coverage", ".git",
27
+ ];
28
+ let entries;
29
+ try {
30
+ entries = fs.readdirSync(dir, { withFileTypes: true });
31
+ }
32
+ catch {
33
+ return;
34
+ }
35
+ for (const entry of entries) {
36
+ const fullPath = path.join(dir, entry.name);
37
+ if (entry.isDirectory()) {
38
+ if (IGNORE_DIRS.includes(entry.name))
39
+ continue;
40
+ addFilesRecursively(fullPath, project);
41
+ }
42
+ else if (entry.isFile()) {
43
+ if (!/\.(ts|tsx|js|jsx)$/.test(entry.name))
44
+ continue;
45
+ if (/\.(test|spec)\.(ts|tsx|js|jsx)$/.test(entry.name))
46
+ continue;
47
+ if (/\.d\.ts$/.test(entry.name))
48
+ continue;
49
+ project.addSourceFileAtPath(fullPath);
50
+ }
51
+ }
52
+ }
53
+ export function detectImportEdges(lookupMp, repoPath) {
54
+ const edges = [];
55
+ const createdEdges = new Set();
56
+ // Dedup method nodes across all files — same named import in multiple files
57
+ // produces only ONE node (e.g. a single [npm]/react::useState node).
58
+ const methodNodesMap = new Map();
59
+ const configPath = getConfigPath(repoPath);
60
+ const project = configPath
61
+ ? new Project({ tsConfigFilePath: configPath, skipAddingFilesFromTsConfig: true })
62
+ : new Project({
63
+ compilerOptions: {
64
+ allowJs: true,
65
+ checkJs: false,
66
+ jsx: 4,
67
+ strict: false,
68
+ },
69
+ skipAddingFilesFromTsConfig: true,
70
+ });
71
+ addFilesRecursively(repoPath, project);
72
+ for (const file of project.getSourceFiles()) {
73
+ const absFilePath = file.getFilePath();
74
+ const sourceRelative = path.relative(repoPath, absFilePath).replace(/\\/g, "/");
75
+ const sourceFileNode = lookupMp.fileNodesByPath.get(sourceRelative);
76
+ if (!sourceFileNode)
77
+ continue;
78
+ const importDeclarations = file.getImportDeclarations();
79
+ for (const importDecl of importDeclarations) {
80
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
81
+ if (!isLocalImport(moduleSpecifier)) {
82
+ // ─── Third-party import ───────────────────────────────────
83
+ const pkgName = extractPackageName(moduleSpecifier);
84
+ const thirdPartyNode = lookupMp.thirdPartyNodesByName.get(pkgName);
85
+ if (thirdPartyNode) {
86
+ const fileAliasMap = lookupMp.thirdPartyImportAliases.get(sourceRelative) ?? new Map();
87
+ // ── Named imports → one method node per imported name ──────
88
+ // e.g. import { useState, useEffect } from 'react'
89
+ // → nodes [npm]/react::useState, [npm]/react::useEffect
90
+ // → fileAliasMap: "useState" → "[npm]/react::useState"
91
+ for (const specifier of importDecl.getNamedImports()) {
92
+ const localAlias = specifier.getAliasNode()?.getText() ?? specifier.getName();
93
+ const importedName = specifier.getName();
94
+ const methodNodeId = `[npm]/${pkgName}::${importedName}`;
95
+ if (!methodNodesMap.has(methodNodeId)) {
96
+ methodNodesMap.set(methodNodeId, {
97
+ id: methodNodeId,
98
+ name: `${pkgName}.${importedName}`,
99
+ type: "THIRD_PARTY",
100
+ filePath: `[npm]/${pkgName}`,
101
+ startLine: 0,
102
+ endLine: 0,
103
+ rawCode: undefined,
104
+ codeHash: undefined,
105
+ metadata: {
106
+ isThirdParty: true,
107
+ packageVersion: thirdPartyNode.metadata.packageVersion,
108
+ category: thirdPartyNode.metadata.category,
109
+ parentPackageId: thirdPartyNode.id,
110
+ methodName: importedName,
111
+ },
112
+ });
113
+ }
114
+ // Map the local alias to the method node so callEdges can resolve it
115
+ fileAliasMap.set(localAlias, methodNodeId);
116
+ // IMPORTS edge: source file → method node
117
+ const edgeKey = `${sourceFileNode.id}→${methodNodeId}:IMPORTS`;
118
+ if (!createdEdges.has(edgeKey)) {
119
+ createdEdges.add(edgeKey);
120
+ edges.push({
121
+ from: sourceFileNode.id,
122
+ to: methodNodeId,
123
+ type: "IMPORTS",
124
+ metadata: { importPath: moduleSpecifier, isThirdParty: true, importedName },
125
+ });
126
+ }
127
+ }
128
+ // ── Default import → package node ─────────────────────────
129
+ // e.g. import axios from 'axios'
130
+ // → fileAliasMap: "axios" → "[npm]/axios"
131
+ // Method nodes for member-access calls (axios.get) are created
132
+ // lazily in callEdges.ts when the actual calls are encountered.
133
+ const defaultImport = importDecl.getDefaultImport();
134
+ if (defaultImport) {
135
+ fileAliasMap.set(defaultImport.getText(), thirdPartyNode.id);
136
+ const edgeKey = `${sourceFileNode.id}→${thirdPartyNode.id}:IMPORTS`;
137
+ if (!createdEdges.has(edgeKey)) {
138
+ createdEdges.add(edgeKey);
139
+ edges.push({
140
+ from: sourceFileNode.id,
141
+ to: thirdPartyNode.id,
142
+ type: "IMPORTS",
143
+ metadata: { importPath: moduleSpecifier, isThirdParty: true },
144
+ });
145
+ }
146
+ }
147
+ // ── Namespace import → package node ───────────────────────
148
+ // e.g. import * as ReactQuery from '@tanstack/react-query'
149
+ // → fileAliasMap: "ReactQuery" → "[npm]/@tanstack/react-query"
150
+ const namespaceImport = importDecl.getNamespaceImport();
151
+ if (namespaceImport) {
152
+ fileAliasMap.set(namespaceImport.getText(), thirdPartyNode.id);
153
+ const edgeKey = `${sourceFileNode.id}→${thirdPartyNode.id}:IMPORTS`;
154
+ if (!createdEdges.has(edgeKey)) {
155
+ createdEdges.add(edgeKey);
156
+ edges.push({
157
+ from: sourceFileNode.id,
158
+ to: thirdPartyNode.id,
159
+ type: "IMPORTS",
160
+ metadata: { importPath: moduleSpecifier, isThirdParty: true },
161
+ });
162
+ }
163
+ }
164
+ lookupMp.thirdPartyImportAliases.set(sourceRelative, fileAliasMap);
165
+ }
166
+ continue; // always skip local-resolution path for non-local imports
167
+ }
168
+ // ─── Resolve the imported file path ───────────────────────────
169
+ let resolvedPath;
170
+ // First try ts-morph automatic resolution
171
+ // Works when tsconfig/jsconfig exists
172
+ const resolvedFile = importDecl.getModuleSpecifierSourceFile();
173
+ if (resolvedFile) {
174
+ resolvedPath = resolvedFile.getFilePath();
175
+ }
176
+ else {
177
+ // Fallback — manually resolve relative and alias paths
178
+ // Used when no tsconfig exists (e.g. in tests)
179
+ const currentDir = path.dirname(absFilePath);
180
+ let basePath = moduleSpecifier;
181
+ // Handle @/ ~/ #/ aliases → resolve to src/
182
+ if (moduleSpecifier.startsWith("@/") ||
183
+ moduleSpecifier.startsWith("~/") ||
184
+ moduleSpecifier.startsWith("#/")) {
185
+ basePath = path.join(repoPath, "src", moduleSpecifier.slice(2));
186
+ }
187
+ else {
188
+ // Relative import — resolve from current file's directory
189
+ basePath = path.join(currentDir, moduleSpecifier);
190
+ }
191
+ // Try all possible extensions and index file patterns
192
+ const candidates = [
193
+ basePath,
194
+ basePath + ".ts",
195
+ basePath + ".tsx",
196
+ basePath + ".js",
197
+ basePath + ".jsx",
198
+ path.join(basePath, "index.ts"),
199
+ path.join(basePath, "index.tsx"),
200
+ path.join(basePath, "index.js"),
201
+ path.join(basePath, "index.jsx"),
202
+ ];
203
+ resolvedPath = candidates.find((c) => fs.existsSync(c));
204
+ }
205
+ if (!resolvedPath)
206
+ continue;
207
+ const targetRelative = path.relative(repoPath, resolvedPath).replace(/\\/g, "/");
208
+ const targetFileNode = lookupMp.fileNodesByPath.get(targetRelative);
209
+ if (!targetFileNode)
210
+ continue;
211
+ const edgeKey = `${sourceFileNode.id}→${targetFileNode.id}`;
212
+ if (createdEdges.has(edgeKey))
213
+ continue;
214
+ createdEdges.add(edgeKey);
215
+ edges.push({
216
+ from: sourceFileNode.id,
217
+ to: targetFileNode.id,
218
+ type: "IMPORTS",
219
+ metadata: { importPath: moduleSpecifier },
220
+ });
221
+ }
222
+ }
223
+ return { edges, thirdPartyMethodNodes: [...methodNodesMap.values()] };
224
+ }
@@ -0,0 +1,3 @@
1
+ import type { CodeEdge, CodeNode } from "../../types.js";
2
+ import type { LookupMaps } from "../buildLookup.js";
3
+ export declare function detectPropEdges(nodes: CodeNode[], lookupMp: LookupMaps, repoPath: string): CodeEdge[];
@@ -0,0 +1,142 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { Project, SyntaxKind } from "ts-morph";
4
+ import { closestByPath } from "./utils.js";
5
+ // Recursively walk directory and add files to project
6
+ // Same approach as parser — reliable on Windows
7
+ function addFilesRecursively(dir, project) {
8
+ const IGNORE_DIRS = [
9
+ "node_modules", "dist", "build",
10
+ ".next", "coverage", ".git",
11
+ ];
12
+ let entries;
13
+ try {
14
+ entries = fs.readdirSync(dir, { withFileTypes: true });
15
+ }
16
+ catch {
17
+ return;
18
+ }
19
+ for (const entry of entries) {
20
+ const fullPath = path.join(dir, entry.name);
21
+ if (entry.isDirectory()) {
22
+ if (IGNORE_DIRS.includes(entry.name))
23
+ continue;
24
+ addFilesRecursively(fullPath, project);
25
+ }
26
+ else if (entry.isFile()) {
27
+ if (!/\.(ts|tsx|js|jsx)$/.test(entry.name))
28
+ continue;
29
+ if (/\.(test|spec)\.(ts|tsx|js|jsx)$/.test(entry.name))
30
+ continue;
31
+ if (/\.d\.ts$/.test(entry.name))
32
+ continue;
33
+ project.addSourceFileAtPath(fullPath);
34
+ }
35
+ }
36
+ }
37
+ export function detectPropEdges(nodes, lookupMp, repoPath) {
38
+ const edges = [];
39
+ // Track created edges for deduplication
40
+ // Key: "fromId→toId" — value: index in edges array
41
+ // So we can update renderCount on duplicates
42
+ const edgeIndex = new Map();
43
+ const project = new Project({
44
+ compilerOptions: {
45
+ allowJs: true,
46
+ checkJs: false,
47
+ jsx: 4,
48
+ strict: false,
49
+ },
50
+ skipAddingFilesFromTsConfig: true,
51
+ });
52
+ addFilesRecursively(repoPath, project);
53
+ const componentNodes = nodes.filter(n => n.type === "COMPONENT");
54
+ for (const component of componentNodes) {
55
+ const absPath = path.resolve(repoPath, component.filePath).replace(/\\/g, "/");
56
+ const sourceFile = project.getSourceFile(absPath);
57
+ if (!sourceFile)
58
+ continue;
59
+ // Find all JSX elements in the file
60
+ const jsxOpeningElements = sourceFile.getDescendantsOfKind(SyntaxKind.JsxOpeningElement);
61
+ const jsxSelfClosingElements = sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement);
62
+ // Filter to only JSX within this component's line range (meaning which lies between this component's startLine and endLine)
63
+ const allJsxElements = [...jsxOpeningElements, ...jsxSelfClosingElements]
64
+ .filter(el => {
65
+ const line = el.getStartLineNumber();
66
+ return line >= component.startLine && line <= component.endLine;
67
+ });
68
+ for (const jsxElement of allJsxElements) {
69
+ const tagName = jsxElement.getTagNameNode().getText();
70
+ // Skip HTML native elements — they start with lowercase
71
+ if (/^[a-z]/.test(tagName))
72
+ continue;
73
+ // ── Context.Provider detection ─────────────────────────────────
74
+ // <AuthContext.Provider value={...}> signals this component
75
+ // provides that context to its subtree → WRITES_TO edge.
76
+ if (tagName.endsWith(".Provider")) {
77
+ const contextName = tagName.slice(0, -".Provider".length);
78
+ const ctxStore = lookupMp.storeNodes.find(n => n.name === contextName && n.metadata.storeType === "context");
79
+ if (ctxStore) {
80
+ edges.push({
81
+ from: component.id,
82
+ to: ctxStore.id,
83
+ type: "WRITES_TO",
84
+ metadata: { isContextProvider: true },
85
+ });
86
+ }
87
+ continue; // not a renderable component node — skip normal lookup
88
+ }
89
+ const targetNodes = lookupMp.nodesByName.get(tagName);
90
+ if (!targetNodes || targetNodes.length === 0)
91
+ continue; // this means that this is not a component we extracted
92
+ //Extract prop names passed to the component
93
+ const attributes = jsxElement.getAttributes();
94
+ const props = [];
95
+ for (const attr of attributes) {
96
+ // Regular prop: user={currentUser} or disabled
97
+ // Spread prop: {...props} — we skip these
98
+ if (attr.getKind() === SyntaxKind.JsxAttribute) {
99
+ const propName = attr.getFirstChild()?.getText();
100
+ if (propName)
101
+ props.push(propName);
102
+ }
103
+ }
104
+ // When multiple components share the same name, pick the one
105
+ // physically closest to the rendering file to avoid false edges.
106
+ const targetNode = targetNodes.length === 1
107
+ ? targetNodes[0]
108
+ : closestByPath(targetNodes, component.filePath);
109
+ if (targetNode.id === component.id)
110
+ continue; //skip self referencing
111
+ const edgeKey = `${component.id}→${targetNode.id}`;
112
+ if (edgeIndex.has(edgeKey)) {
113
+ // Edge already exists — update renderCount
114
+ const idx = edgeIndex.get(edgeKey);
115
+ const existing = edges[idx];
116
+ const currentCount = existing.metadata?.renderCount ?? 1;
117
+ edges[idx] = {
118
+ ...existing,
119
+ metadata: {
120
+ ...existing.metadata,
121
+ renderCount: currentCount + 1,
122
+ },
123
+ };
124
+ }
125
+ else {
126
+ // create new edge
127
+ const newEdge = {
128
+ from: component.id,
129
+ to: targetNode.id,
130
+ type: "PROP_PASS",
131
+ metadata: {
132
+ props,
133
+ renderCount: 1,
134
+ }
135
+ };
136
+ edgeIndex.set(edgeKey, edges.length);
137
+ edges.push(newEdge);
138
+ }
139
+ }
140
+ }
141
+ return edges;
142
+ }
@@ -0,0 +1,3 @@
1
+ import type { CodeNode, CodeEdge } from "../../types.js";
2
+ import type { LookupMaps } from "../buildLookup.js";
3
+ export declare function detectRouteEdges(nodes: CodeNode[], lookup: LookupMaps): CodeEdge[];
@@ -0,0 +1,124 @@
1
+ import { closestByPath } from "./utils.js";
2
+ // ─── detectRouteEdges ─────────────────────────────────────────────────────────
3
+ //
4
+ // Creates HANDLES edges from ROUTE nodes → their handler CodeNodes.
5
+ //
6
+ // Resolution strategy per route kind:
7
+ //
8
+ // Backend (Express/Fastify/Koa):
9
+ // 1. metadata.handlerName + same filePath → nodesByFile exact match
10
+ // 2. metadata.handlerName only → nodesByName fallback
11
+ // (handler imported from another file)
12
+ //
13
+ // Next.js API route (route.ts, routeNodeType = "API_ROUTE"):
14
+ // metadata.httpMethod (e.g. "GET") → find node in same file
15
+ // with metadata.isHttpHandler === true && metadata.httpMethod === method
16
+ //
17
+ // Next.js page route (routeNodeType = "PAGE"):
18
+ // → find node in same file with metadata.exportType === "default"
19
+ //
20
+ // Next.js layout/loading/error/not-found:
21
+ // → same as page — default export is the handler
22
+ //
23
+ // If no handler is resolved the route node is still valid in the graph,
24
+ // it just has no outgoing HANDLES edge. This is intentional — orphan
25
+ // route nodes are useful for "unconnected entry points" analysis.
26
+ export function detectRouteEdges(nodes, lookup) {
27
+ const edges = [];
28
+ // Only process ROUTE nodes
29
+ const routeNodes = nodes.filter(n => n.type === "ROUTE");
30
+ for (const routeNode of routeNodes) {
31
+ const meta = routeNode.metadata;
32
+ const filePath = routeNode.filePath;
33
+ const routeKind = meta.routeKind;
34
+ let handlerNode;
35
+ if (routeKind === "backend") {
36
+ handlerNode = resolveBackendHandler(routeNode, lookup);
37
+ }
38
+ else if (routeKind === "nextjs") {
39
+ const routeNodeType = meta.routeNodeType;
40
+ if (routeNodeType === "API_ROUTE") {
41
+ handlerNode = resolveNextjsApiHandler(routeNode, lookup);
42
+ }
43
+ else {
44
+ // PAGE, LAYOUT, LOADING, ERROR, NOT_FOUND — all point to default export
45
+ handlerNode = resolveDefaultExport(filePath, lookup);
46
+ }
47
+ }
48
+ if (!handlerNode)
49
+ continue;
50
+ if (handlerNode.id === routeNode.id)
51
+ continue; // safety: no self-loop
52
+ edges.push({
53
+ from: routeNode.id,
54
+ to: handlerNode.id,
55
+ type: "HANDLES",
56
+ metadata: {
57
+ urlPath: meta.urlPath,
58
+ httpMethod: meta.httpMethod ?? null,
59
+ routeKind,
60
+ },
61
+ });
62
+ }
63
+ return edges;
64
+ }
65
+ // ─── Resolution helpers ───────────────────────────────────────────────────────
66
+ function resolveBackendHandler(routeNode, lookup) {
67
+ // check for the inline handler first. synthetic node for the inline function was created in the routesToCodeNodes function in the backendRoutes.ts extractor
68
+ const inlineHandlerId = routeNode.metadata.inlineHandlerId;
69
+ if (inlineHandlerId) {
70
+ return lookup.nodesByFile.get(routeNode.filePath)?.find(n => n.id === inlineHandlerId);
71
+ }
72
+ //if no inline handler, then proceed with the regular handler name resolution
73
+ const handlerName = routeNode.metadata.handlerName;
74
+ if (!handlerName)
75
+ return undefined;
76
+ const filePath = routeNode.filePath;
77
+ // Strategy 1 — handler defined in the same file as the route registration
78
+ const nodesInFile = lookup.nodesByFile.get(filePath) ?? [];
79
+ const sameFile = nodesInFile.find(n => n.name === handlerName); //same file won't have duplicates, so no need for checking id I think
80
+ if (sameFile)
81
+ return sameFile;
82
+ // Strategy 2 — handler imported from another file, look up by name only.
83
+ // If multiple nodes share the name, prefer the one closest to the route file
84
+ // (shortest relative path difference — simple heuristic).
85
+ const byName = lookup.nodesByName.get(handlerName) ?? [];
86
+ if (byName.length === 0)
87
+ return undefined;
88
+ if (byName.length === 1)
89
+ return byName[0];
90
+ // Multiple candidates — pick the one whose filePath shares the most
91
+ // path segments with the route file
92
+ return closestByPath(byName, filePath);
93
+ }
94
+ function resolveNextjsApiHandler(routeNode, lookup) {
95
+ const httpMethod = routeNode.metadata.httpMethod;
96
+ if (!httpMethod)
97
+ return undefined;
98
+ const filePath = routeNode.filePath;
99
+ const nodesInFile = lookup.nodesByFile.get(filePath) ?? [];
100
+ // Find a node in the same file that was flagged as an HTTP handler
101
+ // with the matching method name (set by functions.ts extractor)
102
+ const handler = nodesInFile.find(n => n.metadata.isHttpHandler === true &&
103
+ n.metadata.httpMethod === httpMethod);
104
+ // console.log("routeNode filePath:", filePath, "and handler => ", handler, "and http method => ", httpMethod);
105
+ // nodesInFile.forEach(n => console.log("candidate handler in file:", filePath, "for node name: ", n.name, "with metadata => ", n.metadata));
106
+ if (handler)
107
+ return handler;
108
+ // Fallback — look for a node whose name exactly matches the HTTP method
109
+ // (handles cases where isHttpHandler flag wasn't set, e.g. older parse)
110
+ return nodesInFile.find(n => n.name === httpMethod);
111
+ }
112
+ function resolveDefaultExport(filePath, lookup) {
113
+ const nodesInFile = lookup.nodesByFile.get(filePath) ?? [];
114
+ // Find the node explicitly marked as default export
115
+ const defaultExport = nodesInFile.find(n => n.metadata.exportType === "default");
116
+ if (defaultExport)
117
+ return defaultExport;
118
+ // Fallback — if only one component/function in the file, it's likely
119
+ // the default export even if metadata.exportType wasn't captured
120
+ const candidates = nodesInFile.filter(n => n.type === "COMPONENT" || n.type === "FUNCTION");
121
+ if (candidates.length === 1)
122
+ return candidates[0];
123
+ return undefined;
124
+ }
@@ -0,0 +1,3 @@
1
+ import type { CodeNode, CodeEdge } from "../../types.js";
2
+ import type { LookupMaps } from "../buildLookup.js";
3
+ export declare function detectStateEdges(nodes: CodeNode[], lookupMp: LookupMaps): CodeEdge[];