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,240 @@
|
|
|
1
|
+
import { SyntaxKind } from "ts-morph";
|
|
2
|
+
import { returnsJSX } from "./components.js";
|
|
3
|
+
import { detectFunctionDirective } from "../directives.js";
|
|
4
|
+
import { extractParams, extractReturnTypeAnnotation, extractBareTypeNames, extractReferencedInterfaces, } from "../typeUtils.js";
|
|
5
|
+
// these are used to detect the routes in the Nextjs
|
|
6
|
+
const HTTP_METHOD_EXPORTS = new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
|
|
7
|
+
function makeId(filePath, name) {
|
|
8
|
+
return `${filePath}::${name}`;
|
|
9
|
+
}
|
|
10
|
+
function extractFunctionCalls(node) {
|
|
11
|
+
const calls = node.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
12
|
+
const names = [];
|
|
13
|
+
for (const call of calls) {
|
|
14
|
+
const name = call.getExpression().getText();
|
|
15
|
+
// Skip hooks, they are handled by hooks extractor
|
|
16
|
+
if (name.startsWith("use"))
|
|
17
|
+
continue;
|
|
18
|
+
// Skip console calls, they are noise
|
|
19
|
+
if (name.startsWith("console"))
|
|
20
|
+
continue;
|
|
21
|
+
names.push(name);
|
|
22
|
+
}
|
|
23
|
+
return [...new Set(names)];
|
|
24
|
+
}
|
|
25
|
+
function extractHookCalls(node) {
|
|
26
|
+
const calls = node.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
27
|
+
const hooks = [];
|
|
28
|
+
for (const call of calls) {
|
|
29
|
+
const name = call.getExpression().getText();
|
|
30
|
+
// Only capture custom hooks — use[A-Z] pattern
|
|
31
|
+
// Built-in React hooks (useState etc.) are not in our node graph
|
|
32
|
+
if (/^use[A-Z]/.test(name))
|
|
33
|
+
hooks.push(name);
|
|
34
|
+
}
|
|
35
|
+
return [...new Set(hooks)];
|
|
36
|
+
}
|
|
37
|
+
function extractApiCalls(node) {
|
|
38
|
+
const calls = node.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
39
|
+
const apiCalls = [];
|
|
40
|
+
for (const call of calls) {
|
|
41
|
+
const text = call.getText();
|
|
42
|
+
const expr = call.getExpression().getText();
|
|
43
|
+
// ─── fetch ────────────────────────────────────────────────────────────────
|
|
44
|
+
if (expr === "fetch") {
|
|
45
|
+
const args = call.getArguments();
|
|
46
|
+
if (args.length > 0)
|
|
47
|
+
apiCalls.push(`fetch(${args[0].getText()})`);
|
|
48
|
+
}
|
|
49
|
+
// ─── axios ────────────────────────────────────────────────────────────────
|
|
50
|
+
if (expr === "axios.get" ||
|
|
51
|
+
expr === "axios.post" ||
|
|
52
|
+
expr === "axios.put" ||
|
|
53
|
+
expr === "axios.delete" ||
|
|
54
|
+
expr === "axios.patch" ||
|
|
55
|
+
expr === "axios") {
|
|
56
|
+
const args = call.getArguments();
|
|
57
|
+
if (args.length > 0)
|
|
58
|
+
apiCalls.push(`${expr}(${args[0].getText()})`);
|
|
59
|
+
}
|
|
60
|
+
// ─── React Query (useQuery, useMutation, useInfiniteQuery) ────────────────
|
|
61
|
+
if (expr === "useQuery" ||
|
|
62
|
+
expr === "useMutation" ||
|
|
63
|
+
expr === "useInfiniteQuery" ||
|
|
64
|
+
expr === "useSuspenseQuery") {
|
|
65
|
+
const args = call.getArguments();
|
|
66
|
+
if (args.length > 0)
|
|
67
|
+
apiCalls.push(`${expr}(${args[0].getText()})`);
|
|
68
|
+
}
|
|
69
|
+
// ─── SWR ──────────────────────────────────────────────────────────────────
|
|
70
|
+
if (expr === "useSWR" || expr === "useSWRMutation") {
|
|
71
|
+
const args = call.getArguments();
|
|
72
|
+
if (args.length > 0)
|
|
73
|
+
apiCalls.push(`${expr}(${args[0].getText()})`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return [...new Set(apiCalls)];
|
|
77
|
+
}
|
|
78
|
+
function hasErrorHandling(node) {
|
|
79
|
+
const tryCatch = node.getDescendantsOfKind(SyntaxKind.TryStatement);
|
|
80
|
+
return tryCatch.length > 0;
|
|
81
|
+
}
|
|
82
|
+
function extractThrowStatements(node) {
|
|
83
|
+
const throws = node.getDescendantsOfKind(SyntaxKind.ThrowStatement);
|
|
84
|
+
return throws.length > 0;
|
|
85
|
+
}
|
|
86
|
+
export function extractFunctions(file, fileDirective = null) {
|
|
87
|
+
const nodes = [];
|
|
88
|
+
const filePath = file.getFilePath();
|
|
89
|
+
// ─── Function Declarations ─────────────────────────────────────────────────
|
|
90
|
+
for (const fn of file.getFunctions()) {
|
|
91
|
+
const name = fn.getName();
|
|
92
|
+
if (!name)
|
|
93
|
+
continue;
|
|
94
|
+
// Skip React components (uppercase and must return JSX) — handled by components extractor.
|
|
95
|
+
// Exception: HTTP method exports (GET, POST, etc.) are uppercase but are
|
|
96
|
+
// route handlers, not components. Captured in the dedicated section below.
|
|
97
|
+
if (/^[A-Z]/.test(name) && !HTTP_METHOD_EXPORTS.has(name) && returnsJSX(fn))
|
|
98
|
+
continue;
|
|
99
|
+
// Skip hooks - handled by hooks extractor
|
|
100
|
+
if (/^use[A-Z]/.test(name))
|
|
101
|
+
continue;
|
|
102
|
+
const typedParams = extractParams(fn);
|
|
103
|
+
const calls = extractFunctionCalls(fn);
|
|
104
|
+
const hookCalls = extractHookCalls(fn);
|
|
105
|
+
const apiCalls = extractApiCalls(fn);
|
|
106
|
+
const isAsync = fn.isAsync();
|
|
107
|
+
const hasErrors = hasErrorHandling(fn);
|
|
108
|
+
const throws = extractThrowStatements(fn);
|
|
109
|
+
const renderingBoundary = detectFunctionDirective(fn.getBody()) ?? fileDirective;
|
|
110
|
+
const returnType = extractReturnTypeAnnotation(fn);
|
|
111
|
+
const bareTypeNames = extractBareTypeNames([...typedParams.map((p) => p.type), returnType]);
|
|
112
|
+
const referencedTypes = extractReferencedInterfaces(file, bareTypeNames);
|
|
113
|
+
nodes.push({
|
|
114
|
+
id: makeId(filePath, name),
|
|
115
|
+
name,
|
|
116
|
+
type: "FUNCTION",
|
|
117
|
+
filePath,
|
|
118
|
+
startLine: fn.getStartLineNumber(),
|
|
119
|
+
endLine: fn.getEndLineNumber(),
|
|
120
|
+
rawCode: fn.getText(),
|
|
121
|
+
metadata: {
|
|
122
|
+
params: typedParams.map((p) => p.name),
|
|
123
|
+
parameters: typedParams,
|
|
124
|
+
returnType,
|
|
125
|
+
referencedTypes,
|
|
126
|
+
calls,
|
|
127
|
+
hookCalls,
|
|
128
|
+
apiCalls,
|
|
129
|
+
isAsync,
|
|
130
|
+
hasErrorHandling: hasErrors,
|
|
131
|
+
throws,
|
|
132
|
+
lineCount: fn.getEndLineNumber() - fn.getStartLineNumber(),
|
|
133
|
+
isHttpHandler: HTTP_METHOD_EXPORTS.has(name),
|
|
134
|
+
httpMethod: HTTP_METHOD_EXPORTS.has(name) ? name : undefined,
|
|
135
|
+
...(renderingBoundary !== null && { renderingBoundary }),
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
// ─── Arrow Function Declarations ───────────────────────────────────────────
|
|
140
|
+
for (const variable of file.getVariableDeclarations()) {
|
|
141
|
+
const name = variable.getName();
|
|
142
|
+
// Skip React components and not nextJs HTTP routes
|
|
143
|
+
if (/^[A-Z]/.test(name) && !HTTP_METHOD_EXPORTS.has(name))
|
|
144
|
+
continue;
|
|
145
|
+
// Skip hooks
|
|
146
|
+
if (/^use[A-Z]/.test(name))
|
|
147
|
+
continue;
|
|
148
|
+
const initializer = variable.getInitializer();
|
|
149
|
+
if (!initializer)
|
|
150
|
+
continue;
|
|
151
|
+
const isArrow = initializer.getKind() === SyntaxKind.ArrowFunction;
|
|
152
|
+
if (!isArrow)
|
|
153
|
+
continue;
|
|
154
|
+
const typedParams = extractParams(initializer);
|
|
155
|
+
const calls = extractFunctionCalls(initializer);
|
|
156
|
+
const hookCalls = extractHookCalls(initializer);
|
|
157
|
+
const apiCalls = extractApiCalls(initializer);
|
|
158
|
+
const isAsync = initializer.getText().startsWith("async");
|
|
159
|
+
const hasErrors = hasErrorHandling(initializer);
|
|
160
|
+
const throws = extractThrowStatements(initializer);
|
|
161
|
+
const renderingBoundary = detectFunctionDirective(initializer.getBody?.()) ?? fileDirective;
|
|
162
|
+
const returnType = extractReturnTypeAnnotation(initializer);
|
|
163
|
+
const bareTypeNames = extractBareTypeNames([...typedParams.map((p) => p.type), returnType]);
|
|
164
|
+
const referencedTypes = extractReferencedInterfaces(file, bareTypeNames);
|
|
165
|
+
nodes.push({
|
|
166
|
+
id: makeId(filePath, name),
|
|
167
|
+
name,
|
|
168
|
+
type: "FUNCTION",
|
|
169
|
+
filePath,
|
|
170
|
+
startLine: variable.getStartLineNumber(),
|
|
171
|
+
endLine: variable.getEndLineNumber(),
|
|
172
|
+
rawCode: variable.getText(),
|
|
173
|
+
metadata: {
|
|
174
|
+
params: typedParams.map((p) => p.name),
|
|
175
|
+
parameters: typedParams,
|
|
176
|
+
returnType,
|
|
177
|
+
referencedTypes,
|
|
178
|
+
calls,
|
|
179
|
+
hookCalls,
|
|
180
|
+
apiCalls,
|
|
181
|
+
isAsync,
|
|
182
|
+
hasErrorHandling: hasErrors,
|
|
183
|
+
throws,
|
|
184
|
+
lineCount: variable.getEndLineNumber() - variable.getStartLineNumber(),
|
|
185
|
+
isHttpHandler: HTTP_METHOD_EXPORTS.has(name),
|
|
186
|
+
httpMethod: HTTP_METHOD_EXPORTS.has(name) ? name : undefined,
|
|
187
|
+
...(renderingBoundary !== null && { renderingBoundary }),
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
// ─── HTTP Method Exports (re-exported via export { GET } pattern) ──────────
|
|
192
|
+
//
|
|
193
|
+
// Handles the case where a route.ts re-exports a handler defined elsewhere:
|
|
194
|
+
// import { myHandler } from "./handlers.js";
|
|
195
|
+
// export { myHandler as GET };
|
|
196
|
+
//
|
|
197
|
+
// In this case getFunctions() and getVariableDeclarations() won't find GET.
|
|
198
|
+
// We detect export specifiers that alias to an HTTP method name.
|
|
199
|
+
for (const exportDecl of file.getExportDeclarations()) {
|
|
200
|
+
for (const specifier of exportDecl.getNamedExports()) {
|
|
201
|
+
const exportedName = specifier.getAliasNode()?.getText()
|
|
202
|
+
?? specifier.getName();
|
|
203
|
+
if (!HTTP_METHOD_EXPORTS.has(exportedName))
|
|
204
|
+
continue;
|
|
205
|
+
// The local name is what was imported — use it to find the original node
|
|
206
|
+
const localName = specifier.getName();
|
|
207
|
+
// Check if we already captured it above (direct export)
|
|
208
|
+
const alreadyCaptured = nodes.some(n => n.name === exportedName);
|
|
209
|
+
if (alreadyCaptured)
|
|
210
|
+
continue;
|
|
211
|
+
// We can't get line numbers reliably for re-exports, so use the
|
|
212
|
+
// export declaration's position as a proxy
|
|
213
|
+
nodes.push({
|
|
214
|
+
id: makeId(filePath, exportedName),
|
|
215
|
+
name: exportedName,
|
|
216
|
+
type: "FUNCTION",
|
|
217
|
+
filePath,
|
|
218
|
+
startLine: exportDecl.getStartLineNumber(),
|
|
219
|
+
endLine: exportDecl.getEndLineNumber(),
|
|
220
|
+
rawCode: exportDecl.getText(),
|
|
221
|
+
metadata: {
|
|
222
|
+
params: [],
|
|
223
|
+
calls: [],
|
|
224
|
+
apiCalls: [],
|
|
225
|
+
isAsync: false,
|
|
226
|
+
hasErrorHandling: false,
|
|
227
|
+
throws: false,
|
|
228
|
+
lineCount: 1,
|
|
229
|
+
isHttpHandler: true,
|
|
230
|
+
httpMethod: exportedName,
|
|
231
|
+
// Record that this is a re-export so routeEdges can follow
|
|
232
|
+
// the chain to the actual implementation if needed
|
|
233
|
+
isReExport: true,
|
|
234
|
+
reExportedFrom: localName,
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return nodes;
|
|
240
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { SyntaxKind } from "ts-morph";
|
|
2
|
+
import { detectFunctionDirective } from "../directives.js";
|
|
3
|
+
import { extractParams, extractBareTypeNames, extractReferencedInterfaces, } from "../typeUtils.js";
|
|
4
|
+
function makeId(filePath, name) {
|
|
5
|
+
return `${filePath}::${name}`;
|
|
6
|
+
}
|
|
7
|
+
function extractDependencies(node) {
|
|
8
|
+
const calls = node.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
9
|
+
const deps = [];
|
|
10
|
+
for (const call of calls) {
|
|
11
|
+
const name = call.getExpression().getText();
|
|
12
|
+
if (name.startsWith("use")) {
|
|
13
|
+
deps.push(name);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return [...new Set(deps)];
|
|
17
|
+
}
|
|
18
|
+
function extractContextRefs(node) {
|
|
19
|
+
const refs = [];
|
|
20
|
+
for (const call of node.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
21
|
+
if (call.getExpression().getText() === "useContext") {
|
|
22
|
+
const arg = call.getArguments()[0];
|
|
23
|
+
if (arg)
|
|
24
|
+
refs.push(arg.getText());
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return [...new Set(refs)];
|
|
28
|
+
}
|
|
29
|
+
// Try explicit annotation first; fall back to shape heuristic.
|
|
30
|
+
function extractReturnType(node) {
|
|
31
|
+
const explicit = node.getReturnTypeNode?.()?.getText();
|
|
32
|
+
if (explicit)
|
|
33
|
+
return explicit;
|
|
34
|
+
const returnStatements = node.getDescendantsOfKind(SyntaxKind.ReturnStatement);
|
|
35
|
+
if (returnStatements.length === 0)
|
|
36
|
+
return "void";
|
|
37
|
+
for (const ret of returnStatements) {
|
|
38
|
+
const expr = ret.getExpression();
|
|
39
|
+
if (!expr)
|
|
40
|
+
continue;
|
|
41
|
+
if (expr.getKind() === SyntaxKind.ArrayLiteralExpression)
|
|
42
|
+
return "array";
|
|
43
|
+
if (expr.getKind() === SyntaxKind.ObjectLiteralExpression)
|
|
44
|
+
return "object";
|
|
45
|
+
}
|
|
46
|
+
return "unknown";
|
|
47
|
+
}
|
|
48
|
+
export function extractHooks(file, fileDirective = null) {
|
|
49
|
+
const nodes = [];
|
|
50
|
+
const filePath = file.getFilePath();
|
|
51
|
+
// ─── Function Declaration Hooks ────────────────────────────────────────────
|
|
52
|
+
// e.g. function useAuth() { ... }
|
|
53
|
+
for (const fn of file.getFunctions()) {
|
|
54
|
+
const name = fn.getName();
|
|
55
|
+
if (!name)
|
|
56
|
+
continue;
|
|
57
|
+
// Hooks must start with "use" followed by uppercase
|
|
58
|
+
if (!/^use[A-Z]/.test(name))
|
|
59
|
+
continue;
|
|
60
|
+
const dependencies = extractDependencies(fn);
|
|
61
|
+
const returnType = extractReturnType(fn);
|
|
62
|
+
const isAsync = fn.isAsync();
|
|
63
|
+
const contextRefs = extractContextRefs(fn);
|
|
64
|
+
const renderingBoundary = detectFunctionDirective(fn.getBody()) ?? fileDirective;
|
|
65
|
+
const typedParams = extractParams(fn);
|
|
66
|
+
const bareTypeNames = extractBareTypeNames([...typedParams.map((p) => p.type), returnType]);
|
|
67
|
+
const referencedTypes = extractReferencedInterfaces(file, bareTypeNames);
|
|
68
|
+
nodes.push({
|
|
69
|
+
id: makeId(filePath, name),
|
|
70
|
+
name,
|
|
71
|
+
type: "HOOK",
|
|
72
|
+
filePath,
|
|
73
|
+
startLine: fn.getStartLineNumber(),
|
|
74
|
+
endLine: fn.getEndLineNumber(),
|
|
75
|
+
rawCode: fn.getText(),
|
|
76
|
+
metadata: {
|
|
77
|
+
dependencies,
|
|
78
|
+
contextRefs,
|
|
79
|
+
returnType,
|
|
80
|
+
parameters: typedParams,
|
|
81
|
+
referencedTypes,
|
|
82
|
+
isAsync,
|
|
83
|
+
...(renderingBoundary !== null && { renderingBoundary }),
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
// ─── Arrow Function Hooks ──────────────────────────────────────────────────
|
|
88
|
+
// e.g. const useAuth = () => { ... }
|
|
89
|
+
for (const variable of file.getVariableDeclarations()) {
|
|
90
|
+
const name = variable.getName();
|
|
91
|
+
// Hooks must start with "use" followed by uppercase
|
|
92
|
+
if (!/^use[A-Z]/.test(name))
|
|
93
|
+
continue;
|
|
94
|
+
const initializer = variable.getInitializer();
|
|
95
|
+
if (!initializer)
|
|
96
|
+
continue;
|
|
97
|
+
const isArrow = initializer.getKind() === SyntaxKind.ArrowFunction;
|
|
98
|
+
if (!isArrow)
|
|
99
|
+
continue;
|
|
100
|
+
const dependencies = extractDependencies(initializer);
|
|
101
|
+
const returnType = extractReturnType(initializer);
|
|
102
|
+
const isAsync = initializer.asKind(SyntaxKind.ArrowFunction)?.isAsync() ?? false;
|
|
103
|
+
const contextRefs = extractContextRefs(initializer);
|
|
104
|
+
const renderingBoundary = detectFunctionDirective(initializer.getBody?.()) ?? fileDirective;
|
|
105
|
+
const typedParams = extractParams(initializer);
|
|
106
|
+
const bareTypeNames = extractBareTypeNames([...typedParams.map((p) => p.type), returnType]);
|
|
107
|
+
const referencedTypes = extractReferencedInterfaces(file, bareTypeNames);
|
|
108
|
+
nodes.push({
|
|
109
|
+
id: makeId(filePath, name),
|
|
110
|
+
name,
|
|
111
|
+
type: "HOOK",
|
|
112
|
+
filePath,
|
|
113
|
+
startLine: variable.getStartLineNumber(),
|
|
114
|
+
endLine: variable.getEndLineNumber(),
|
|
115
|
+
rawCode: variable.getText(),
|
|
116
|
+
metadata: {
|
|
117
|
+
dependencies,
|
|
118
|
+
contextRefs,
|
|
119
|
+
returnType,
|
|
120
|
+
parameters: typedParams,
|
|
121
|
+
referencedTypes,
|
|
122
|
+
isAsync,
|
|
123
|
+
...(renderingBoundary !== null && { renderingBoundary }),
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return nodes;
|
|
128
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { SyntaxKind } from "ts-morph";
|
|
2
|
+
function makeId(filePath, name) {
|
|
3
|
+
return `${filePath}::${name}`;
|
|
4
|
+
}
|
|
5
|
+
function extractStateShape(node, storeType) {
|
|
6
|
+
const properties = [];
|
|
7
|
+
const objLiterals = node.getDescendantsOfKind(SyntaxKind.ObjectLiteralExpression);
|
|
8
|
+
if (objLiterals.length === 0)
|
|
9
|
+
return properties;
|
|
10
|
+
//Takes the first object literal as the state shape
|
|
11
|
+
// Extracts all property names from that object
|
|
12
|
+
// Example: create((set) => ({ items: [], total: 0 })) → ["items", "total"]
|
|
13
|
+
if (storeType === "zustand") {
|
|
14
|
+
// First object literal is the state shape
|
|
15
|
+
const firstObj = objLiterals[0];
|
|
16
|
+
for (const prop of firstObj.getProperties()) {
|
|
17
|
+
const propName = prop.getName ? prop.getName() : null;
|
|
18
|
+
if (propName)
|
|
19
|
+
properties.push(propName);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
// Looks for an initialState property within object literals
|
|
23
|
+
// Extracts properties from inside the initialState object
|
|
24
|
+
// Example: createSlice({ initialState: { items: [], total: 0 } }) → ["items", "total"]
|
|
25
|
+
if (storeType === "redux") {
|
|
26
|
+
// Look specifically for initialState object
|
|
27
|
+
for (const obj of objLiterals) {
|
|
28
|
+
for (const prop of obj.getProperties()) {
|
|
29
|
+
const propName = prop.getName ? prop.getName() : null;
|
|
30
|
+
if (propName === "initialState") {
|
|
31
|
+
const initializer = prop.getInitializer();
|
|
32
|
+
if (!initializer || initializer.getKind() !== SyntaxKind.ObjectLiteralExpression)
|
|
33
|
+
continue;
|
|
34
|
+
for (const innerProp of initializer.getProperties()) {
|
|
35
|
+
const innerName = innerProp.getName ? innerProp.getName() : null;
|
|
36
|
+
if (innerName)
|
|
37
|
+
properties.push(innerName);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Looks for a key property and extracts its value as the state identifier
|
|
44
|
+
// Returns the key name (not property names)
|
|
45
|
+
// Example: atom({ key: "cartState", default: { items: [] } }) → ["cartState"]
|
|
46
|
+
if (storeType === "recoil") {
|
|
47
|
+
// Recoil has a single default value not a shape
|
|
48
|
+
// We store the key name as the state identifier
|
|
49
|
+
for (const obj of objLiterals) {
|
|
50
|
+
for (const prop of obj.getProperties()) {
|
|
51
|
+
const propName = prop.getName ? prop.getName() : null;
|
|
52
|
+
if (propName === "key") {
|
|
53
|
+
const initializer = prop.getInitializer
|
|
54
|
+
? prop.getInitializer()
|
|
55
|
+
: null;
|
|
56
|
+
if (initializer)
|
|
57
|
+
properties.push(initializer.getText().replace(/['"]/g, ""));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (storeType === "jotai") {
|
|
63
|
+
// Jotai atoms don't have a shape
|
|
64
|
+
// We just return empty — the atom name itself is the identifier
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
return properties;
|
|
68
|
+
}
|
|
69
|
+
//This function will extract action names from different store types.
|
|
70
|
+
function extractActions(node, storeType) {
|
|
71
|
+
const actions = [];
|
|
72
|
+
const objLiterals = node.getDescendantsOfKind(SyntaxKind.ObjectLiteralExpression);
|
|
73
|
+
// Finds function-valued properties in the first object literal
|
|
74
|
+
// Extracts names of arrow functions and function expressions
|
|
75
|
+
// Example: create((set) => ({ addItem: () => set({ items: [] }) })) → ["addItem"]
|
|
76
|
+
if (storeType === "zustand") {
|
|
77
|
+
// Actions are function-valued properties in the first object
|
|
78
|
+
for (const obj of objLiterals) {
|
|
79
|
+
for (const prop of obj.getProperties()) {
|
|
80
|
+
const initializer = prop.getInitializer
|
|
81
|
+
? prop.getInitializer()
|
|
82
|
+
: null;
|
|
83
|
+
if (!initializer)
|
|
84
|
+
continue;
|
|
85
|
+
if (initializer.getKind() === SyntaxKind.ArrowFunction ||
|
|
86
|
+
initializer.getKind() === SyntaxKind.FunctionExpression) {
|
|
87
|
+
const name = prop.getName ? prop.getName() : null;
|
|
88
|
+
if (name)
|
|
89
|
+
actions.push(name);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Looks specifically inside a reducers object
|
|
95
|
+
// Extracts property names from the reducers object
|
|
96
|
+
// Example: createSlice({ reducers: { addItem: (state) => {} } }) → ["addItem"]
|
|
97
|
+
if (storeType === "redux") {
|
|
98
|
+
// Actions live inside the reducers object specifically
|
|
99
|
+
for (const obj of objLiterals) {
|
|
100
|
+
for (const prop of obj.getProperties()) {
|
|
101
|
+
const propName = prop.getName ? prop.getName() : null;
|
|
102
|
+
if (propName === "reducers") {
|
|
103
|
+
const initializer = prop.getInitializer();
|
|
104
|
+
if (!initializer || initializer.getKind() !== SyntaxKind.ObjectLiteralExpression)
|
|
105
|
+
continue;
|
|
106
|
+
for (const reducerProp of initializer.getProperties()) {
|
|
107
|
+
const reducerName = reducerProp.getName ? reducerProp.getName() : null;
|
|
108
|
+
if (reducerName)
|
|
109
|
+
actions.push(reducerName);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (storeType === "recoil" || storeType === "jotai") {
|
|
116
|
+
// No actions defined inside atom/selector definitions
|
|
117
|
+
// Actions happen via setters in components
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
return actions;
|
|
121
|
+
}
|
|
122
|
+
function detectStoreType(text) {
|
|
123
|
+
if (text.startsWith("create(") || text.startsWith("create<"))
|
|
124
|
+
return "zustand";
|
|
125
|
+
if (text.includes("createSlice") || text.includes("createReducer"))
|
|
126
|
+
return "redux";
|
|
127
|
+
if (text.includes("createContext"))
|
|
128
|
+
return "context";
|
|
129
|
+
if (text.startsWith("atom(") && text.includes("key:"))
|
|
130
|
+
return "recoil";
|
|
131
|
+
if (text.startsWith("selector(") || text.startsWith("atomFamily("))
|
|
132
|
+
return "recoil";
|
|
133
|
+
if (text.startsWith("atom(") && !text.includes("key:"))
|
|
134
|
+
return "jotai";
|
|
135
|
+
return "unknown";
|
|
136
|
+
}
|
|
137
|
+
export function extractStores(file) {
|
|
138
|
+
const nodes = [];
|
|
139
|
+
const filePath = file.getFilePath();
|
|
140
|
+
for (const variable of file.getVariableDeclarations()) {
|
|
141
|
+
const name = variable.getName();
|
|
142
|
+
const initializer = variable.getInitializer();
|
|
143
|
+
if (!initializer)
|
|
144
|
+
continue;
|
|
145
|
+
const text = initializer.getText();
|
|
146
|
+
// ─── Zustand Store ────────────────────────────────────────────────────────
|
|
147
|
+
// e.g. const useCartStore = create((set) => ({ ... }))
|
|
148
|
+
const isZustand = text.startsWith("create(") || text.startsWith("create<");
|
|
149
|
+
// ─── Redux Slice ──────────────────────────────────────────────────────────
|
|
150
|
+
// e.g. const cartSlice = createSlice({ name, initialState, reducers })
|
|
151
|
+
const isRedux = text.startsWith("createSlice(") || text.startsWith("createReducer(");
|
|
152
|
+
// ---- Recoil Atom Selector ──────────────────────────────────────────────────────────
|
|
153
|
+
const isRecoil = text.startsWith("atom(") || text.startsWith("selector(") || text.startsWith("atomFamily(") || text.startsWith("selectorFamily(");
|
|
154
|
+
// ----- Jotai Atom ──────────────────────────────────────────────────────────
|
|
155
|
+
const isJotai = text.startsWith("atom(") && !text.includes("key:"); // recoil atoms always have a key property , jotai atoms do not
|
|
156
|
+
// ─── React Context ────────────────────────────────────────────────────────
|
|
157
|
+
// e.g. const AuthContext = createContext(null)
|
|
158
|
+
const isContext = text.startsWith("createContext(") ||
|
|
159
|
+
text.startsWith("React.createContext(");
|
|
160
|
+
if (!isZustand && !isRedux && !isContext && !isRecoil && !isJotai)
|
|
161
|
+
continue;
|
|
162
|
+
const storeType = detectStoreType(text);
|
|
163
|
+
const stateShape = extractStateShape(initializer, storeType);
|
|
164
|
+
const actions = extractActions(initializer, storeType);
|
|
165
|
+
nodes.push({
|
|
166
|
+
id: makeId(filePath, name),
|
|
167
|
+
name,
|
|
168
|
+
type: "STATE_STORE",
|
|
169
|
+
filePath,
|
|
170
|
+
startLine: variable.getStartLineNumber(),
|
|
171
|
+
endLine: variable.getEndLineNumber(),
|
|
172
|
+
rawCode: variable.getText(),
|
|
173
|
+
metadata: {
|
|
174
|
+
storeType,
|
|
175
|
+
stateShape, // e.g. ["items", "total", "isOpen"]
|
|
176
|
+
actions, // e.g. ["addItem", "removeItem", "clearCart"]
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return nodes;
|
|
181
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CodeNode } from "../types.js";
|
|
2
|
+
export interface ParserResult {
|
|
3
|
+
nodes: CodeNode[];
|
|
4
|
+
stats: {
|
|
5
|
+
totalFiles: number;
|
|
6
|
+
totalNodes: number;
|
|
7
|
+
componentCount: number;
|
|
8
|
+
hookCount: number;
|
|
9
|
+
functionCount: number;
|
|
10
|
+
storeCount: number;
|
|
11
|
+
skippedFiles: number;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export declare function parseRepo(repoPath: string): ParserResult;
|