@elench/testkit 0.1.61 → 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 +48 -36
- package/lib/coverage/evidence.test.mjs +30 -16
- package/lib/coverage/graph-builder.mjs +18 -3
- package/lib/coverage/index.test.mjs +303 -0
- package/lib/coverage/next-discovery.mjs +36 -5
- package/lib/coverage/next-static-analysis.mjs +445 -126
- package/lib/coverage/shared.mjs +68 -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 +108 -2
- package/node_modules/@elench/testkit-bridge/src/index.test.mjs +212 -16
- package/node_modules/@elench/testkit-protocol/package.json +1 -1
- package/node_modules/@elench/testkit-protocol/src/index.d.ts +26 -0
- package/node_modules/@elench/testkit-protocol/src/index.mjs +18 -0
- package/node_modules/@elench/testkit-protocol/src/index.test.mjs +75 -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,163 +54,366 @@ 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
|
-
|
|
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
|
+
};
|
|
82
|
+
|
|
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);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
nodes: dedupeById(nodes),
|
|
95
|
+
edges: dedupeById(edges),
|
|
96
|
+
requests: directRequests,
|
|
97
|
+
serverActionRefs: dedupeServerActionRefs(directServerActionRefs),
|
|
98
|
+
surfacesByTargetValue,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function extractHttpSuiteRequests(content, filePath = "suite.int.testkit.ts") {
|
|
103
|
+
const sourceFile = createSourceFile(filePath, content);
|
|
104
|
+
const requests = [];
|
|
105
|
+
|
|
106
|
+
const visit = (node) => {
|
|
107
|
+
if (ts.isCallExpression(node)) {
|
|
108
|
+
const request = resolveHttpRequestCall(node, { normalizeRoute: defaultNormalizeRoute });
|
|
109
|
+
if (request) requests.push(request);
|
|
54
110
|
}
|
|
55
|
-
|
|
56
|
-
|
|
111
|
+
ts.forEachChild(node, visit);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
visit(sourceFile);
|
|
115
|
+
return dedupeRequests(requests);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function extractPlaywrightVisitedRoutes(content, filePath = "suite.pw.testkit.ts") {
|
|
119
|
+
const sourceFile = createSourceFile(filePath, content);
|
|
120
|
+
const routes = [];
|
|
121
|
+
|
|
122
|
+
const visit = (node) => {
|
|
123
|
+
if (ts.isCallExpression(node)) {
|
|
124
|
+
const route = resolveGotoRoute(node);
|
|
125
|
+
if (route) routes.push(route);
|
|
126
|
+
}
|
|
127
|
+
ts.forEachChild(node, visit);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
visit(sourceFile);
|
|
131
|
+
return [...new Set(routes)];
|
|
132
|
+
}
|
|
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,
|
|
57
175
|
node,
|
|
58
176
|
method: request.method,
|
|
59
177
|
path: request.path,
|
|
60
178
|
});
|
|
61
|
-
edges.push({
|
|
62
|
-
id: `requests
|
|
179
|
+
analyzerContext.state.edges.push({
|
|
180
|
+
id: `requests:${analyzerContext.pageNodeId}:${node.id}`,
|
|
63
181
|
kind: "requests",
|
|
64
|
-
from:
|
|
182
|
+
from: analyzerContext.pageNodeId,
|
|
65
183
|
to: node.id,
|
|
66
184
|
confidence: request.confidence,
|
|
67
185
|
});
|
|
68
186
|
}
|
|
69
187
|
|
|
70
|
-
for (const serverActionRef of
|
|
71
|
-
directServerActionRefs.push({
|
|
72
|
-
originNodeId:
|
|
188
|
+
for (const serverActionRef of actionAnalysis.serverActionRefs) {
|
|
189
|
+
analyzerContext.state.directServerActionRefs.push({
|
|
190
|
+
originNodeId: analyzerContext.pageNodeId,
|
|
73
191
|
...serverActionRef,
|
|
74
192
|
});
|
|
75
193
|
}
|
|
76
194
|
|
|
77
|
-
walkJsx(
|
|
195
|
+
walkJsx(callable, (element, formContext) => {
|
|
78
196
|
const descriptor = describeSurface(element, formContext);
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
};
|
|
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
|
+
};
|
|
96
213
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
+
});
|
|
105
222
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
223
|
+
if (descriptor.target?.kind === "testId") {
|
|
224
|
+
analyzerContext.state.surfacesByTargetValue.set(descriptor.target.value, surfaceNode);
|
|
225
|
+
}
|
|
109
226
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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,
|
|
125
251
|
});
|
|
126
|
-
}
|
|
127
252
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
+
});
|
|
135
261
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
+
}
|
|
141
287
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
288
|
+
for (const serverActionRef of bindingAnalysis.serverActionRefs) {
|
|
289
|
+
analyzerContext.state.directServerActionRefs.push({
|
|
290
|
+
originNodeId: actionId,
|
|
291
|
+
...serverActionRef,
|
|
292
|
+
});
|
|
147
293
|
}
|
|
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
294
|
}
|
|
162
295
|
|
|
163
|
-
|
|
164
|
-
directServerActionRefs.push({
|
|
165
|
-
originNodeId: actionId,
|
|
166
|
-
...serverActionRef,
|
|
167
|
-
});
|
|
168
|
-
}
|
|
296
|
+
return descriptor.formContext ?? formContext;
|
|
169
297
|
}
|
|
170
298
|
|
|
171
|
-
|
|
299
|
+
const componentRef = resolveImportedComponentRef(element, moduleInfo.imports);
|
|
300
|
+
if (componentRef) {
|
|
301
|
+
analyzeImportedComponent(componentRef, analyzerContext);
|
|
302
|
+
}
|
|
303
|
+
return formContext;
|
|
172
304
|
});
|
|
305
|
+
}
|
|
173
306
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
serverActionRefs: dedupeServerActionRefs(directServerActionRefs),
|
|
179
|
-
surfacesByTargetValue,
|
|
180
|
-
};
|
|
307
|
+
function analyzeImportedComponent(componentRef, analyzerContext) {
|
|
308
|
+
const moduleInfo = loadModuleInfoFromFile(componentRef.filePath, analyzerContext);
|
|
309
|
+
if (!moduleInfo) return;
|
|
310
|
+
analyzeModuleExport(moduleInfo, componentRef.exportName, analyzerContext);
|
|
181
311
|
}
|
|
182
312
|
|
|
183
|
-
|
|
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
|
+
|
|
184
355
|
const sourceFile = createSourceFile(filePath, content);
|
|
185
|
-
const
|
|
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
|
+
}
|
|
186
374
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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,
|
|
193
397
|
};
|
|
398
|
+
}
|
|
194
399
|
|
|
195
|
-
|
|
196
|
-
|
|
400
|
+
function resolveGotoRoute(callExpression) {
|
|
401
|
+
const callee = callExpression.expression;
|
|
402
|
+
if (!ts.isPropertyAccessExpression(callee)) return null;
|
|
403
|
+
if (callee.name.text !== "goto") return null;
|
|
404
|
+
const pathArg = callExpression.arguments[0];
|
|
405
|
+
const literal = extractStringLiteral(pathArg);
|
|
406
|
+
if (!literal) return null;
|
|
407
|
+
if (/^https?:\/\//u.test(literal)) {
|
|
408
|
+
try {
|
|
409
|
+
const parsed = new URL(literal);
|
|
410
|
+
if (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1") {
|
|
411
|
+
return defaultNormalizeRoute(parsed.pathname.split("?")[0]);
|
|
412
|
+
}
|
|
413
|
+
} catch { /* ignore malformed */ }
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
return defaultNormalizeRoute(literal.split("?")[0]);
|
|
197
417
|
}
|
|
198
418
|
|
|
199
419
|
function walkJsx(sourceFile, visitor) {
|
|
@@ -324,6 +544,10 @@ function analyzeActionBinding(binding, options) {
|
|
|
324
544
|
};
|
|
325
545
|
}
|
|
326
546
|
|
|
547
|
+
if (imported?.resolvedFilePath) {
|
|
548
|
+
return options.analyzeImportedCallable(imported, options, new Set([imported.resolvedFilePath]));
|
|
549
|
+
}
|
|
550
|
+
|
|
327
551
|
const localFunction = options.localFunctions.get(identifier);
|
|
328
552
|
if (!localFunction) return emptyActionAnalysis();
|
|
329
553
|
return analyzeCallableLikeNode(localFunction, options, new Set([identifier]));
|
|
@@ -361,6 +585,17 @@ function analyzeCallableLikeNode(node, options, visited = new Set(), settings =
|
|
|
361
585
|
if (localCall) {
|
|
362
586
|
for (const requestEntry of localCall.requests) requests.push(requestEntry);
|
|
363
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
|
+
}
|
|
364
599
|
}
|
|
365
600
|
}
|
|
366
601
|
}
|
|
@@ -393,6 +628,26 @@ function resolveLocalFunctionCall(callExpression, options, visited) {
|
|
|
393
628
|
return analyzeCallableLikeNode(localFunction, options, nextVisited);
|
|
394
629
|
}
|
|
395
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
|
+
|
|
396
651
|
function resolveServerActionCall(callExpression, options) {
|
|
397
652
|
const callee = callExpression.expression;
|
|
398
653
|
if (!ts.isIdentifier(callee)) return null;
|
|
@@ -508,6 +763,58 @@ function collectTopLevelFunctions(sourceFile) {
|
|
|
508
763
|
return functions;
|
|
509
764
|
}
|
|
510
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
|
+
|
|
511
818
|
function findDefaultPageComponent(sourceFile, localFunctions) {
|
|
512
819
|
for (const statement of sourceFile.statements) {
|
|
513
820
|
if (ts.isFunctionDeclaration(statement) && hasDefaultModifier(statement.modifiers)) {
|
|
@@ -526,6 +833,10 @@ function findDefaultPageComponent(sourceFile, localFunctions) {
|
|
|
526
833
|
return null;
|
|
527
834
|
}
|
|
528
835
|
|
|
836
|
+
function hasExportModifier(modifiers) {
|
|
837
|
+
return Boolean(modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword));
|
|
838
|
+
}
|
|
839
|
+
|
|
529
840
|
function collectJsxAttributes(openingElement) {
|
|
530
841
|
const attributes = {};
|
|
531
842
|
for (const property of openingElement.attributes.properties) {
|
|
@@ -578,7 +889,7 @@ function flattenJsxText(children) {
|
|
|
578
889
|
function extractRouteLiteral(node, normalizeRoute) {
|
|
579
890
|
const literal = extractStringLiteral(node);
|
|
580
891
|
if (!literal) return null;
|
|
581
|
-
return normalizeRoute(literal);
|
|
892
|
+
return normalizeRoute(literal.split("?")[0]);
|
|
582
893
|
}
|
|
583
894
|
|
|
584
895
|
function extractFetchMethod(node) {
|
|
@@ -596,6 +907,14 @@ function extractStringLiteral(node) {
|
|
|
596
907
|
if (!node) return null;
|
|
597
908
|
if (ts.isStringLiteralLike(node)) return node.text;
|
|
598
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
|
+
}
|
|
599
918
|
return null;
|
|
600
919
|
}
|
|
601
920
|
|