@elench/testkit 0.1.62 → 0.1.63
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/lib/coverage/evidence.mjs +14 -3
- package/lib/coverage/evidence.test.mjs +3 -2
- package/lib/coverage/graph-builder.mjs +4 -3
- package/lib/coverage/index.test.mjs +135 -0
- package/lib/coverage/next-discovery.mjs +36 -5
- package/lib/coverage/next-static-analysis.mjs +420 -136
- package/lib/coverage/shared.mjs +67 -0
- package/lib/coverage/shared.test.mjs +33 -0
- package/node_modules/@elench/testkit-bridge/package.json +2 -2
- package/node_modules/@elench/testkit-bridge/src/index.mjs +34 -2
- package/node_modules/@elench/testkit-bridge/src/index.test.mjs +204 -17
- package/node_modules/@elench/testkit-protocol/package.json +1 -1
- package/package.json +3 -3
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import ts from "typescript";
|
|
3
|
+
import { DYNAMIC_SEGMENT_TOKEN } from "./shared.mjs";
|
|
3
4
|
|
|
4
5
|
const HTTP_WRAPPER_METHODS = {
|
|
5
6
|
getJson: "GET",
|
|
@@ -23,13 +24,29 @@ export function analyzeNextPageFile({
|
|
|
23
24
|
isServerActionFile,
|
|
24
25
|
normalizeRoute,
|
|
25
26
|
}) {
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
return analyzeNextRouteTree({
|
|
28
|
+
serviceName,
|
|
29
|
+
serviceRoot,
|
|
30
|
+
route,
|
|
31
|
+
rootFiles: [{ filePath, content }],
|
|
28
32
|
readSourceFile,
|
|
29
|
-
resolveImportPath,
|
|
33
|
+
resolveImportPath: (fromFilePath, specifier) =>
|
|
34
|
+
fromFilePath === filePath ? resolveImportPath(specifier) : resolveImportToSourceFile(serviceRoot, fromFilePath, specifier),
|
|
30
35
|
isServerActionFile,
|
|
36
|
+
normalizeRoute,
|
|
31
37
|
});
|
|
32
|
-
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function analyzeNextRouteTree({
|
|
41
|
+
serviceName,
|
|
42
|
+
serviceRoot,
|
|
43
|
+
route,
|
|
44
|
+
rootFiles = [],
|
|
45
|
+
readSourceFile,
|
|
46
|
+
resolveImportPath,
|
|
47
|
+
isServerActionFile,
|
|
48
|
+
normalizeRoute,
|
|
49
|
+
}) {
|
|
33
50
|
const directRequests = [];
|
|
34
51
|
const directServerActionRefs = [];
|
|
35
52
|
const nodes = [];
|
|
@@ -37,140 +54,42 @@ export function analyzeNextPageFile({
|
|
|
37
54
|
const surfacesByTargetValue = new Map();
|
|
38
55
|
const actionNodeIds = new Set();
|
|
39
56
|
const requestNodeIds = new Set();
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
to: node.id,
|
|
66
|
-
confidence: request.confidence,
|
|
67
|
-
});
|
|
68
|
-
}
|
|
57
|
+
const pageNodeId = `page_view:${serviceName}:${route}`;
|
|
58
|
+
const moduleCache = new Map();
|
|
59
|
+
const visitedExports = new Set();
|
|
60
|
+
|
|
61
|
+
const analyzerContext = {
|
|
62
|
+
serviceName,
|
|
63
|
+
serviceRoot,
|
|
64
|
+
route,
|
|
65
|
+
pageNodeId,
|
|
66
|
+
readSourceFile,
|
|
67
|
+
resolveImportPath,
|
|
68
|
+
isServerActionFile,
|
|
69
|
+
normalizeRoute,
|
|
70
|
+
moduleCache,
|
|
71
|
+
visitedExports,
|
|
72
|
+
state: {
|
|
73
|
+
nodes,
|
|
74
|
+
edges,
|
|
75
|
+
directRequests,
|
|
76
|
+
directServerActionRefs,
|
|
77
|
+
surfacesByTargetValue,
|
|
78
|
+
actionNodeIds,
|
|
79
|
+
requestNodeIds,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
69
82
|
|
|
70
|
-
for (const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
83
|
+
for (const rootFile of rootFiles) {
|
|
84
|
+
const rootContent =
|
|
85
|
+
typeof rootFile.content === "string"
|
|
86
|
+
? rootFile.content
|
|
87
|
+
: readSourceFile(rootFile.filePath);
|
|
88
|
+
if (!rootContent) continue;
|
|
89
|
+
const moduleInfo = loadModuleInfo(rootFile.filePath, rootContent, analyzerContext);
|
|
90
|
+
analyzeModuleExport(moduleInfo, "default", analyzerContext);
|
|
75
91
|
}
|
|
76
92
|
|
|
77
|
-
walkJsx(sourceFile, (element, formContext) => {
|
|
78
|
-
const descriptor = describeSurface(element, formContext);
|
|
79
|
-
if (!descriptor) return formContext;
|
|
80
|
-
|
|
81
|
-
const surfaceKey = descriptor.target?.value || descriptor.actionName || `${descriptor.tagName}:${descriptor.line}`;
|
|
82
|
-
const surfaceId = `ui_surface:${serviceName}:${route}:${surfaceKey}`;
|
|
83
|
-
const surfaceNode = {
|
|
84
|
-
id: surfaceId,
|
|
85
|
-
kind: "ui_surface",
|
|
86
|
-
service: serviceName,
|
|
87
|
-
label: descriptor.label,
|
|
88
|
-
route,
|
|
89
|
-
filePath,
|
|
90
|
-
...(descriptor.target ? { target: descriptor.target } : {}),
|
|
91
|
-
metadata: {
|
|
92
|
-
surfaceKind: descriptor.surfaceKind,
|
|
93
|
-
tagName: descriptor.tagName,
|
|
94
|
-
},
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
nodes.push(surfaceNode);
|
|
98
|
-
edges.push({
|
|
99
|
-
id: `contains:page_view:${serviceName}:${route}:${surfaceId}`,
|
|
100
|
-
kind: "contains",
|
|
101
|
-
from: `page_view:${serviceName}:${route}`,
|
|
102
|
-
to: surfaceId,
|
|
103
|
-
confidence: "high",
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
if (descriptor.target?.kind === "testId") {
|
|
107
|
-
surfacesByTargetValue.set(descriptor.target.value, surfaceNode);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (descriptor.actionBinding) {
|
|
111
|
-
const actionId = `ui_action:${serviceName}:${route}:${descriptor.actionBinding.key}`;
|
|
112
|
-
if (!actionNodeIds.has(actionId)) {
|
|
113
|
-
actionNodeIds.add(actionId);
|
|
114
|
-
nodes.push({
|
|
115
|
-
id: actionId,
|
|
116
|
-
kind: "ui_action",
|
|
117
|
-
service: serviceName,
|
|
118
|
-
label: descriptor.actionBinding.label,
|
|
119
|
-
route,
|
|
120
|
-
filePath,
|
|
121
|
-
metadata: {
|
|
122
|
-
bindingKind: descriptor.actionBinding.kind,
|
|
123
|
-
actionProp: descriptor.actionBinding.actionProp,
|
|
124
|
-
},
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
edges.push({
|
|
129
|
-
id: `triggers:${surfaceId}:${actionId}`,
|
|
130
|
-
kind: "triggers",
|
|
131
|
-
from: surfaceId,
|
|
132
|
-
to: actionId,
|
|
133
|
-
confidence: descriptor.actionBinding.confidence,
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
const actionAnalysis = analyzeActionBinding(descriptor.actionBinding, {
|
|
137
|
-
imports,
|
|
138
|
-
localFunctions,
|
|
139
|
-
normalizeRoute,
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
for (const request of actionAnalysis.requests) {
|
|
143
|
-
const node = createClientRequestNode(serviceName, filePath, request, descriptor.actionBinding.key);
|
|
144
|
-
if (!requestNodeIds.has(node.id)) {
|
|
145
|
-
requestNodeIds.add(node.id);
|
|
146
|
-
nodes.push(node);
|
|
147
|
-
}
|
|
148
|
-
directRequests.push({
|
|
149
|
-
originNodeId: actionId,
|
|
150
|
-
node,
|
|
151
|
-
method: request.method,
|
|
152
|
-
path: request.path,
|
|
153
|
-
});
|
|
154
|
-
edges.push({
|
|
155
|
-
id: `requests:${actionId}:${node.id}`,
|
|
156
|
-
kind: "requests",
|
|
157
|
-
from: actionId,
|
|
158
|
-
to: node.id,
|
|
159
|
-
confidence: request.confidence,
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
for (const serverActionRef of actionAnalysis.serverActionRefs) {
|
|
164
|
-
directServerActionRefs.push({
|
|
165
|
-
originNodeId: actionId,
|
|
166
|
-
...serverActionRef,
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return descriptor.formContext ?? formContext;
|
|
172
|
-
});
|
|
173
|
-
|
|
174
93
|
return {
|
|
175
94
|
nodes: dedupeById(nodes),
|
|
176
95
|
edges: dedupeById(edges),
|
|
@@ -212,6 +131,272 @@ export function extractPlaywrightVisitedRoutes(content, filePath = "suite.pw.tes
|
|
|
212
131
|
return [...new Set(routes)];
|
|
213
132
|
}
|
|
214
133
|
|
|
134
|
+
function analyzeModuleExport(moduleInfo, contextExportName, analyzerContext) {
|
|
135
|
+
if (!moduleInfo) return;
|
|
136
|
+
const visitKey = `${moduleInfo.filePath}#${contextExportName}`;
|
|
137
|
+
if (analyzerContext.visitedExports.has(visitKey)) return;
|
|
138
|
+
analyzerContext.visitedExports.add(visitKey);
|
|
139
|
+
|
|
140
|
+
const callable = resolveModuleExportCallable(moduleInfo, contextExportName);
|
|
141
|
+
if (!callable) return;
|
|
142
|
+
|
|
143
|
+
const localFunctions = new Map(moduleInfo.topLevelFunctions);
|
|
144
|
+
for (const [name, node] of collectScopeLocalFunctions(callable)) {
|
|
145
|
+
localFunctions.set(name, node);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const actionAnalysis = analyzeCallableLikeNode(
|
|
149
|
+
callable,
|
|
150
|
+
{
|
|
151
|
+
imports: moduleInfo.imports,
|
|
152
|
+
localFunctions,
|
|
153
|
+
normalizeRoute: analyzerContext.normalizeRoute,
|
|
154
|
+
moduleLoader: (filePath) => loadModuleInfoFromFile(filePath, analyzerContext),
|
|
155
|
+
resolveImportedExport: resolveImportedExportCallable,
|
|
156
|
+
analyzeImportedCallable,
|
|
157
|
+
},
|
|
158
|
+
new Set(),
|
|
159
|
+
{ skipNestedFunctions: true }
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
for (const request of actionAnalysis.requests) {
|
|
163
|
+
const node = createClientRequestNode(
|
|
164
|
+
analyzerContext.serviceName,
|
|
165
|
+
moduleInfo.filePath,
|
|
166
|
+
request,
|
|
167
|
+
`page:${analyzerContext.route}:${moduleInfo.filePath}`
|
|
168
|
+
);
|
|
169
|
+
if (!analyzerContext.state.requestNodeIds.has(node.id)) {
|
|
170
|
+
analyzerContext.state.requestNodeIds.add(node.id);
|
|
171
|
+
analyzerContext.state.nodes.push(node);
|
|
172
|
+
}
|
|
173
|
+
analyzerContext.state.directRequests.push({
|
|
174
|
+
originNodeId: analyzerContext.pageNodeId,
|
|
175
|
+
node,
|
|
176
|
+
method: request.method,
|
|
177
|
+
path: request.path,
|
|
178
|
+
});
|
|
179
|
+
analyzerContext.state.edges.push({
|
|
180
|
+
id: `requests:${analyzerContext.pageNodeId}:${node.id}`,
|
|
181
|
+
kind: "requests",
|
|
182
|
+
from: analyzerContext.pageNodeId,
|
|
183
|
+
to: node.id,
|
|
184
|
+
confidence: request.confidence,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
for (const serverActionRef of actionAnalysis.serverActionRefs) {
|
|
189
|
+
analyzerContext.state.directServerActionRefs.push({
|
|
190
|
+
originNodeId: analyzerContext.pageNodeId,
|
|
191
|
+
...serverActionRef,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
walkJsx(callable, (element, formContext) => {
|
|
196
|
+
const descriptor = describeSurface(element, formContext);
|
|
197
|
+
if (descriptor) {
|
|
198
|
+
const surfaceKey = descriptor.target?.value || descriptor.actionName || `${moduleInfo.filePath}:${descriptor.tagName}:${descriptor.line}`;
|
|
199
|
+
const surfaceId = `ui_surface:${analyzerContext.serviceName}:${analyzerContext.route}:${surfaceKey}`;
|
|
200
|
+
const surfaceNode = {
|
|
201
|
+
id: surfaceId,
|
|
202
|
+
kind: "ui_surface",
|
|
203
|
+
service: analyzerContext.serviceName,
|
|
204
|
+
label: descriptor.label,
|
|
205
|
+
route: analyzerContext.route,
|
|
206
|
+
filePath: moduleInfo.filePath,
|
|
207
|
+
...(descriptor.target ? { target: descriptor.target } : {}),
|
|
208
|
+
metadata: {
|
|
209
|
+
surfaceKind: descriptor.surfaceKind,
|
|
210
|
+
tagName: descriptor.tagName,
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
analyzerContext.state.nodes.push(surfaceNode);
|
|
215
|
+
analyzerContext.state.edges.push({
|
|
216
|
+
id: `contains:${analyzerContext.pageNodeId}:${surfaceId}`,
|
|
217
|
+
kind: "contains",
|
|
218
|
+
from: analyzerContext.pageNodeId,
|
|
219
|
+
to: surfaceId,
|
|
220
|
+
confidence: "high",
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
if (descriptor.target?.kind === "testId") {
|
|
224
|
+
analyzerContext.state.surfacesByTargetValue.set(descriptor.target.value, surfaceNode);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (descriptor.actionBinding) {
|
|
228
|
+
const actionId = `ui_action:${analyzerContext.serviceName}:${analyzerContext.route}:${descriptor.actionBinding.key}`;
|
|
229
|
+
if (!analyzerContext.state.actionNodeIds.has(actionId)) {
|
|
230
|
+
analyzerContext.state.actionNodeIds.add(actionId);
|
|
231
|
+
analyzerContext.state.nodes.push({
|
|
232
|
+
id: actionId,
|
|
233
|
+
kind: "ui_action",
|
|
234
|
+
service: analyzerContext.serviceName,
|
|
235
|
+
label: descriptor.actionBinding.label,
|
|
236
|
+
route: analyzerContext.route,
|
|
237
|
+
filePath: moduleInfo.filePath,
|
|
238
|
+
metadata: {
|
|
239
|
+
bindingKind: descriptor.actionBinding.kind,
|
|
240
|
+
actionProp: descriptor.actionBinding.actionProp,
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
analyzerContext.state.edges.push({
|
|
246
|
+
id: `triggers:${surfaceId}:${actionId}`,
|
|
247
|
+
kind: "triggers",
|
|
248
|
+
from: surfaceId,
|
|
249
|
+
to: actionId,
|
|
250
|
+
confidence: descriptor.actionBinding.confidence,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const bindingAnalysis = analyzeActionBinding(descriptor.actionBinding, {
|
|
254
|
+
imports: moduleInfo.imports,
|
|
255
|
+
localFunctions,
|
|
256
|
+
normalizeRoute: analyzerContext.normalizeRoute,
|
|
257
|
+
moduleLoader: (filePath) => loadModuleInfoFromFile(filePath, analyzerContext),
|
|
258
|
+
resolveImportedExport: resolveImportedExportCallable,
|
|
259
|
+
analyzeImportedCallable,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
for (const request of bindingAnalysis.requests) {
|
|
263
|
+
const node = createClientRequestNode(
|
|
264
|
+
analyzerContext.serviceName,
|
|
265
|
+
moduleInfo.filePath,
|
|
266
|
+
request,
|
|
267
|
+
descriptor.actionBinding.key
|
|
268
|
+
);
|
|
269
|
+
if (!analyzerContext.state.requestNodeIds.has(node.id)) {
|
|
270
|
+
analyzerContext.state.requestNodeIds.add(node.id);
|
|
271
|
+
analyzerContext.state.nodes.push(node);
|
|
272
|
+
}
|
|
273
|
+
analyzerContext.state.directRequests.push({
|
|
274
|
+
originNodeId: actionId,
|
|
275
|
+
node,
|
|
276
|
+
method: request.method,
|
|
277
|
+
path: request.path,
|
|
278
|
+
});
|
|
279
|
+
analyzerContext.state.edges.push({
|
|
280
|
+
id: `requests:${actionId}:${node.id}`,
|
|
281
|
+
kind: "requests",
|
|
282
|
+
from: actionId,
|
|
283
|
+
to: node.id,
|
|
284
|
+
confidence: request.confidence,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
for (const serverActionRef of bindingAnalysis.serverActionRefs) {
|
|
289
|
+
analyzerContext.state.directServerActionRefs.push({
|
|
290
|
+
originNodeId: actionId,
|
|
291
|
+
...serverActionRef,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return descriptor.formContext ?? formContext;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const componentRef = resolveImportedComponentRef(element, moduleInfo.imports);
|
|
300
|
+
if (componentRef) {
|
|
301
|
+
analyzeImportedComponent(componentRef, analyzerContext);
|
|
302
|
+
}
|
|
303
|
+
return formContext;
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function analyzeImportedComponent(componentRef, analyzerContext) {
|
|
308
|
+
const moduleInfo = loadModuleInfoFromFile(componentRef.filePath, analyzerContext);
|
|
309
|
+
if (!moduleInfo) return;
|
|
310
|
+
analyzeModuleExport(moduleInfo, componentRef.exportName, analyzerContext);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function analyzeImportedCallable(imported, options, visited) {
|
|
314
|
+
if (!imported?.resolvedFilePath || imported.isServerAction || visited.has(imported.resolvedFilePath)) {
|
|
315
|
+
return emptyActionAnalysis();
|
|
316
|
+
}
|
|
317
|
+
const moduleInfo = options.moduleLoader(imported.resolvedFilePath);
|
|
318
|
+
if (!moduleInfo) return emptyActionAnalysis();
|
|
319
|
+
const callable = options.resolveImportedExport(moduleInfo, imported.importedName);
|
|
320
|
+
if (!callable) return emptyActionAnalysis();
|
|
321
|
+
|
|
322
|
+
const localFunctions = new Map(moduleInfo.topLevelFunctions);
|
|
323
|
+
for (const [name, node] of collectScopeLocalFunctions(callable)) {
|
|
324
|
+
localFunctions.set(name, node);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const nextVisited = new Set(visited);
|
|
328
|
+
nextVisited.add(imported.resolvedFilePath);
|
|
329
|
+
return analyzeCallableLikeNode(
|
|
330
|
+
callable,
|
|
331
|
+
{
|
|
332
|
+
imports: moduleInfo.imports,
|
|
333
|
+
localFunctions,
|
|
334
|
+
normalizeRoute: options.normalizeRoute,
|
|
335
|
+
moduleLoader: options.moduleLoader,
|
|
336
|
+
resolveImportedExport: options.resolveImportedExport,
|
|
337
|
+
analyzeImportedCallable,
|
|
338
|
+
},
|
|
339
|
+
nextVisited
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function loadModuleInfoFromFile(filePath, analyzerContext) {
|
|
344
|
+
const cached = analyzerContext.moduleCache.get(filePath);
|
|
345
|
+
if (cached) return cached;
|
|
346
|
+
const content = analyzerContext.readSourceFile(filePath);
|
|
347
|
+
if (!content) return null;
|
|
348
|
+
return loadModuleInfo(filePath, content, analyzerContext);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function loadModuleInfo(filePath, content, analyzerContext) {
|
|
352
|
+
const cached = analyzerContext.moduleCache.get(filePath);
|
|
353
|
+
if (cached) return cached;
|
|
354
|
+
|
|
355
|
+
const sourceFile = createSourceFile(filePath, content);
|
|
356
|
+
const imports = collectImports(sourceFile, {
|
|
357
|
+
readSourceFile: analyzerContext.readSourceFile,
|
|
358
|
+
resolveImportPath: (specifier) => analyzerContext.resolveImportPath(filePath, specifier),
|
|
359
|
+
isServerActionFile: analyzerContext.isServerActionFile,
|
|
360
|
+
});
|
|
361
|
+
const topLevelFunctions = collectTopLevelFunctions(sourceFile);
|
|
362
|
+
const exports = collectExportedCallables(sourceFile, topLevelFunctions);
|
|
363
|
+
const moduleInfo = {
|
|
364
|
+
filePath,
|
|
365
|
+
sourceFile,
|
|
366
|
+
imports,
|
|
367
|
+
topLevelFunctions,
|
|
368
|
+
exports,
|
|
369
|
+
defaultExport: findDefaultPageComponent(sourceFile, topLevelFunctions),
|
|
370
|
+
};
|
|
371
|
+
analyzerContext.moduleCache.set(filePath, moduleInfo);
|
|
372
|
+
return moduleInfo;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function resolveModuleExportCallable(moduleInfo, exportName) {
|
|
376
|
+
if (!moduleInfo) return null;
|
|
377
|
+
if (exportName === "default") return moduleInfo.defaultExport || null;
|
|
378
|
+
return moduleInfo.exports.get(exportName) || null;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function resolveImportedExportCallable(moduleInfo, importedName) {
|
|
382
|
+
if (!moduleInfo) return null;
|
|
383
|
+
if (importedName === "default") return moduleInfo.defaultExport || null;
|
|
384
|
+
return moduleInfo.exports.get(importedName) || null;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function resolveImportedComponentRef(node, imports) {
|
|
388
|
+
const opening = ts.isJsxElement(node) ? node.openingElement : node;
|
|
389
|
+
const tagName = opening.tagName.getText();
|
|
390
|
+
if (!/^[A-Z]/u.test(tagName)) return null;
|
|
391
|
+
const imported = imports.get(tagName);
|
|
392
|
+
if (!imported?.resolvedFilePath || imported.isServerAction) return null;
|
|
393
|
+
if (imported.resolvedFilePath.includes("/components/ui/")) return null;
|
|
394
|
+
return {
|
|
395
|
+
filePath: imported.resolvedFilePath,
|
|
396
|
+
exportName: imported.importedName,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
215
400
|
function resolveGotoRoute(callExpression) {
|
|
216
401
|
const callee = callExpression.expression;
|
|
217
402
|
if (!ts.isPropertyAccessExpression(callee)) return null;
|
|
@@ -359,6 +544,10 @@ function analyzeActionBinding(binding, options) {
|
|
|
359
544
|
};
|
|
360
545
|
}
|
|
361
546
|
|
|
547
|
+
if (imported?.resolvedFilePath) {
|
|
548
|
+
return options.analyzeImportedCallable(imported, options, new Set([imported.resolvedFilePath]));
|
|
549
|
+
}
|
|
550
|
+
|
|
362
551
|
const localFunction = options.localFunctions.get(identifier);
|
|
363
552
|
if (!localFunction) return emptyActionAnalysis();
|
|
364
553
|
return analyzeCallableLikeNode(localFunction, options, new Set([identifier]));
|
|
@@ -396,6 +585,17 @@ function analyzeCallableLikeNode(node, options, visited = new Set(), settings =
|
|
|
396
585
|
if (localCall) {
|
|
397
586
|
for (const requestEntry of localCall.requests) requests.push(requestEntry);
|
|
398
587
|
for (const serverActionRef of localCall.serverActionRefs) serverActionRefs.push(serverActionRef);
|
|
588
|
+
} else {
|
|
589
|
+
const importedCall = resolveImportedLocalCall(child, options, visited);
|
|
590
|
+
if (importedCall) {
|
|
591
|
+
for (const requestEntry of importedCall.requests) requests.push(requestEntry);
|
|
592
|
+
for (const serverActionRef of importedCall.serverActionRefs) serverActionRefs.push(serverActionRef);
|
|
593
|
+
} else {
|
|
594
|
+
for (const callbackAnalysis of resolveExecutedCallbackAnalyses(child, options, visited)) {
|
|
595
|
+
for (const requestEntry of callbackAnalysis.requests) requests.push(requestEntry);
|
|
596
|
+
for (const serverActionRef of callbackAnalysis.serverActionRefs) serverActionRefs.push(serverActionRef);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
399
599
|
}
|
|
400
600
|
}
|
|
401
601
|
}
|
|
@@ -428,6 +628,26 @@ function resolveLocalFunctionCall(callExpression, options, visited) {
|
|
|
428
628
|
return analyzeCallableLikeNode(localFunction, options, nextVisited);
|
|
429
629
|
}
|
|
430
630
|
|
|
631
|
+
function resolveImportedLocalCall(callExpression, options, visited) {
|
|
632
|
+
const callee = callExpression.expression;
|
|
633
|
+
if (!ts.isIdentifier(callee)) return null;
|
|
634
|
+
const imported = options.imports.get(callee.text);
|
|
635
|
+
if (!imported?.resolvedFilePath || imported.isServerAction) return null;
|
|
636
|
+
return options.analyzeImportedCallable(imported, options, visited);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function resolveExecutedCallbackAnalyses(callExpression, options, visited) {
|
|
640
|
+
const callbacks = [];
|
|
641
|
+
const callee = callExpression.expression;
|
|
642
|
+
if (ts.isIdentifier(callee) && (callee.text === "useEffect" || callee.text === "useLayoutEffect")) {
|
|
643
|
+
const callbackArg = callExpression.arguments[0];
|
|
644
|
+
if (callbackArg && (ts.isArrowFunction(callbackArg) || ts.isFunctionExpression(callbackArg))) {
|
|
645
|
+
callbacks.push(analyzeCallableLikeNode(callbackArg, options, new Set(visited)));
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return callbacks;
|
|
649
|
+
}
|
|
650
|
+
|
|
431
651
|
function resolveServerActionCall(callExpression, options) {
|
|
432
652
|
const callee = callExpression.expression;
|
|
433
653
|
if (!ts.isIdentifier(callee)) return null;
|
|
@@ -543,6 +763,58 @@ function collectTopLevelFunctions(sourceFile) {
|
|
|
543
763
|
return functions;
|
|
544
764
|
}
|
|
545
765
|
|
|
766
|
+
function collectExportedCallables(sourceFile, topLevelFunctions) {
|
|
767
|
+
const exports = new Map();
|
|
768
|
+
for (const statement of sourceFile.statements) {
|
|
769
|
+
if (ts.isFunctionDeclaration(statement) && statement.name && hasExportModifier(statement.modifiers)) {
|
|
770
|
+
exports.set(statement.name.text, statement);
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
if (!ts.isVariableStatement(statement) || !hasExportModifier(statement.modifiers)) continue;
|
|
775
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
776
|
+
if (!ts.isIdentifier(declaration.name) || !declaration.initializer) continue;
|
|
777
|
+
if (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer)) {
|
|
778
|
+
exports.set(declaration.name.text, declaration.initializer);
|
|
779
|
+
} else if (ts.isIdentifier(declaration.initializer)) {
|
|
780
|
+
const referenced = topLevelFunctions.get(declaration.initializer.text);
|
|
781
|
+
if (referenced) exports.set(declaration.name.text, referenced);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
return exports;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function collectScopeLocalFunctions(node) {
|
|
789
|
+
const functions = new Map();
|
|
790
|
+
const callableBody = "body" in node ? node.body : null;
|
|
791
|
+
if (!callableBody) return functions;
|
|
792
|
+
|
|
793
|
+
const visit = (child) => {
|
|
794
|
+
if (
|
|
795
|
+
child !== callableBody &&
|
|
796
|
+
(ts.isFunctionDeclaration(child) || ts.isArrowFunction(child) || ts.isFunctionExpression(child))
|
|
797
|
+
) {
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
if (ts.isFunctionDeclaration(child) && child.name) {
|
|
802
|
+
functions.set(child.name.text, child);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
if (ts.isVariableDeclaration(child) && ts.isIdentifier(child.name) && child.initializer) {
|
|
806
|
+
if (ts.isArrowFunction(child.initializer) || ts.isFunctionExpression(child.initializer)) {
|
|
807
|
+
functions.set(child.name.text, child.initializer);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
ts.forEachChild(child, visit);
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
visit(callableBody);
|
|
815
|
+
return functions;
|
|
816
|
+
}
|
|
817
|
+
|
|
546
818
|
function findDefaultPageComponent(sourceFile, localFunctions) {
|
|
547
819
|
for (const statement of sourceFile.statements) {
|
|
548
820
|
if (ts.isFunctionDeclaration(statement) && hasDefaultModifier(statement.modifiers)) {
|
|
@@ -561,6 +833,10 @@ function findDefaultPageComponent(sourceFile, localFunctions) {
|
|
|
561
833
|
return null;
|
|
562
834
|
}
|
|
563
835
|
|
|
836
|
+
function hasExportModifier(modifiers) {
|
|
837
|
+
return Boolean(modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword));
|
|
838
|
+
}
|
|
839
|
+
|
|
564
840
|
function collectJsxAttributes(openingElement) {
|
|
565
841
|
const attributes = {};
|
|
566
842
|
for (const property of openingElement.attributes.properties) {
|
|
@@ -613,7 +889,7 @@ function flattenJsxText(children) {
|
|
|
613
889
|
function extractRouteLiteral(node, normalizeRoute) {
|
|
614
890
|
const literal = extractStringLiteral(node);
|
|
615
891
|
if (!literal) return null;
|
|
616
|
-
return normalizeRoute(literal);
|
|
892
|
+
return normalizeRoute(literal.split("?")[0]);
|
|
617
893
|
}
|
|
618
894
|
|
|
619
895
|
function extractFetchMethod(node) {
|
|
@@ -631,6 +907,14 @@ function extractStringLiteral(node) {
|
|
|
631
907
|
if (!node) return null;
|
|
632
908
|
if (ts.isStringLiteralLike(node)) return node.text;
|
|
633
909
|
if (ts.isNoSubstitutionTemplateLiteral(node)) return node.text;
|
|
910
|
+
if (ts.isTemplateExpression(node)) {
|
|
911
|
+
let value = node.head.text;
|
|
912
|
+
for (const span of node.templateSpans) {
|
|
913
|
+
value += DYNAMIC_SEGMENT_TOKEN;
|
|
914
|
+
value += span.literal.text;
|
|
915
|
+
}
|
|
916
|
+
return value;
|
|
917
|
+
}
|
|
634
918
|
return null;
|
|
635
919
|
}
|
|
636
920
|
|