@skyramp/mcp 0.3.8 → 0.4.0-rc.1
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/build/commands/commandLibrary.d.ts +1 -1
- package/build/commands/commandLibrary.js +3 -3
- package/build/commands/recommendTestsAndExecuteCommand.d.ts +1 -1
- package/build/commands/recommendTestsAndExecuteCommand.js +35 -20
- package/build/commands/testThisEndpointCommand.js +35 -19
- package/build/index.js +9 -3
- package/build/playwright/blueprintDigest.d.ts +15 -0
- package/build/playwright/blueprintDigest.js +152 -0
- package/build/playwright/blueprintDigestStore.d.ts +31 -0
- package/build/playwright/blueprintDigestStore.js +117 -0
- package/build/playwright/registerPlaywrightTools.js +60 -12
- package/build/playwright/traceRecordingPrompt.js +8 -7
- package/build/prompts/enhance-assertions/sharedAssertionRules.js +9 -8
- package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +24 -2
- package/build/prompts/promptAssets.d.ts +20 -0
- package/build/prompts/promptAssets.js +55 -0
- package/build/prompts/sut-setup/modes/dockerComposePrompt.js +19 -5
- package/build/prompts/test-maintenance/actionsInstructions.d.ts +4 -0
- package/build/prompts/test-maintenance/actionsInstructions.js +14 -2
- package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +0 -10
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +2 -11
- package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +8 -4
- package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +5 -22
- package/build/prompts/test-recommendation/diffExecutionPlan.js +37 -465
- package/build/prompts/test-recommendation/recommendationSections.d.ts +7 -17
- package/build/prompts/test-recommendation/recommendationSections.js +67 -309
- package/build/prompts/test-recommendation/recommendationShared.d.ts +19 -47
- package/build/prompts/test-recommendation/recommendationShared.js +49 -155
- package/build/prompts/test-recommendation/registerRecommendTestsPrompt.d.ts +0 -5
- package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +10 -153
- package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +2 -29
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +32 -457
- package/build/prompts/testbot/planDeclarations.d.ts +6 -0
- package/build/prompts/testbot/planDeclarations.js +9 -0
- package/build/prompts/testbot/testbot-prompts.d.ts +8 -0
- package/build/prompts/testbot/testbot-prompts.js +256 -381
- package/build/recommendation/answers.d.ts +35 -0
- package/build/recommendation/answers.js +96 -0
- package/build/recommendation/registerPlan.d.ts +49 -0
- package/build/recommendation/registerPlan.js +117 -0
- package/build/recommendation/runVerifiers.d.ts +10 -0
- package/build/recommendation/runVerifiers.js +49 -0
- package/build/recommendation/subjectStep.d.ts +42 -0
- package/build/recommendation/subjectStep.js +86 -0
- package/build/recommendation/types.d.ts +163 -0
- package/build/recommendation/types.js +20 -0
- package/build/recommendation/verifierContracts.d.ts +382 -0
- package/build/recommendation/verifierContracts.js +263 -0
- package/build/recommendation/verifiers/changedFile.d.ts +2 -0
- package/build/recommendation/verifiers/changedFile.js +82 -0
- package/build/recommendation/verifiers/citedPath.d.ts +12 -0
- package/build/recommendation/verifiers/citedPath.js +35 -0
- package/build/recommendation/verifiers/coverage.d.ts +7 -0
- package/build/recommendation/verifiers/coverage.js +617 -0
- package/build/recommendation/verifiers/deliveredMatchesPlan.d.ts +11 -0
- package/build/recommendation/verifiers/deliveredMatchesPlan.js +33 -0
- package/build/recommendation/verifiers/endpointGrounded.d.ts +17 -0
- package/build/recommendation/verifiers/endpointGrounded.js +128 -0
- package/build/recommendation/verifiers/existingCoverage.d.ts +6 -0
- package/build/recommendation/verifiers/existingCoverage.js +51 -0
- package/build/recommendation/verifiers/expectedOutcome.d.ts +31 -0
- package/build/recommendation/verifiers/expectedOutcome.js +105 -0
- package/build/recommendation/verifiers/removedElementGuarded.d.ts +2 -0
- package/build/recommendation/verifiers/removedElementGuarded.js +57 -0
- package/build/recommendation/verifiers/reportedCategory.d.ts +26 -0
- package/build/recommendation/verifiers/reportedCategory.js +84 -0
- package/build/recommendation/verifiers/screenRoute.d.ts +10 -0
- package/build/recommendation/verifiers/screenRoute.js +118 -0
- package/build/recommendation/verifiers/statedDifference.d.ts +6 -0
- package/build/recommendation/verifiers/statedDifference.js +140 -0
- package/build/recommendation/verifiers/uiElementGrounded.d.ts +7 -0
- package/build/recommendation/verifiers/uiElementGrounded.js +318 -0
- package/build/resources/analysisResources.js +1 -114
- package/build/resources/testbotResource.js +23 -13
- package/build/services/ModularizationService.js +2 -1
- package/build/services/TestDiscoveryService.d.ts +3 -72
- package/build/services/TestDiscoveryService.js +10 -303
- package/build/services/containerEnv.d.ts +1 -1
- package/build/services/containerEnv.js +12 -0
- package/build/skills/fixTestImportErrorsSkill.d.ts +13 -0
- package/build/skills/fixTestImportErrorsSkill.js +20 -0
- package/build/toolNames.d.ts +1 -0
- package/build/toolNames.js +1 -0
- package/build/tools/code-refactor/enhanceAssertionsTool.js +3 -3
- package/build/tools/code-refactor/modularizationTool.js +2 -1
- package/build/tools/executeSkyrampTestTool.d.ts +80 -0
- package/build/tools/executeSkyrampTestTool.js +246 -19
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +6 -0
- package/build/tools/generate-tests/generateContractRestTool.js +3 -3
- package/build/tools/generate-tests/planGuard.d.ts +2 -2
- package/build/tools/generate-tests/planGuard.js +78 -18
- package/build/tools/one-click/oneClickTool.d.ts +0 -1
- package/build/tools/one-click/oneClickTool.js +0 -5
- package/build/tools/submitReportTool.d.ts +48 -42
- package/build/tools/submitReportTool.js +576 -193
- package/build/tools/test-management/actionsTool.js +72 -4
- package/build/tools/test-management/analyzeChangesTool.d.ts +144 -48
- package/build/tools/test-management/analyzeChangesTool.js +212 -1219
- package/build/tools/test-management/analyzeTestHealthTool.js +13 -24
- package/build/tools/test-management/index.d.ts +1 -0
- package/build/tools/test-management/index.js +1 -0
- package/build/tools/test-management/registerTestPlanTool.d.ts +795 -172
- package/build/tools/test-management/registerTestPlanTool.js +609 -542
- package/build/tools/test-management/resolveScreenTool.d.ts +75 -0
- package/build/tools/test-management/resolveScreenTool.js +289 -0
- package/build/types/BlueprintDigest.d.ts +34 -0
- package/build/types/BlueprintDigest.js +1 -0
- package/build/types/RepositoryAnalysis.d.ts +20 -1559
- package/build/types/RepositoryAnalysis.js +2 -58
- package/build/types/StepMethod.d.ts +40 -0
- package/build/types/StepMethod.js +77 -0
- package/build/types/TestAnalysis.d.ts +12 -0
- package/build/types/TestExecution.d.ts +4 -0
- package/build/types/TestRecommendation.d.ts +24 -24
- package/build/types/TestRecommendation.js +91 -89
- package/build/types/TestbotPromptOptions.d.ts +0 -4
- package/build/types/TestbotReport.d.ts +64 -2
- package/build/utils/AnalysisStateManager.d.ts +79 -113
- package/build/utils/AnalysisStateManager.js +147 -57
- package/build/utils/assertion-verify/api-shared-lints.js +1 -1
- package/build/utils/assertion-verify/metrics.js +85 -36
- package/build/utils/assertion-verify/ui-lints.d.ts +0 -5
- package/build/utils/assertion-verify/ui-lints.js +32 -0
- package/build/utils/branchDiff.d.ts +63 -31
- package/build/utils/branchDiff.js +242 -94
- package/build/utils/containedPath.d.ts +18 -0
- package/build/utils/containedPath.js +73 -0
- package/build/utils/dartRouteExtractor.d.ts +18 -34
- package/build/utils/dartRouteExtractor.js +101 -173
- package/build/utils/featureFlags.d.ts +12 -0
- package/build/utils/featureFlags.js +14 -0
- package/build/utils/frontendSelectors.d.ts +48 -27
- package/build/utils/frontendSelectors.js +241 -80
- package/build/utils/pathMatching.d.ts +2 -4
- package/build/utils/pathMatching.js +2 -4
- package/build/utils/planMatchKeys.d.ts +38 -47
- package/build/utils/planMatchKeys.js +143 -81
- package/build/utils/rebaselineSnapshots.d.ts +24 -0
- package/build/utils/rebaselineSnapshots.js +65 -0
- package/build/utils/removedUiElements.d.ts +22 -0
- package/build/utils/removedUiElements.js +106 -0
- package/build/utils/reportVerification.d.ts +2 -6
- package/build/utils/reportVerification.js +61 -2
- package/build/utils/screenRoutes.d.ts +66 -0
- package/build/utils/screenRoutes.js +727 -0
- package/build/utils/sourceRouteExtractor.js +320 -112
- package/build/utils/testFileClassification.d.ts +11 -2
- package/build/utils/testFileClassification.js +44 -2
- package/build/utils/testFixtures.d.ts +5 -0
- package/build/utils/testFixtures.js +13 -0
- package/build/utils/utils.d.ts +0 -1
- package/build/utils/utils.js +0 -11
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/build/workspace/workspace.d.ts +12 -12
- package/node_modules/playwright/lib/mcp/skyramp/assertHiddenTool.js +56 -0
- package/node_modules/playwright/lib/mcp/skyramp/assertTool.js +2 -1
- package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +10 -0
- package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +4 -1
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +160 -1
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +4 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +1 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderSignalProcessor.js +2 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder.js +5 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.-Id052Lr.js → index.B7KbSQcC.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +1 -1
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderSignalProcessor.ts +7 -0
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder.ts +6 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +4 -3
- package/plugin/.claude-plugin/plugin.json +8 -0
- package/plugin/plugin.json +6 -0
- package/plugin/prompts/declaring-a-plan.md +20 -0
- package/plugin/prompts/generate-tests/context-fetching.md +4 -0
- package/plugin/prompts/generate-tests/execution-plan.md +63 -0
- package/plugin/prompts/generate-tests/generation.md +108 -0
- package/plugin/prompts/generate-tests/path-parameters.md +1 -0
- package/plugin/prompts/generate-tests/reasoning-protocol.md +17 -0
- package/plugin/prompts/generate-tests/tool-workflow-variants.md +61 -0
- package/plugin/prompts/generate-tests/tool-workflows.md +65 -0
- package/plugin/prompts/plan-tests.md +42 -0
- package/plugin/prompts/testbot-task1.md +82 -0
- package/plugin/skills/fix-test-import-errors/SKILL.md +98 -0
- package/build/prompts/test-recommendation/analysisOutputPrompt.d.ts +0 -84
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +0 -369
- package/build/prompts/test-recommendation/fullRepoCatalog.d.ts +0 -7
- package/build/prompts/test-recommendation/fullRepoCatalog.js +0 -283
- package/build/prompts/test-recommendation/scopeAssessment.d.ts +0 -81
- package/build/prompts/test-recommendation/scopeAssessment.js +0 -359
- package/build/recommendation/budgeters/diversityBalancedBudgeter.d.ts +0 -7
- package/build/recommendation/budgeters/diversityBalancedBudgeter.js +0 -105
- package/build/recommendation/budgeters/fixedNBudgeter.d.ts +0 -7
- package/build/recommendation/budgeters/fixedNBudgeter.js +0 -11
- package/build/recommendation/budgeters/shared.d.ts +0 -32
- package/build/recommendation/budgeters/shared.js +0 -246
- package/build/recommendation/discriminators.d.ts +0 -37
- package/build/recommendation/discriminators.js +0 -379
- package/build/recommendation/diversity.d.ts +0 -47
- package/build/recommendation/diversity.js +0 -101
- package/build/recommendation/planRanker.d.ts +0 -65
- package/build/recommendation/planRanker.js +0 -83
- package/build/recommendation/testFixtures.d.ts +0 -25
- package/build/recommendation/testFixtures.js +0 -45
- package/build/types/FrontendIntegration.d.ts +0 -28
- package/build/types/FrontendIntegration.js +0 -22
- package/build/types/Recommendation.d.ts +0 -146
- package/build/types/Recommendation.js +0 -74
- package/build/utils/changedRoutes.d.ts +0 -29
- package/build/utils/changedRoutes.js +0 -87
- package/build/utils/frontendIntegration.d.ts +0 -9
- package/build/utils/frontendIntegration.js +0 -243
- package/build/utils/importerHop.d.ts +0 -135
- package/build/utils/importerHop.js +0 -489
- package/build/utils/pathAffinityClassification.d.ts +0 -49
- package/build/utils/pathAffinityClassification.js +0 -180
- package/build/utils/pythonMountPrefixes.d.ts +0 -25
- package/build/utils/pythonMountPrefixes.js +0 -347
- package/build/utils/repoScanner.d.ts +0 -34
- package/build/utils/repoScanner.js +0 -300
- package/build/utils/routeParsers.d.ts +0 -95
- package/build/utils/routeParsers.js +0 -951
- package/build/utils/scenarioDrafting.d.ts +0 -92
- package/build/utils/scenarioDrafting.js +0 -951
- package/build/utils/subjectEndpoints.d.ts +0 -19
- package/build/utils/subjectEndpoints.js +0 -98
- package/build/utils/uiPageEnumerator.d.ts +0 -172
- package/build/utils/uiPageEnumerator.js +0 -474
|
@@ -32,7 +32,7 @@ import * as path from "path";
|
|
|
32
32
|
import * as ts from "typescript";
|
|
33
33
|
import { collectFiles } from "./fileWalk.js";
|
|
34
34
|
function makeParseCache() {
|
|
35
|
-
return { files: new Map(),
|
|
35
|
+
return { files: new Map(), tsconfig: new Map() };
|
|
36
36
|
}
|
|
37
37
|
/** Recursion cap for cross-file constant resolution. */
|
|
38
38
|
const MAX_RESOLVE_DEPTH = 4;
|
|
@@ -155,9 +155,7 @@ function extractRoutesFromFile(filePath, cache) {
|
|
|
155
155
|
visit(sf, (node) => {
|
|
156
156
|
// 1. JSX route elements: <Route path="…" element={<X/>} />
|
|
157
157
|
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
158
|
-
|
|
159
|
-
if (r)
|
|
160
|
-
routes.push(r);
|
|
158
|
+
routes.push(...jsxRoutesFromNode(node, filePath, imports, localConsts, cache));
|
|
161
159
|
return;
|
|
162
160
|
}
|
|
163
161
|
// 2. defineModule({ id, routes: [...] }) — Directus per-module aggregation.
|
|
@@ -179,37 +177,66 @@ function extractRoutesFromFile(filePath, cache) {
|
|
|
179
177
|
routes.push(...vueRouterRoutesFromFile(sf, filePath, imports, localConsts, cache));
|
|
180
178
|
return routes;
|
|
181
179
|
}
|
|
182
|
-
function
|
|
180
|
+
function jsxRoutesFromNode(node, filePath, imports, localConsts, cache) {
|
|
183
181
|
const tag = jsxTagOf(node);
|
|
184
182
|
if (!tag || !ROUTE_TAG_NAMES.has(tag))
|
|
185
|
-
return
|
|
183
|
+
return [];
|
|
186
184
|
const attrs = jsxAttributesOf(node);
|
|
185
|
+
const componentAttr = attrs.find((a) => a.name === "element" || a.name === "component");
|
|
186
|
+
if (!componentAttr || componentAttr.kind !== "identifier")
|
|
187
|
+
return [];
|
|
188
|
+
const mountedAt = (routePath) => ({
|
|
189
|
+
path: routePath,
|
|
190
|
+
componentName: componentAttr.value,
|
|
191
|
+
componentFile: imports.get(componentAttr.value)?.filePath,
|
|
192
|
+
declaredIn: filePath,
|
|
193
|
+
});
|
|
194
|
+
// React Router child paths are relative to the enclosing <Route>; compose
|
|
195
|
+
// with the ancestor chain so nested routes yield full navigable URLs.
|
|
196
|
+
const parentPrefix = enclosingJsxRoutePath(node, filePath, imports, localConsts, cache);
|
|
187
197
|
const pathAttr = attrs.find((a) => a.name === "path");
|
|
188
|
-
let ownPath;
|
|
189
198
|
if (pathAttr) {
|
|
190
|
-
ownPath = resolveAttrToString(pathAttr, filePath, imports, localConsts, cache);
|
|
199
|
+
const ownPath = resolveAttrToString(pathAttr, filePath, imports, localConsts, cache);
|
|
191
200
|
if (ownPath === null)
|
|
192
|
-
return
|
|
201
|
+
return [];
|
|
202
|
+
return [mountedAt(composeRoutePath(parentPrefix, ownPath))];
|
|
193
203
|
}
|
|
194
|
-
|
|
204
|
+
if (attrs.some((a) => a.name === "index")) {
|
|
195
205
|
// <Route index> has no path — it renders at the parent route's path.
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
else {
|
|
199
|
-
return null;
|
|
206
|
+
return [mountedAt(composeRoutePath(parentPrefix, ""))];
|
|
200
207
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
208
|
+
// A pathless layout route has no URL of its own: it renders on every route
|
|
209
|
+
// nested under it. Reporting nothing would read as "no route renders this
|
|
210
|
+
// file", the one answer that is definitely wrong for a layout, and the parent
|
|
211
|
+
// path on its own would be a URL the app may not serve. Report the pages it
|
|
212
|
+
// appears on, the way readTanStackRouteTree reports a pathless TanStack layout.
|
|
213
|
+
return nestedJsxRoutePaths(node, filePath, imports, localConsts, cache).map(mountedAt);
|
|
214
|
+
}
|
|
215
|
+
/** The composed paths of the routes written inside a JSX route element. */
|
|
216
|
+
function nestedJsxRoutePaths(node, filePath, imports, localConsts, cache) {
|
|
217
|
+
const element = node.parent;
|
|
218
|
+
if (!ts.isJsxElement(element) || element.openingElement !== node)
|
|
219
|
+
return [];
|
|
220
|
+
const paths = [];
|
|
221
|
+
visit(element, (inner) => {
|
|
222
|
+
if (inner === node)
|
|
223
|
+
return;
|
|
224
|
+
if (!ts.isJsxOpeningElement(inner) && !ts.isJsxSelfClosingElement(inner))
|
|
225
|
+
return;
|
|
226
|
+
const tag = jsxTagOf(inner);
|
|
227
|
+
if (!tag || !ROUTE_TAG_NAMES.has(tag))
|
|
228
|
+
return;
|
|
229
|
+
const pathAttr = jsxAttributesOf(inner).find((a) => a.name === "path");
|
|
230
|
+
if (!pathAttr)
|
|
231
|
+
return;
|
|
232
|
+
const ownPath = resolveAttrToString(pathAttr, filePath, imports, localConsts, cache);
|
|
233
|
+
if (ownPath === null)
|
|
234
|
+
return;
|
|
235
|
+
const composed = composeRoutePath(enclosingJsxRoutePath(inner, filePath, imports, localConsts, cache), ownPath);
|
|
236
|
+
if (!paths.includes(composed))
|
|
237
|
+
paths.push(composed);
|
|
238
|
+
});
|
|
239
|
+
return paths;
|
|
213
240
|
}
|
|
214
241
|
/**
|
|
215
242
|
* Walk a JSX route node's ancestor chain and compose the paths of the enclosing
|
|
@@ -323,36 +350,19 @@ function parseExpressionAttr(name, expr) {
|
|
|
323
350
|
* import { WORKSPACE_URL } from "constants/routes" // baseUrl-resolved
|
|
324
351
|
*
|
|
325
352
|
* Bare imports (`react-router-dom`) are skipped — those aren't local files.
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
* subtrees, which downgrades to "couldn't resolve, skip the route" — safe.
|
|
353
|
+
* Aliased imports (`@/components/X`) go through tsconfig `paths`; an alias with
|
|
354
|
+
* no matching pattern downgrades to "couldn't resolve, skip the route" — safe.
|
|
329
355
|
*/
|
|
330
356
|
function collectImports(sf, filePath, cache) {
|
|
331
357
|
const result = new Map();
|
|
332
358
|
const fileDir = path.dirname(filePath);
|
|
333
|
-
const baseUrl = findTsconfigBaseUrl(fileDir, cache);
|
|
334
359
|
for (const stmt of sf.statements) {
|
|
335
360
|
if (!ts.isImportDeclaration(stmt))
|
|
336
361
|
continue;
|
|
337
362
|
const moduleSpec = stmt.moduleSpecifier;
|
|
338
363
|
if (!ts.isStringLiteral(moduleSpec))
|
|
339
364
|
continue;
|
|
340
|
-
const
|
|
341
|
-
let resolved;
|
|
342
|
-
if (spec.startsWith("/")) {
|
|
343
|
-
// Absolute import specs would let a hostile repo point the analyzer
|
|
344
|
-
// at arbitrary host paths during route extraction (e.g. /etc/passwd).
|
|
345
|
-
// Real codebases don't use literal absolute imports — they use
|
|
346
|
-
// relative paths or tsconfig path aliases.
|
|
347
|
-
continue;
|
|
348
|
-
}
|
|
349
|
-
if (spec.startsWith(".")) {
|
|
350
|
-
resolved = resolveImportPath(fileDir, spec);
|
|
351
|
-
}
|
|
352
|
-
else if (baseUrl) {
|
|
353
|
-
// Try baseUrl resolution for non-relative imports.
|
|
354
|
-
resolved = resolveImportPath(baseUrl, spec);
|
|
355
|
-
}
|
|
365
|
+
const resolved = resolveModuleSpecifier(moduleSpec.text, fileDir, cache);
|
|
356
366
|
if (!resolved)
|
|
357
367
|
continue;
|
|
358
368
|
const clause = stmt.importClause;
|
|
@@ -494,7 +504,6 @@ function resolveExportedSymbolToString(filePath, exportedName, cache, depth) {
|
|
|
494
504
|
const imports = collectImports(sf, filePath, cache);
|
|
495
505
|
const localConsts = collectLocalConsts(sf);
|
|
496
506
|
const fileDir = path.dirname(filePath);
|
|
497
|
-
const baseUrl = findTsconfigBaseUrl(fileDir, cache);
|
|
498
507
|
// 1. Look for `export const X = …` directly in this file.
|
|
499
508
|
for (const stmt of sf.statements) {
|
|
500
509
|
if (!ts.isVariableStatement(stmt))
|
|
@@ -547,17 +556,7 @@ function resolveExportedSymbolToString(filePath, exportedName, cache, depth) {
|
|
|
547
556
|
if (!stmt.moduleSpecifier || !ts.isStringLiteral(stmt.moduleSpecifier))
|
|
548
557
|
continue;
|
|
549
558
|
const targetSpec = stmt.moduleSpecifier.text;
|
|
550
|
-
|
|
551
|
-
if (targetSpec.startsWith("/")) {
|
|
552
|
-
// Absolute paths rejected for the same reason as collectImports.
|
|
553
|
-
continue;
|
|
554
|
-
}
|
|
555
|
-
if (targetSpec.startsWith(".")) {
|
|
556
|
-
targetFile = resolveImportPath(fileDir, targetSpec);
|
|
557
|
-
}
|
|
558
|
-
else if (baseUrl) {
|
|
559
|
-
targetFile = resolveImportPath(baseUrl, targetSpec);
|
|
560
|
-
}
|
|
559
|
+
const targetFile = resolveModuleSpecifier(targetSpec, fileDir, cache);
|
|
561
560
|
if (!targetFile)
|
|
562
561
|
continue;
|
|
563
562
|
if (stmt.exportClause && ts.isNamedExports(stmt.exportClause)) {
|
|
@@ -581,18 +580,43 @@ function resolveExportedSymbolToString(filePath, exportedName, cache, depth) {
|
|
|
581
580
|
return null;
|
|
582
581
|
}
|
|
583
582
|
const IMPORT_EXTENSIONS = [".tsx", ".jsx", ".ts", ".js", ".vue"];
|
|
583
|
+
/**
|
|
584
|
+
* TypeScript's `moduleResolution: node16/nodenext` requires an ESM import to
|
|
585
|
+
* name the file the compiler EMITS, so a `.tsx` component is imported as
|
|
586
|
+
* `./Page.js`. Appending extensions to that specifier only ever tries
|
|
587
|
+
* `Page.js.tsx`, so the TS source has to be looked up under its own extension.
|
|
588
|
+
*/
|
|
589
|
+
const TS_SOURCE_EXTENSIONS = {
|
|
590
|
+
".js": [".ts", ".tsx"],
|
|
591
|
+
".jsx": [".tsx"],
|
|
592
|
+
".mjs": [".mts"],
|
|
593
|
+
".cjs": [".cts"],
|
|
594
|
+
};
|
|
584
595
|
function resolveImportPath(fromDir, spec) {
|
|
585
|
-
|
|
596
|
+
return resolveFilePath(path.resolve(fromDir, spec));
|
|
597
|
+
}
|
|
598
|
+
/** The file an already-joined absolute module path names, if any. */
|
|
599
|
+
function resolveFilePath(base) {
|
|
586
600
|
// 1. Exact file (with the extension already in spec).
|
|
587
601
|
if (fileExistsSync(base))
|
|
588
602
|
return base;
|
|
589
|
-
// 2.
|
|
603
|
+
// 2. A JavaScript extension standing in for the TypeScript source.
|
|
604
|
+
const jsExt = /\.(?:js|jsx|mjs|cjs)$/i.exec(base);
|
|
605
|
+
if (jsExt) {
|
|
606
|
+
const stem = base.slice(0, base.length - jsExt[0].length);
|
|
607
|
+
for (const ext of TS_SOURCE_EXTENSIONS[jsExt[0].toLowerCase()] ?? []) {
|
|
608
|
+
const candidate = stem + ext;
|
|
609
|
+
if (fileExistsSync(candidate))
|
|
610
|
+
return candidate;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
// 3. Try each known extension.
|
|
590
614
|
for (const ext of IMPORT_EXTENSIONS) {
|
|
591
615
|
const candidate = base + ext;
|
|
592
616
|
if (fileExistsSync(candidate))
|
|
593
617
|
return candidate;
|
|
594
618
|
}
|
|
595
|
-
//
|
|
619
|
+
// 4. Treat as directory with index.{tsx,jsx,ts,js,vue}.
|
|
596
620
|
for (const ext of IMPORT_EXTENSIONS) {
|
|
597
621
|
const candidate = path.join(base, "index" + ext);
|
|
598
622
|
if (fileExistsSync(candidate))
|
|
@@ -600,6 +624,51 @@ function resolveImportPath(fromDir, spec) {
|
|
|
600
624
|
}
|
|
601
625
|
return undefined;
|
|
602
626
|
}
|
|
627
|
+
/**
|
|
628
|
+
* The repository file a module specifier written in `fromDir` names, or
|
|
629
|
+
* undefined when nothing in the project answers it.
|
|
630
|
+
*
|
|
631
|
+
* An absolute specifier is refused: it would let a scanned repository point the
|
|
632
|
+
* analyzer at any file on the host (`import x from "/etc/passwd"`). Real
|
|
633
|
+
* projects write relative paths or tsconfig aliases.
|
|
634
|
+
*/
|
|
635
|
+
function resolveModuleSpecifier(spec, fromDir, cache) {
|
|
636
|
+
if (spec.startsWith("/") || path.isAbsolute(spec))
|
|
637
|
+
return undefined;
|
|
638
|
+
if (spec.startsWith("."))
|
|
639
|
+
return resolveImportPath(fromDir, spec);
|
|
640
|
+
const tsconfig = findTsconfig(fromDir, cache);
|
|
641
|
+
if (!tsconfig)
|
|
642
|
+
return undefined;
|
|
643
|
+
const viaPaths = resolveViaTsconfigPaths(spec, tsconfig);
|
|
644
|
+
if (viaPaths)
|
|
645
|
+
return viaPaths;
|
|
646
|
+
return tsconfig.baseUrl ? resolveImportPath(tsconfig.baseUrl, spec) : undefined;
|
|
647
|
+
}
|
|
648
|
+
/** The file a `paths` alias names, trying the most specific pattern first and,
|
|
649
|
+
* within one pattern, the first target that exists. */
|
|
650
|
+
function resolveViaTsconfigPaths(spec, tsconfig) {
|
|
651
|
+
for (const alias of tsconfig.paths) {
|
|
652
|
+
let substitution;
|
|
653
|
+
if (alias.wildcard) {
|
|
654
|
+
if (spec.length < alias.prefix.length + alias.suffix.length)
|
|
655
|
+
continue;
|
|
656
|
+
if (!spec.startsWith(alias.prefix) || !spec.endsWith(alias.suffix))
|
|
657
|
+
continue;
|
|
658
|
+
substitution = spec.slice(alias.prefix.length, spec.length - alias.suffix.length);
|
|
659
|
+
}
|
|
660
|
+
else if (spec !== alias.prefix) {
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
for (const target of alias.targets) {
|
|
664
|
+
const candidate = substitution === undefined ? target : target.replace("*", substitution);
|
|
665
|
+
const resolved = resolveFilePath(candidate);
|
|
666
|
+
if (resolved)
|
|
667
|
+
return resolved;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return undefined;
|
|
671
|
+
}
|
|
603
672
|
function fileExistsSync(p) {
|
|
604
673
|
try {
|
|
605
674
|
const stat = fs.statSync(p);
|
|
@@ -610,40 +679,37 @@ function fileExistsSync(p) {
|
|
|
610
679
|
}
|
|
611
680
|
}
|
|
612
681
|
/**
|
|
613
|
-
* Walk parent directories until a `tsconfig.json` is found, then
|
|
614
|
-
* `baseUrl` (following
|
|
682
|
+
* Walk parent directories until a `tsconfig.json` is found, then read its
|
|
683
|
+
* `baseUrl` and `paths` (following `extends` for the common case where
|
|
615
684
|
* tsconfig.json extends a tsconfig.path.json sibling). Returns null when no
|
|
616
|
-
* tsconfig is found or
|
|
685
|
+
* tsconfig is found, or when the one found configures neither.
|
|
617
686
|
*
|
|
618
687
|
* Memoized in the parse cache: cache hits AND misses, both keyed by the
|
|
619
688
|
* directory we started searching from. Prevents re-stat'ing the same parent
|
|
620
689
|
* chain on every import in a deeply nested tree.
|
|
621
690
|
*/
|
|
622
|
-
function
|
|
623
|
-
const cached = cache.
|
|
691
|
+
function findTsconfig(fromDir, cache) {
|
|
692
|
+
const cached = cache.tsconfig.get(fromDir);
|
|
624
693
|
if (cached !== undefined)
|
|
625
694
|
return cached;
|
|
626
695
|
let cur = fromDir;
|
|
627
696
|
for (let i = 0; i < 12; i++) {
|
|
628
697
|
const tsconfigPath = path.join(cur, "tsconfig.json");
|
|
629
698
|
if (fileExistsSync(tsconfigPath)) {
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
699
|
+
// Cap recursion through `extends` to avoid pathological loops.
|
|
700
|
+
const resolution = readTsconfig(tsconfigPath, 0);
|
|
701
|
+
cache.tsconfig.set(fromDir, resolution);
|
|
702
|
+
return resolution;
|
|
633
703
|
}
|
|
634
704
|
const parent = path.dirname(cur);
|
|
635
705
|
if (parent === cur)
|
|
636
706
|
break;
|
|
637
707
|
cur = parent;
|
|
638
708
|
}
|
|
639
|
-
cache.
|
|
709
|
+
cache.tsconfig.set(fromDir, null);
|
|
640
710
|
return null;
|
|
641
711
|
}
|
|
642
|
-
function
|
|
643
|
-
// Cap recursion through `extends` to avoid pathological loops.
|
|
644
|
-
return readTsconfigBaseUrlInner(tsconfigPath, 0);
|
|
645
|
-
}
|
|
646
|
-
function readTsconfigBaseUrlInner(tsconfigPath, depth) {
|
|
712
|
+
function readTsconfig(tsconfigPath, depth) {
|
|
647
713
|
if (depth > 4)
|
|
648
714
|
return null;
|
|
649
715
|
let parsed;
|
|
@@ -655,26 +721,86 @@ function readTsconfigBaseUrlInner(tsconfigPath, depth) {
|
|
|
655
721
|
catch {
|
|
656
722
|
return null;
|
|
657
723
|
}
|
|
658
|
-
const
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
if (
|
|
667
|
-
extendedPath = path.resolve(tsconfigDir, extendsPath);
|
|
668
|
-
}
|
|
669
|
-
else {
|
|
670
|
-
// Bare-extend (e.g. "@tsconfig/node20/tsconfig.json") — skip.
|
|
724
|
+
const tsconfigDir = path.dirname(tsconfigPath);
|
|
725
|
+
const rawBaseUrl = parsed?.compilerOptions?.baseUrl;
|
|
726
|
+
let baseUrl = null;
|
|
727
|
+
if (typeof rawBaseUrl === "string") {
|
|
728
|
+
// An absolute baseUrl makes every non-relative import in the repo resolve
|
|
729
|
+
// against a directory of the tsconfig's choosing, anywhere on the host. The
|
|
730
|
+
// same reason absolute import specifiers are skipped applies here, and a
|
|
731
|
+
// real project writes baseUrl relative to its own tsconfig.
|
|
732
|
+
if (path.isAbsolute(rawBaseUrl))
|
|
671
733
|
return null;
|
|
734
|
+
baseUrl = path.resolve(tsconfigDir, rawBaseUrl);
|
|
735
|
+
}
|
|
736
|
+
// TypeScript resolves `paths` targets against baseUrl when the project sets
|
|
737
|
+
// one, and against the tsconfig's own directory otherwise.
|
|
738
|
+
const paths = readPathAliases(parsed?.compilerOptions?.paths, baseUrl ?? tsconfigDir);
|
|
739
|
+
if (baseUrl !== null && paths.length > 0)
|
|
740
|
+
return { baseUrl, paths };
|
|
741
|
+
const inherited = readExtendedTsconfig(parsed?.extends, tsconfigDir, depth);
|
|
742
|
+
const merged = {
|
|
743
|
+
baseUrl: baseUrl ?? inherited?.baseUrl ?? null,
|
|
744
|
+
paths: paths.length > 0 ? paths : (inherited?.paths ?? []),
|
|
745
|
+
};
|
|
746
|
+
if (merged.baseUrl === null && merged.paths.length === 0)
|
|
747
|
+
return null;
|
|
748
|
+
return merged;
|
|
749
|
+
}
|
|
750
|
+
/** Follow `extends`, refusing anything that is not a config file named relative
|
|
751
|
+
* to this one. */
|
|
752
|
+
function readExtendedTsconfig(extendsPath, tsconfigDir, depth) {
|
|
753
|
+
if (typeof extendsPath !== "string")
|
|
754
|
+
return null;
|
|
755
|
+
if (path.isAbsolute(extendsPath)) {
|
|
756
|
+
// `extends: "/etc/passwd"` would have this function read that file during
|
|
757
|
+
// a scan. Only a sibling config, named relative to this one, is followed.
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
760
|
+
// Bare-extend (e.g. "@tsconfig/node20/tsconfig.json") — skip.
|
|
761
|
+
if (!extendsPath.startsWith("."))
|
|
762
|
+
return null;
|
|
763
|
+
let extended = path.resolve(tsconfigDir, extendsPath);
|
|
764
|
+
if (!extended.endsWith(".json"))
|
|
765
|
+
extended += ".json";
|
|
766
|
+
return readTsconfig(extended, depth + 1);
|
|
767
|
+
}
|
|
768
|
+
/** Parse `compilerOptions.paths` into match-ready aliases. */
|
|
769
|
+
function readPathAliases(raw, resolveAgainst) {
|
|
770
|
+
if (!raw || typeof raw !== "object")
|
|
771
|
+
return [];
|
|
772
|
+
const aliases = [];
|
|
773
|
+
for (const [pattern, value] of Object.entries(raw)) {
|
|
774
|
+
if (!Array.isArray(value))
|
|
775
|
+
continue;
|
|
776
|
+
const star = pattern.indexOf("*");
|
|
777
|
+
// TypeScript accepts at most one `*` per pattern and per target.
|
|
778
|
+
if (star !== pattern.lastIndexOf("*"))
|
|
779
|
+
continue;
|
|
780
|
+
const targets = [];
|
|
781
|
+
for (const target of value) {
|
|
782
|
+
if (typeof target !== "string")
|
|
783
|
+
continue;
|
|
784
|
+
// An absolute target reaches outside the project, the same way an
|
|
785
|
+
// absolute baseUrl would.
|
|
786
|
+
if (path.isAbsolute(target))
|
|
787
|
+
continue;
|
|
788
|
+
if (target.indexOf("*") !== target.lastIndexOf("*"))
|
|
789
|
+
continue;
|
|
790
|
+
targets.push(path.resolve(resolveAgainst, target));
|
|
672
791
|
}
|
|
673
|
-
if (
|
|
674
|
-
|
|
675
|
-
|
|
792
|
+
if (targets.length === 0)
|
|
793
|
+
continue;
|
|
794
|
+
aliases.push({
|
|
795
|
+
prefix: star >= 0 ? pattern.slice(0, star) : pattern,
|
|
796
|
+
suffix: star >= 0 ? pattern.slice(star + 1) : "",
|
|
797
|
+
wildcard: star >= 0,
|
|
798
|
+
targets,
|
|
799
|
+
});
|
|
676
800
|
}
|
|
677
|
-
|
|
801
|
+
// Longest prefix first, so `@/components/*` is tried before `@/*`.
|
|
802
|
+
aliases.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
803
|
+
return aliases;
|
|
678
804
|
}
|
|
679
805
|
/**
|
|
680
806
|
* Strip line/block comments from JSON before parsing. tsconfig.json files
|
|
@@ -713,12 +839,55 @@ function vueRouterRoutesFromFile(sf, filePath, imports, localConsts, cache) {
|
|
|
713
839
|
// heuristic anyway, so this guard only bites the rarer `component:` form.
|
|
714
840
|
if (isInsideCreateRouter(node))
|
|
715
841
|
return;
|
|
716
|
-
const
|
|
842
|
+
const prefix = enclosingObjectRoutePath(node, filePath, imports, localConsts, cache);
|
|
843
|
+
if (prefix === null)
|
|
844
|
+
return;
|
|
845
|
+
const r = routeFromObjectLiteral(node, prefix, filePath, imports, localConsts, cache);
|
|
717
846
|
if (r)
|
|
718
847
|
out.push(r);
|
|
719
848
|
});
|
|
720
849
|
return out;
|
|
721
850
|
}
|
|
851
|
+
/**
|
|
852
|
+
* The path prefix a route object literal inherits from the route objects that
|
|
853
|
+
* hold it in a `children:` array, composed root-to-leaf. `""` for a top-level
|
|
854
|
+
* route. `null` when an ancestor states a path this scan cannot resolve: the
|
|
855
|
+
* descendant's URL would then be wrong, and a wrong URL sends the browser to
|
|
856
|
+
* the wrong page with the server's authority behind it.
|
|
857
|
+
*/
|
|
858
|
+
function enclosingObjectRoutePath(node, filePath, imports, localConsts, cache) {
|
|
859
|
+
const segments = [];
|
|
860
|
+
for (let cur = node.parent; cur; cur = cur.parent) {
|
|
861
|
+
if (!ts.isPropertyAssignment(cur))
|
|
862
|
+
continue;
|
|
863
|
+
if (!ts.isIdentifier(cur.name) || cur.name.text !== "children")
|
|
864
|
+
continue;
|
|
865
|
+
if (!ts.isObjectLiteralExpression(cur.parent))
|
|
866
|
+
continue;
|
|
867
|
+
const value = objectLiteralPath(cur.parent, filePath, imports, localConsts, cache);
|
|
868
|
+
if (value === null)
|
|
869
|
+
return null;
|
|
870
|
+
// Nearest ancestor is seen first, so prepend for root→leaf order.
|
|
871
|
+
if (value !== undefined)
|
|
872
|
+
segments.unshift(value);
|
|
873
|
+
}
|
|
874
|
+
let prefix = "";
|
|
875
|
+
for (const segment of segments)
|
|
876
|
+
prefix = composeRoutePath(prefix, segment);
|
|
877
|
+
return prefix;
|
|
878
|
+
}
|
|
879
|
+
/** A route object's `path`: the string it states, `undefined` when it states
|
|
880
|
+
* none (a pathless layout), `null` when it states one that cannot be resolved. */
|
|
881
|
+
function objectLiteralPath(obj, filePath, imports, localConsts, cache) {
|
|
882
|
+
for (const prop of obj.properties) {
|
|
883
|
+
if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name))
|
|
884
|
+
continue;
|
|
885
|
+
if (prop.name.text !== "path")
|
|
886
|
+
continue;
|
|
887
|
+
return resolveExpressionToString(prop.initializer, filePath, imports, localConsts, cache, 0);
|
|
888
|
+
}
|
|
889
|
+
return undefined;
|
|
890
|
+
}
|
|
722
891
|
function isInsideDefineModule(node) {
|
|
723
892
|
let cur = node.parent;
|
|
724
893
|
while (cur) {
|
|
@@ -826,7 +995,38 @@ function createRouterRoutesFromCall(call, filePath, imports, localConsts, cache)
|
|
|
826
995
|
const arg = call.arguments[0];
|
|
827
996
|
if (!arg || !ts.isArrayLiteralExpression(arg))
|
|
828
997
|
return null;
|
|
829
|
-
|
|
998
|
+
const routes = dataRouterRoutesFromArray(arg, "", filePath, imports, localConsts, cache);
|
|
999
|
+
// A route path inside the array is written absolute, so the basename cannot be
|
|
1000
|
+
// threaded through as a prefix — composeRoutePath drops a prefix for an
|
|
1001
|
+
// absolute child. Apply it to the finished paths instead.
|
|
1002
|
+
const basename = basenameOfRouterCall(call);
|
|
1003
|
+
if (!basename)
|
|
1004
|
+
return routes;
|
|
1005
|
+
return routes.map((route) => ({
|
|
1006
|
+
...route,
|
|
1007
|
+
path: composeRoutePath(basename, route.path.replace(/^\//, "")),
|
|
1008
|
+
}));
|
|
1009
|
+
}
|
|
1010
|
+
/** The `basename` a data-router factory mounts its tree under, from its options
|
|
1011
|
+
* argument: `createBrowserRouter(routes, { basename: "/app" })`. Every URL the
|
|
1012
|
+
* app serves carries it. */
|
|
1013
|
+
function basenameOfRouterCall(call) {
|
|
1014
|
+
const options = call.arguments[1];
|
|
1015
|
+
if (!options || !ts.isObjectLiteralExpression(options))
|
|
1016
|
+
return undefined;
|
|
1017
|
+
for (const prop of options.properties) {
|
|
1018
|
+
if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name))
|
|
1019
|
+
continue;
|
|
1020
|
+
if (prop.name.text !== "basename")
|
|
1021
|
+
continue;
|
|
1022
|
+
if (!ts.isStringLiteral(prop.initializer))
|
|
1023
|
+
return undefined;
|
|
1024
|
+
const value = prop.initializer.text.replace(/\/+$/, "");
|
|
1025
|
+
if (value === "")
|
|
1026
|
+
return undefined;
|
|
1027
|
+
return value.startsWith("/") ? value : "/" + value;
|
|
1028
|
+
}
|
|
1029
|
+
return undefined;
|
|
830
1030
|
}
|
|
831
1031
|
function dataRouterRoutesFromArray(arr, prefix, filePath, imports, localConsts, cache) {
|
|
832
1032
|
const out = [];
|
|
@@ -861,30 +1061,38 @@ function dataRouterRoutesFromArray(arr, prefix, filePath, imports, localConsts,
|
|
|
861
1061
|
if (hasPath && pathValue === null)
|
|
862
1062
|
continue;
|
|
863
1063
|
let childPrefix = prefix;
|
|
1064
|
+
let ownPath;
|
|
864
1065
|
if (hasPath) {
|
|
865
1066
|
childPrefix = composeRoutePath(prefix, pathValue);
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
componentFile: imports.get(componentName)?.filePath,
|
|
871
|
-
declaredIn: filePath,
|
|
872
|
-
});
|
|
873
|
-
}
|
|
1067
|
+
ownPath = childPrefix;
|
|
1068
|
+
}
|
|
1069
|
+
else if (isIndex) {
|
|
1070
|
+
ownPath = composeRoutePath(prefix, "");
|
|
874
1071
|
}
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
1072
|
+
const childRoutes = childrenArray
|
|
1073
|
+
? dataRouterRoutesFromArray(childrenArray, childPrefix, filePath, imports, localConsts, cache)
|
|
1074
|
+
: [];
|
|
1075
|
+
if (componentName) {
|
|
1076
|
+
const component = componentName;
|
|
1077
|
+
const mountedAt = (routePath) => ({
|
|
1078
|
+
path: routePath,
|
|
1079
|
+
componentName: component,
|
|
1080
|
+
componentFile: imports.get(component)?.filePath,
|
|
880
1081
|
declaredIn: filePath,
|
|
881
1082
|
});
|
|
1083
|
+
if (ownPath !== undefined) {
|
|
1084
|
+
out.push(mountedAt(ownPath));
|
|
1085
|
+
}
|
|
1086
|
+
else {
|
|
1087
|
+
// Pathless layout route: it contributes no URL segment and has none of
|
|
1088
|
+
// its own, so it renders on every route nested under it — reported the
|
|
1089
|
+
// same way a pathless TanStack layout is.
|
|
1090
|
+
for (const routePath of new Set(childRoutes.map((route) => route.path))) {
|
|
1091
|
+
out.push(mountedAt(routePath));
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
882
1094
|
}
|
|
883
|
-
|
|
884
|
-
// its children inherit the current prefix unchanged.
|
|
885
|
-
if (childrenArray) {
|
|
886
|
-
out.push(...dataRouterRoutesFromArray(childrenArray, childPrefix, filePath, imports, localConsts, cache));
|
|
887
|
-
}
|
|
1095
|
+
out.push(...childRoutes);
|
|
888
1096
|
}
|
|
889
1097
|
return out;
|
|
890
1098
|
}
|
|
@@ -2,10 +2,19 @@
|
|
|
2
2
|
* The single definition of "discovery treats this path as a test file", shared by
|
|
3
3
|
* discovery itself and by callers that must agree with what discovery classified as
|
|
4
4
|
* external coverage. A narrower predicate silently disagrees: `tests/test_orders.py`
|
|
5
|
-
* is external coverage here but is not a test file to `isTestFile`
|
|
6
|
-
*
|
|
5
|
+
* is external coverage here but is not a test file to `isTestFile` below, which
|
|
6
|
+
* only knows the Skyramp and JS/TS spec spellings.
|
|
7
7
|
*
|
|
8
8
|
* Accepts absolute or repo-relative paths. The directory patterns need a leading
|
|
9
9
|
* separator, which a repo-relative path has not got, so one is prepended.
|
|
10
10
|
*/
|
|
11
11
|
export declare function isDiscoveredTestFile(filePath: string): boolean;
|
|
12
|
+
/** The NARROW predicate: a Skyramp-generated test or scenario file, a
|
|
13
|
+
* Vitest/Jest/Playwright spec, or anything under `__tests__/`. Deliberately not
|
|
14
|
+
* `isDiscoveredTestFile`'s rule, which also counts `tests/`, a page-object
|
|
15
|
+
* directory and `test_orders.py`. The maintenance tool wants this narrow one. */
|
|
16
|
+
export declare function isTestFile(filePath: string): boolean;
|
|
17
|
+
/** Whether a path is a test file, by directory or by name. Read by the route
|
|
18
|
+
* checks: a route declared in a mock server or a spec fixture is not a surface
|
|
19
|
+
* anyone serves. */
|
|
20
|
+
export declare function isTestPath(file: string): boolean;
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import * as path from "path";
|
|
2
|
+
// Four questions about a path, four predicates, one file. Each is answered by its
|
|
3
|
+
// own rule on purpose, and merging them has broken a caller before:
|
|
4
|
+
// isDiscoveredTestFile — does test DISCOVERY own this file?
|
|
5
|
+
// isTestFile — is it a Skyramp test or a JS/TS spec? (maintenance)
|
|
6
|
+
// isTestPath — can this file DECLARE a route? (route citations)
|
|
7
|
+
// Two more patterns still live elsewhere and are out of scope here:
|
|
8
|
+
// `pr-comment-parser.ts:45` and `test-recommendation-prompt.ts:45`.
|
|
2
9
|
// Test file naming patterns — match against basename.
|
|
3
10
|
// Includes Skyramp-generated suffixes so that files like orders_smoke.py placed
|
|
4
11
|
// outside a recognized test directory are still discovered.
|
|
@@ -34,8 +41,8 @@ const TEST_DIR_PATTERNS = [
|
|
|
34
41
|
* The single definition of "discovery treats this path as a test file", shared by
|
|
35
42
|
* discovery itself and by callers that must agree with what discovery classified as
|
|
36
43
|
* external coverage. A narrower predicate silently disagrees: `tests/test_orders.py`
|
|
37
|
-
* is external coverage here but is not a test file to `isTestFile`
|
|
38
|
-
*
|
|
44
|
+
* is external coverage here but is not a test file to `isTestFile` below, which
|
|
45
|
+
* only knows the Skyramp and JS/TS spec spellings.
|
|
39
46
|
*
|
|
40
47
|
* Accepts absolute or repo-relative paths. The directory patterns need a leading
|
|
41
48
|
* separator, which a repo-relative path has not got, so one is prepended.
|
|
@@ -45,3 +52,38 @@ export function isDiscoveredTestFile(filePath) {
|
|
|
45
52
|
return true;
|
|
46
53
|
return TEST_DIR_PATTERNS.some((p) => p.test(`/${filePath}`));
|
|
47
54
|
}
|
|
55
|
+
/** The NARROW predicate: a Skyramp-generated test or scenario file, a
|
|
56
|
+
* Vitest/Jest/Playwright spec, or anything under `__tests__/`. Deliberately not
|
|
57
|
+
* `isDiscoveredTestFile`'s rule, which also counts `tests/`, a page-object
|
|
58
|
+
* directory and `test_orders.py`. The maintenance tool wants this narrow one. */
|
|
59
|
+
export function isTestFile(filePath) {
|
|
60
|
+
return (/(?:_test|_smoke|_contract|_fuzz|_integration|_load|_e2e|_ui)\.[^/]+$/.test(filePath) ||
|
|
61
|
+
/scenario_[^/]+\.json$/.test(filePath) ||
|
|
62
|
+
// Include the `.e2e.` dot form so Playwright/Cypress specs discovery treats
|
|
63
|
+
// as UI tests are recognized here too — e.g. `login.e2e.ts`.
|
|
64
|
+
/\.(spec|test|e2e)\.(tsx?|jsx?)$/.test(filePath) ||
|
|
65
|
+
/(?:^|\/)__tests__\//.test(filePath));
|
|
66
|
+
}
|
|
67
|
+
// ── The route-citation rule ──────────────────────────────────────────────────
|
|
68
|
+
// Its three regexes stay separate from the discovery patterns above. Each one
|
|
69
|
+
// differs deliberately, and the differences are the point — see each comment.
|
|
70
|
+
/** A directory that is a test root whatever the file inside is called. `spec/`
|
|
71
|
+
* and `specs/` are deliberately absent, unlike `TEST_DIR_PATTERNS`: a spec-first
|
|
72
|
+
* service keeps its OpenAPI document there, and that document IS its route
|
|
73
|
+
* definition. */
|
|
74
|
+
const TEST_ROOT_DIR = /(^|[\\/])(tests?|__tests__|__mocks__|e2e|cypress|testing)([\\/]|$)/i;
|
|
75
|
+
/** A file whose own NAME says it is a test, wherever it sits. Wider than the
|
|
76
|
+
* `TEST_FILE_PATTERNS` rows it resembles — any extension, and `.`, `_` or `-`
|
|
77
|
+
* before `test` — so no row above can stand in for it. */
|
|
78
|
+
const TEST_FILE_NAME = /[._-](test|spec)\.[a-z]+$|(^|[\\/])test_[^\\/]*$/i;
|
|
79
|
+
/** The CamelCase spellings, which carry no separator before `Test`.
|
|
80
|
+
* Case-SENSITIVE and must stay so: with `i` it also matches `Latest.java` and
|
|
81
|
+
* `Manifest.cs`, ordinary source files. That is why the case-insensitive
|
|
82
|
+
* `*Test.java` / `*Tests.cs` rows above cannot serve here. */
|
|
83
|
+
const CAMEL_TEST_FILE_NAME = /[a-z0-9](Tests?|IT)\.[A-Za-z0-9]+$/;
|
|
84
|
+
/** Whether a path is a test file, by directory or by name. Read by the route
|
|
85
|
+
* checks: a route declared in a mock server or a spec fixture is not a surface
|
|
86
|
+
* anyone serves. */
|
|
87
|
+
export function isTestPath(file) {
|
|
88
|
+
return TEST_ROOT_DIR.test(file) || TEST_FILE_NAME.test(file) || CAMEL_TEST_FILE_NAME.test(file);
|
|
89
|
+
}
|