@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.
@@ -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
- const sourceFile = createSourceFile(filePath, content);
27
- const imports = collectImports(sourceFile, {
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
- const localFunctions = collectTopLevelFunctions(sourceFile);
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 defaultPageComponent = findDefaultPageComponent(sourceFile, localFunctions);
41
- const directPageAnalysis = defaultPageComponent
42
- ? analyzeCallableLikeNode(defaultPageComponent, {
43
- imports,
44
- localFunctions,
45
- normalizeRoute,
46
- }, new Set(), { skipNestedFunctions: true })
47
- : emptyActionAnalysis();
48
-
49
- for (const request of directPageAnalysis.requests) {
50
- const node = createClientRequestNode(serviceName, filePath, request, `page:${route}`);
51
- if (!requestNodeIds.has(node.id)) {
52
- requestNodeIds.add(node.id);
53
- nodes.push(node);
54
- }
55
- directRequests.push({
56
- originNodeId: `page_view:${serviceName}:${route}`,
57
- node,
58
- method: request.method,
59
- path: request.path,
60
- });
61
- edges.push({
62
- id: `requests:page_view:${serviceName}:${route}:${node.id}`,
63
- kind: "requests",
64
- from: `page_view:${serviceName}:${route}`,
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 serverActionRef of directPageAnalysis.serverActionRefs) {
71
- directServerActionRefs.push({
72
- originNodeId: `page_view:${serviceName}:${route}`,
73
- ...serverActionRef,
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