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,206 @@
|
|
|
1
|
+
const REDUX_READ_HOOKS = ["useSelector", "useSelectorShallowEqual"]; // Redux hooks that indicate reads
|
|
2
|
+
const REDUX_WRITE_HOOKS = ["useDispatch"]; // Redux hooks that indicate writes
|
|
3
|
+
// Recoil hooks and what they mean
|
|
4
|
+
const RECOIL_READ_HOOKS = ["useRecoilValue", "useRecoilValueLoadable"];
|
|
5
|
+
const RECOIL_WRITE_HOOKS = ["useSetRecoilState", "useResetRecoilState"];
|
|
6
|
+
const RECOIL_BOTH_HOOKS = ["useRecoilState", "useRecoilStateLoadable"];
|
|
7
|
+
// Jotai hooks and what they mean
|
|
8
|
+
const JOTAI_READ_HOOKS = ["useAtomValue"];
|
|
9
|
+
const JOTAI_WRITE_HOOKS = ["useSetAtom"];
|
|
10
|
+
const JOTAI_BOTH_HOOKS = ["useAtom"];
|
|
11
|
+
// Context hooks
|
|
12
|
+
const CONTEXT_HOOKS = ["useContext"];
|
|
13
|
+
// Main function to detect state edges based on hooks used in components and hooks
|
|
14
|
+
export function detectStateEdges(nodes, lookupMp) {
|
|
15
|
+
const edges = [];
|
|
16
|
+
// Build a fast name → store node map from storeNodes
|
|
17
|
+
// This is separate from nodesByName because we only want
|
|
18
|
+
// to match against STATE_STORE nodes not all nodes
|
|
19
|
+
const storesByName = new Map();
|
|
20
|
+
for (const store of lookupMp.storeNodes) {
|
|
21
|
+
storesByName.set(store.name, store);
|
|
22
|
+
}
|
|
23
|
+
// Separate Redux stores for useSelector/useDispatch
|
|
24
|
+
// which don't reference a store by name directly
|
|
25
|
+
const reduxStores = lookupMp.storeNodes.filter((n) => n.metadata.storeType === "redux");
|
|
26
|
+
for (const node of nodes) {
|
|
27
|
+
// Only components and hooks use state
|
|
28
|
+
if (node.type !== "COMPONENT" && node.type !== "HOOK")
|
|
29
|
+
continue;
|
|
30
|
+
// COMPONENT nodes store hook calls in metadata.hooks; HOOK nodes use metadata.dependencies
|
|
31
|
+
const hooks = (node.type === "HOOK" ? node.metadata.dependencies : node.metadata.hooks);
|
|
32
|
+
if (!hooks || hooks.length === 0)
|
|
33
|
+
continue;
|
|
34
|
+
for (const hookName of hooks) {
|
|
35
|
+
// ─── Zustand ──────────────────────────────────────────────────────────
|
|
36
|
+
// Zustand stores are used directly by their hook name
|
|
37
|
+
// e.g. useCartStore, useAuthStore
|
|
38
|
+
const zustandStore = storesByName.get(hookName);
|
|
39
|
+
if (zustandStore && zustandStore.metadata.storeType === "zustand") {
|
|
40
|
+
// Create both edges — can't distinguish read vs write without
|
|
41
|
+
// expensive type analysis
|
|
42
|
+
edges.push({
|
|
43
|
+
from: node.id,
|
|
44
|
+
to: zustandStore.id,
|
|
45
|
+
type: "READS_FROM",
|
|
46
|
+
metadata: { hookUsed: hookName, storeType: "zustand" },
|
|
47
|
+
});
|
|
48
|
+
edges.push({
|
|
49
|
+
from: node.id,
|
|
50
|
+
to: zustandStore.id,
|
|
51
|
+
type: "WRITES_TO",
|
|
52
|
+
metadata: { hookUsed: hookName, storeType: "zustand" },
|
|
53
|
+
});
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
// ─── Redux ────────────────────────────────────────────────────────────
|
|
57
|
+
// Redux uses useSelector (read) and useDispatch (write)
|
|
58
|
+
// Neither references a specific store by name so we connect
|
|
59
|
+
// to all Redux stores found in the codebase
|
|
60
|
+
if (REDUX_READ_HOOKS.includes(hookName)) {
|
|
61
|
+
for (const store of reduxStores) {
|
|
62
|
+
edges.push({
|
|
63
|
+
from: node.id,
|
|
64
|
+
to: store.id,
|
|
65
|
+
type: "READS_FROM",
|
|
66
|
+
metadata: { hookUsed: hookName, storeType: "redux" },
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (REDUX_WRITE_HOOKS.includes(hookName)) {
|
|
72
|
+
for (const store of reduxStores) {
|
|
73
|
+
edges.push({
|
|
74
|
+
from: node.id,
|
|
75
|
+
to: store.id,
|
|
76
|
+
type: "WRITES_TO",
|
|
77
|
+
metadata: { hookUsed: hookName, storeType: "redux" },
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
// ─── Context ──────────────────────────────────────────────────────────
|
|
83
|
+
// useContext(AuthContext) — the parser now extracts the argument name
|
|
84
|
+
// and stores it in metadata.contextRefs (e.g. ["AuthContext"]).
|
|
85
|
+
// We do a direct name lookup so the edge is always correct.
|
|
86
|
+
// useContext is read-only — no WRITES_TO edge here.
|
|
87
|
+
if (CONTEXT_HOOKS.includes(hookName)) {
|
|
88
|
+
const contextRefs = node.metadata.contextRefs;
|
|
89
|
+
if (!contextRefs || contextRefs.length === 0)
|
|
90
|
+
continue;
|
|
91
|
+
for (const ref of contextRefs) {
|
|
92
|
+
const contextStore = storesByName.get(ref);
|
|
93
|
+
if (contextStore && contextStore.metadata.storeType === "context") {
|
|
94
|
+
edges.push({
|
|
95
|
+
from: node.id,
|
|
96
|
+
to: contextStore.id,
|
|
97
|
+
type: "READS_FROM",
|
|
98
|
+
metadata: { hookUsed: hookName, storeType: "context" },
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
// ─── Recoil ───────────────────────────────────────────────────────────
|
|
105
|
+
if (RECOIL_READ_HOOKS.includes(hookName)) {
|
|
106
|
+
// Find recoil atoms referenced in this component
|
|
107
|
+
for (const ref of hooks) {
|
|
108
|
+
const atomStore = storesByName.get(ref);
|
|
109
|
+
if (atomStore && atomStore.metadata.storeType === "recoil") {
|
|
110
|
+
edges.push({
|
|
111
|
+
from: node.id,
|
|
112
|
+
to: atomStore.id,
|
|
113
|
+
type: "READS_FROM",
|
|
114
|
+
metadata: { hookUsed: hookName, storeType: "recoil" },
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (RECOIL_WRITE_HOOKS.includes(hookName)) {
|
|
121
|
+
for (const ref of hooks) {
|
|
122
|
+
const atomStore = storesByName.get(ref);
|
|
123
|
+
if (atomStore && atomStore.metadata.storeType === "recoil") {
|
|
124
|
+
edges.push({
|
|
125
|
+
from: node.id,
|
|
126
|
+
to: atomStore.id,
|
|
127
|
+
type: "WRITES_TO",
|
|
128
|
+
metadata: { hookUsed: hookName, storeType: "recoil" },
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (RECOIL_BOTH_HOOKS.includes(hookName)) {
|
|
135
|
+
for (const ref of hooks) {
|
|
136
|
+
const atomStore = storesByName.get(ref);
|
|
137
|
+
if (atomStore && atomStore.metadata.storeType === "recoil") {
|
|
138
|
+
edges.push({
|
|
139
|
+
from: node.id,
|
|
140
|
+
to: atomStore.id,
|
|
141
|
+
type: "READS_FROM",
|
|
142
|
+
metadata: { hookUsed: hookName, storeType: "recoil" },
|
|
143
|
+
});
|
|
144
|
+
edges.push({
|
|
145
|
+
from: node.id,
|
|
146
|
+
to: atomStore.id,
|
|
147
|
+
type: "WRITES_TO",
|
|
148
|
+
metadata: { hookUsed: hookName, storeType: "recoil" },
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
// ─── Jotai ────────────────────────────────────────────────────────────
|
|
155
|
+
if (JOTAI_READ_HOOKS.includes(hookName)) {
|
|
156
|
+
for (const ref of hooks) {
|
|
157
|
+
const atomStore = storesByName.get(ref);
|
|
158
|
+
if (atomStore && atomStore.metadata.storeType === "jotai") {
|
|
159
|
+
edges.push({
|
|
160
|
+
from: node.id,
|
|
161
|
+
to: atomStore.id,
|
|
162
|
+
type: "READS_FROM",
|
|
163
|
+
metadata: { hookUsed: hookName, storeType: "jotai" },
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (JOTAI_WRITE_HOOKS.includes(hookName)) {
|
|
170
|
+
for (const ref of hooks) {
|
|
171
|
+
const atomStore = storesByName.get(ref);
|
|
172
|
+
if (atomStore && atomStore.metadata.storeType === "jotai") {
|
|
173
|
+
edges.push({
|
|
174
|
+
from: node.id,
|
|
175
|
+
to: atomStore.id,
|
|
176
|
+
type: "WRITES_TO",
|
|
177
|
+
metadata: { hookUsed: hookName, storeType: "jotai" },
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (JOTAI_BOTH_HOOKS.includes(hookName)) {
|
|
184
|
+
for (const ref of hooks) {
|
|
185
|
+
const atomStore = storesByName.get(ref);
|
|
186
|
+
if (atomStore && atomStore.metadata.storeType === "jotai") {
|
|
187
|
+
edges.push({
|
|
188
|
+
from: node.id,
|
|
189
|
+
to: atomStore.id,
|
|
190
|
+
type: "READS_FROM",
|
|
191
|
+
metadata: { hookUsed: hookName, storeType: "jotai" },
|
|
192
|
+
});
|
|
193
|
+
edges.push({
|
|
194
|
+
from: node.id,
|
|
195
|
+
to: atomStore.id,
|
|
196
|
+
type: "WRITES_TO",
|
|
197
|
+
metadata: { hookUsed: hookName, storeType: "jotai" },
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return edges;
|
|
206
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
//This file will detect the TEST edges from the test/story files to the components they are testing.
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import { Project } from "ts-morph";
|
|
5
|
+
import { isLocalImport } from "./importEdges.js";
|
|
6
|
+
function getConfigPath(repoPath) {
|
|
7
|
+
const tsConfig = path.join(repoPath, "tsconfig.json");
|
|
8
|
+
const jsconfig = path.join(repoPath, "jsconfig.json");
|
|
9
|
+
if (fs.existsSync(tsConfig))
|
|
10
|
+
return tsConfig;
|
|
11
|
+
if (fs.existsSync(jsconfig))
|
|
12
|
+
return jsconfig;
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
function addFilesRecursively(dir, project) {
|
|
16
|
+
const IGNORE_DIRS = ["node_modules", "dist", "build", ".next", "coverage", ".git"];
|
|
17
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
18
|
+
for (const entry of entries) {
|
|
19
|
+
if (entry.isDirectory()) {
|
|
20
|
+
if (IGNORE_DIRS.includes(entry.name))
|
|
21
|
+
continue;
|
|
22
|
+
addFilesRecursively(path.join(dir, entry.name), project);
|
|
23
|
+
}
|
|
24
|
+
else if (/\.(ts|tsx|js|jsx)$/.test(entry.name)) {
|
|
25
|
+
project.addSourceFileAtPath(path.join(dir, entry.name));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function resolveAlias(moduleSpecifier, repoPath, currentDir) {
|
|
30
|
+
// Try to read tsconfig paths
|
|
31
|
+
const tsconfigPath = path.join(repoPath, "tsconfig.json");
|
|
32
|
+
if (fs.existsSync(tsconfigPath)) {
|
|
33
|
+
try {
|
|
34
|
+
const tsconfig = JSON.parse(fs.readFileSync(tsconfigPath, "utf-8"));
|
|
35
|
+
const paths = tsconfig.compilerOptions?.paths ?? {};
|
|
36
|
+
const baseUrl = tsconfig.compilerOptions?.baseUrl ?? ".";
|
|
37
|
+
const base = path.join(repoPath, baseUrl);
|
|
38
|
+
// Check each alias — e.g. "@/*": ["./src/*"]
|
|
39
|
+
for (const [alias, targets] of Object.entries(paths)) {
|
|
40
|
+
const aliasPrefix = alias.replace("/*", "/");
|
|
41
|
+
if (moduleSpecifier.startsWith(aliasPrefix)) {
|
|
42
|
+
const rest = moduleSpecifier.slice(aliasPrefix.length);
|
|
43
|
+
const target = targets[0].replace("/*", "");
|
|
44
|
+
return path.join(base, target, rest);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// malformed tsconfig — fall through
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Fallback — relative import
|
|
53
|
+
return path.join(currentDir, moduleSpecifier);
|
|
54
|
+
}
|
|
55
|
+
// ─── detectTestEdges ──────────────────────────────────────────────────────────
|
|
56
|
+
//
|
|
57
|
+
// Creates TESTS edges from TEST/STORY file nodes to the actual component/
|
|
58
|
+
// function/hook nodes they import from production code.
|
|
59
|
+
//
|
|
60
|
+
// Resolution strategy:
|
|
61
|
+
// 1. Find all TEST and STORY file nodes
|
|
62
|
+
// 2. For each, read its import declarations via ts-morph
|
|
63
|
+
// 3. Resolve each named import to its target file
|
|
64
|
+
// 4. Look up the named export in nodesByFile to find the exact node
|
|
65
|
+
// 5. Create TESTS edge: testNode → TESTS → productionNode
|
|
66
|
+
export function detectTestEdges(lookup, repoPath) {
|
|
67
|
+
const edges = [];
|
|
68
|
+
const createdEdges = new Set();
|
|
69
|
+
//filter all TEST and STORY file nodes
|
|
70
|
+
const testFileNodes = [...lookup.fileNodesByPath.values()].filter(n => ["TEST", "STORY"].includes(n.type));
|
|
71
|
+
if (testFileNodes.length === 0)
|
|
72
|
+
return edges;
|
|
73
|
+
//setup ts-morph project for import resolution
|
|
74
|
+
const configPath = getConfigPath(repoPath);
|
|
75
|
+
const project = configPath
|
|
76
|
+
? new Project({
|
|
77
|
+
tsConfigFilePath: configPath,
|
|
78
|
+
skipAddingFilesFromTsConfig: true, // we do not want it to consider node modules or other unwanted files. So we manually walk down the repo.
|
|
79
|
+
})
|
|
80
|
+
: new Project({ skipAddingFilesFromTsConfig: true });
|
|
81
|
+
addFilesRecursively(repoPath, project);
|
|
82
|
+
for (const testNode of testFileNodes) {
|
|
83
|
+
const absFilePath = path.join(repoPath, testNode.filePath);
|
|
84
|
+
const sourceFile = project.getSourceFile(absFilePath);
|
|
85
|
+
if (!sourceFile)
|
|
86
|
+
continue;
|
|
87
|
+
const importDecls = sourceFile.getImportDeclarations();
|
|
88
|
+
for (const importDecl of importDecls) {
|
|
89
|
+
const moduleSpecifier = importDecl.getModuleSpecifierValue();
|
|
90
|
+
if (!isLocalImport(moduleSpecifier))
|
|
91
|
+
continue; // we are not considering third party or library imports.
|
|
92
|
+
let resolvedPath;
|
|
93
|
+
const resolvedFile = importDecl.getModuleSpecifierSourceFile();
|
|
94
|
+
if (resolvedFile) {
|
|
95
|
+
resolvedPath = resolvedFile.getFilePath();
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
//This code block will rarely hit because ts-morph will handlethe resolvedFile from the project
|
|
99
|
+
const currentDir = path.dirname(absFilePath);
|
|
100
|
+
let basePath = resolveAlias(moduleSpecifier, repoPath, currentDir);
|
|
101
|
+
const candidates = [
|
|
102
|
+
basePath,
|
|
103
|
+
basePath + ".ts", basePath + ".tsx",
|
|
104
|
+
basePath + ".js", basePath + ".jsx",
|
|
105
|
+
path.join(basePath, "index.ts"),
|
|
106
|
+
path.join(basePath, "index.tsx"),
|
|
107
|
+
];
|
|
108
|
+
resolvedPath = candidates.find(c => fs.existsSync(c));
|
|
109
|
+
}
|
|
110
|
+
if (!resolvedPath)
|
|
111
|
+
continue;
|
|
112
|
+
const targetRelative = path.relative(repoPath, resolvedPath).replace(/\\/g, "/");
|
|
113
|
+
// Get named imports — { Button } from "./Button"
|
|
114
|
+
const namedImports = importDecl.getNamedImports();
|
|
115
|
+
for (const namedImport of namedImports) {
|
|
116
|
+
const importedName = namedImport.getName();
|
|
117
|
+
// Find the actual production node by name in the target file
|
|
118
|
+
const nodesInFile = lookup.nodesByFile.get(targetRelative) ?? [];
|
|
119
|
+
const targetNode = nodesInFile.find(n => n.name === importedName);
|
|
120
|
+
if (!targetNode)
|
|
121
|
+
continue;
|
|
122
|
+
// Skip if target is also a TEST/STORY node — test importing test
|
|
123
|
+
if (targetNode.type === "TEST" || targetNode.type === "STORY")
|
|
124
|
+
continue;
|
|
125
|
+
const edgeKey = `${testNode.id}→${targetNode.id}`;
|
|
126
|
+
if (createdEdges.has(edgeKey))
|
|
127
|
+
continue;
|
|
128
|
+
createdEdges.add(edgeKey);
|
|
129
|
+
edges.push({
|
|
130
|
+
from: testNode.id,
|
|
131
|
+
to: targetNode.id,
|
|
132
|
+
type: "TESTS",
|
|
133
|
+
metadata: {
|
|
134
|
+
importPath: moduleSpecifier,
|
|
135
|
+
testFileType: testNode.type,
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
console.log(` TESTS edges: ${edges.length}`);
|
|
142
|
+
return edges;
|
|
143
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Picks the node whose filePath shares the most leading path segments
|
|
2
|
+
// with the reference path. When multiple nodes have the same name (e.g.
|
|
3
|
+
// two files both export a function called "handleSubmit"), this heuristic
|
|
4
|
+
// selects the one physically closest to the caller in the directory tree.
|
|
5
|
+
export function closestByPath(candidates, referencePath) {
|
|
6
|
+
const refParts = referencePath.split("/");
|
|
7
|
+
let best = candidates[0];
|
|
8
|
+
let bestScore = 0;
|
|
9
|
+
for (const candidate of candidates) {
|
|
10
|
+
const parts = candidate.filePath.split("/");
|
|
11
|
+
let score = 0;
|
|
12
|
+
const len = Math.min(refParts.length, parts.length);
|
|
13
|
+
for (let i = 0; i < len; i++) {
|
|
14
|
+
if (refParts[i] === parts[i])
|
|
15
|
+
score++;
|
|
16
|
+
else
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
if (score > bestScore) {
|
|
20
|
+
bestScore = score;
|
|
21
|
+
best = candidate;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return best;
|
|
25
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { BackendRouteNode, CodeEdge, CodeNode, ProjectFingerprint, RouteNode } from "../types.js";
|
|
2
|
+
export interface EdgeDetectionResult {
|
|
3
|
+
edges: CodeEdge[];
|
|
4
|
+
ghostNodes: CodeNode[];
|
|
5
|
+
}
|
|
6
|
+
export declare function detectEdges(nodes: CodeNode[], routeNodes: (RouteNode | BackendRouteNode)[], repoPath: string, fingerprint: ProjectFingerprint): EdgeDetectionResult;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { buildLookupMaps } from "./buildLookup.js";
|
|
2
|
+
import { detectCallEdges } from "./edges/callEdges.js";
|
|
3
|
+
import { detectEventEdges } from "./edges/eventEdges.js";
|
|
4
|
+
import { detectGuardEdges } from "./edges/guardEdges.js";
|
|
5
|
+
import { detectHookEdges } from "./edges/hookEdges.js";
|
|
6
|
+
import { detectImportEdges } from "./edges/importEdges.js";
|
|
7
|
+
import { detectPropEdges } from "./edges/propEdges.js";
|
|
8
|
+
import { detectRouteEdges } from "./edges/routeEdge.js";
|
|
9
|
+
import { detectStateEdges } from "./edges/stateEdges.js";
|
|
10
|
+
import { detectTestEdges } from "./edges/testEdges.js";
|
|
11
|
+
export function detectEdges(nodes, routeNodes, repoPath, fingerprint) {
|
|
12
|
+
console.log(`Building lookup maps for edge detection for ${nodes.length} nodes...`);
|
|
13
|
+
//building lookup maps
|
|
14
|
+
const lookupMp = buildLookupMaps(nodes);
|
|
15
|
+
console.log("Running edge detectors...");
|
|
16
|
+
// importEdges MUST run before callEdges — it populates lookupMp.thirdPartyImportAliases
|
|
17
|
+
// as a side-effect, and callEdges reads that map to resolve third-party CALLS edges.
|
|
18
|
+
const importResult = detectImportEdges(lookupMp, repoPath);
|
|
19
|
+
const importEdges = importResult.edges;
|
|
20
|
+
const callResult = detectCallEdges(nodes, lookupMp);
|
|
21
|
+
const callEdges = callResult.edges;
|
|
22
|
+
const stateEdges = detectStateEdges(nodes, lookupMp);
|
|
23
|
+
const propEdges = detectPropEdges(nodes, lookupMp, repoPath);
|
|
24
|
+
const hookEdges = detectHookEdges(nodes, lookupMp);
|
|
25
|
+
const eventResults = detectEventEdges(lookupMp, repoPath);
|
|
26
|
+
const routeEdges = detectRouteEdges(nodes, lookupMp);
|
|
27
|
+
// GUARDS — middleware to route protection
|
|
28
|
+
const guardEdges = detectGuardEdges(nodes, lookupMp, routeNodes, repoPath, fingerprint);
|
|
29
|
+
const testEdges = detectTestEdges(lookupMp, repoPath); // This does not needs nodes, as it detect edges from the file
|
|
30
|
+
// Collect all dynamically-created third-party method nodes (dedup by id)
|
|
31
|
+
const newThirdPartyNodesMap = new Map();
|
|
32
|
+
for (const n of [...importResult.thirdPartyMethodNodes, ...callResult.newThirdPartyNodes]) {
|
|
33
|
+
if (!newThirdPartyNodesMap.has(n.id))
|
|
34
|
+
newThirdPartyNodesMap.set(n.id, n);
|
|
35
|
+
}
|
|
36
|
+
const newThirdPartyNodes = [...newThirdPartyNodesMap.values()];
|
|
37
|
+
console.log(`Running edge detectors...`);
|
|
38
|
+
console.log(` CALLS edges: ${callEdges.length}`);
|
|
39
|
+
console.log(` IMPORTS edges: ${importEdges.length}`);
|
|
40
|
+
console.log(` STATE edges: ${stateEdges.length}`);
|
|
41
|
+
console.log(` PROP edges: ${propEdges.length}`);
|
|
42
|
+
console.log(` HOOK edges: ${hookEdges.length}`);
|
|
43
|
+
console.log(` EVENT edges: ${eventResults.edges.length}`);
|
|
44
|
+
console.log(` ROUTE edges: ${routeEdges.length}`);
|
|
45
|
+
console.log(` GUARD edges: ${guardEdges.length}`);
|
|
46
|
+
console.log(` TEST edges: ${testEdges.length}`);
|
|
47
|
+
console.log(` Ghost nodes created: ${eventResults.ghostNodes.length}`);
|
|
48
|
+
console.log(` Third-party method nodes: ${newThirdPartyNodes.length}`);
|
|
49
|
+
const allEdges = [
|
|
50
|
+
...callEdges,
|
|
51
|
+
...importEdges,
|
|
52
|
+
...stateEdges,
|
|
53
|
+
...propEdges,
|
|
54
|
+
...hookEdges,
|
|
55
|
+
...eventResults.edges,
|
|
56
|
+
...routeEdges,
|
|
57
|
+
...guardEdges,
|
|
58
|
+
...testEdges,
|
|
59
|
+
];
|
|
60
|
+
console.log(`Total edges detected: ${allEdges.length}`);
|
|
61
|
+
return {
|
|
62
|
+
edges: allEdges,
|
|
63
|
+
ghostNodes: [...eventResults.ghostNodes, ...newThirdPartyNodes],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|