@skyramp/mcp 0.3.0-rc.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/prompts/test-maintenance/actionsInstructions.js +4 -2
- package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +9 -0
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +8 -7
- package/build/prompts/test-maintenance/driftAnalysisSections.js +4 -5
- package/build/prompts/test-maintenance/driftAnalysisShared.d.ts +11 -0
- package/build/prompts/test-maintenance/driftAnalysisShared.js +22 -6
- package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +23 -31
- package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +7 -1
- package/build/prompts/test-recommendation/diffExecutionPlan.js +20 -6
- package/build/prompts/test-recommendation/scopeAssessment.d.ts +10 -1
- package/build/prompts/test-recommendation/scopeAssessment.js +27 -2
- package/build/prompts/test-recommendation/scopeAssessment.test.js +35 -0
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +5 -1
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +61 -0
- package/build/prompts/testbot/testbot-prompts.js +7 -5
- package/build/prompts/testbot/testbot-prompts.test.js +1 -1
- package/build/services/AnalyticsService.js +37 -2
- package/build/services/TestDiscoveryService.d.ts +38 -4
- package/build/services/TestDiscoveryService.js +187 -16
- package/build/services/TestDiscoveryService.test.js +232 -10
- package/build/services/TestExecutionService.d.ts +12 -1
- package/build/services/TestExecutionService.js +20 -41
- package/build/services/TestGenerationService.d.ts +8 -0
- package/build/services/TestGenerationService.js +62 -17
- package/build/services/TestGenerationService.test.js +67 -1
- package/build/tools/auth/loginTool.js +1 -1
- package/build/tools/auth/logoutTool.js +1 -1
- package/build/tools/code-refactor/codeReuseTool.js +1 -1
- package/build/tools/code-refactor/modularizationTool.js +1 -1
- package/build/tools/executeSkyrampTestTool.js +30 -1
- package/build/tools/fixErrorTool.js +1 -1
- package/build/tools/one-click/oneClickTool.js +1 -1
- package/build/tools/test-management/actionsTool.js +76 -41
- package/build/tools/test-management/actionsTool.test.js +104 -38
- package/build/tools/test-management/analyzeChangesTool.js +102 -4
- package/build/tools/test-management/analyzeChangesTool.test.js +3 -1
- package/build/tools/test-management/analyzeTestHealthTool.js +9 -1
- package/build/tools/test-management/analyzeTestHealthTool.test.js +1 -1
- package/build/tools/trace/startTraceCollectionTool.js +1 -1
- package/build/tools/trace/stopTraceCollectionTool.js +1 -1
- package/build/tools/workspace/initScanWorkspaceTool.js +1 -1
- package/build/tools/workspace/initializeWorkspaceTool.js +1 -1
- package/build/tools/workspace/initializeWorkspaceTool.test.js +1 -1
- package/build/types/RepositoryAnalysis.d.ts +6 -6
- package/build/types/TestAnalysis.d.ts +13 -0
- package/build/utils/dartRouteExtractor.js +8 -30
- package/build/utils/docker.test.js +1 -1
- package/build/utils/fileWalk.d.ts +71 -0
- package/build/utils/fileWalk.js +79 -0
- package/build/utils/fileWalk.test.d.ts +1 -0
- package/build/utils/fileWalk.test.js +252 -0
- package/build/utils/frontendIntegration.js +20 -36
- package/build/utils/frontendSelectors.d.ts +34 -0
- package/build/utils/frontendSelectors.js +124 -0
- package/build/utils/frontendSelectors.test.d.ts +1 -0
- package/build/utils/frontendSelectors.test.js +118 -0
- package/build/utils/importerHop.d.ts +18 -0
- package/build/utils/importerHop.js +48 -3
- package/build/utils/importerHop.test.js +71 -1
- package/build/utils/pythonMountPrefixes.d.ts +25 -0
- package/build/utils/pythonMountPrefixes.js +347 -0
- package/build/utils/pythonMountPrefixes.test.d.ts +1 -0
- package/build/utils/pythonMountPrefixes.test.js +113 -0
- package/build/utils/repoScanner.js +30 -23
- package/build/utils/repoScanner.test.js +98 -0
- package/build/utils/routeParsers.d.ts +7 -1
- package/build/utils/routeParsers.js +32 -18
- package/build/utils/sourceRouteExtractor.js +17 -40
- package/build/utils/trace-parser.js +7 -19
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/build/utils/walkerCharacterization.test.d.ts +1 -0
- package/build/utils/walkerCharacterization.test.js +233 -0
- package/node_modules/playwright/lib/common/config.js +1 -1
- package/node_modules/playwright/lib/common/configLoader.js +1 -1
- package/node_modules/playwright/lib/common/fixtures.js +1 -1
- package/node_modules/playwright/lib/common/testType.js +1 -1
- package/node_modules/playwright/lib/index.js +2 -2
- package/node_modules/playwright/lib/isomorphic/testTree.js +1 -1
- package/node_modules/playwright/lib/matchers/expect.js +1 -1
- package/node_modules/playwright/lib/mcp/browser/tab.js +1 -1
- package/node_modules/playwright/lib/mcp/browser/tools/navigate.js +1 -1
- package/node_modules/playwright/lib/mcp/browser/tools/tracing.js +1 -1
- package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +7 -0
- package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +2 -1
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +3 -1
- package/node_modules/playwright/lib/reporters/base.js +2 -2
- package/node_modules/playwright/lib/reporters/list.js +1 -1
- package/node_modules/playwright/lib/reporters/teleEmitter.js +1 -1
- package/node_modules/playwright/lib/worker/testInfo.js +2 -2
- package/node_modules/playwright/node_modules/playwright-core/ThirdPartyNotices.txt +23 -126
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +2 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-Cqp3cwEJ.js → codeMirrorModule-aszq5EdG.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-CYf9adZh.js → defaultSettingsView-BxS7Jm4s.js} +94 -94
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.d4gkVSou.js → index.D4JTTy4R.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.u4_8VnCV.js → uiMode.DaRMQKOI.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/generated/injectedScriptSource.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/generated/pollingRecorderSource.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +1 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +3 -3
- package/node_modules/playwright/._ThirdPartyNotices.txt +0 -0
- package/node_modules/playwright/._cli.js +0 -0
- package/node_modules/playwright/._index.d.ts +0 -0
- package/node_modules/playwright/._index.js +0 -0
- package/node_modules/playwright/._index.mjs +0 -0
- package/node_modules/playwright/._jsx-runtime.js +0 -0
- package/node_modules/playwright/._jsx-runtime.mjs +0 -0
- package/node_modules/playwright/._lib +0 -0
- package/node_modules/playwright/._node_modules +0 -0
- package/node_modules/playwright/._package.json +0 -0
- package/node_modules/playwright/._test.d.ts +0 -0
- package/node_modules/playwright/._test.js +0 -0
- package/node_modules/playwright/._test.mjs +0 -0
- package/node_modules/playwright/._types +0 -0
- package/node_modules/playwright/._vitest.config.ts +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._.DS_Store +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._.npmignore +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._README.md +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._ThirdPartyNotices.txt +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._bin +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._browsers.json +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._bundles +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._cli.js +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._index.d.ts +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._index.js +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._index.mjs +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._lib +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._package.json +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._src +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._types +0 -0
|
@@ -1,29 +1,12 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { nextjsFileToApiPath, parseFileEndpoints, } from "./routeParsers.js";
|
|
4
|
+
import { resolvePythonMountPrefixes } from "./pythonMountPrefixes.js";
|
|
5
|
+
import { walkSourceFiles } from "./fileWalk.js";
|
|
4
6
|
import { logger } from "./logger.js";
|
|
7
|
+
const GLOB_SKIP_DIRS = new Set(["node_modules", ".git", ".next", "dist", "build"]);
|
|
5
8
|
function globRecursive(dir, extensions) {
|
|
6
|
-
|
|
7
|
-
let entries;
|
|
8
|
-
try {
|
|
9
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
10
|
-
}
|
|
11
|
-
catch (err) {
|
|
12
|
-
logger.debug("globRecursive: skipping directory", { dir, error: err instanceof Error ? err.message : String(err) });
|
|
13
|
-
return results;
|
|
14
|
-
}
|
|
15
|
-
for (const entry of entries) {
|
|
16
|
-
const fullPath = path.join(dir, entry.name);
|
|
17
|
-
if (entry.isDirectory()) {
|
|
18
|
-
if (entry.name === "node_modules" || entry.name === ".git" || entry.name === ".next" || entry.name === "dist" || entry.name === "build")
|
|
19
|
-
continue;
|
|
20
|
-
results.push(...globRecursive(fullPath, extensions));
|
|
21
|
-
}
|
|
22
|
-
else if (extensions.some((ext) => entry.name.endsWith(ext))) {
|
|
23
|
-
results.push(fullPath);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return results;
|
|
9
|
+
return walkSourceFiles(dir, { extensions, skipDirs: GLOB_SKIP_DIRS });
|
|
27
10
|
}
|
|
28
11
|
function safeReadFile(filePath) {
|
|
29
12
|
try {
|
|
@@ -231,8 +214,28 @@ export function findCandidateRouteFiles(repositoryPath) {
|
|
|
231
214
|
}
|
|
232
215
|
return result;
|
|
233
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* Cross-file FastAPI mount-prefix lookup, resolved lazily: `resolvePythonMountPrefixes`
|
|
219
|
+
* walks the whole repo's Python, so it's only invoked the first time a `.py` file is
|
|
220
|
+
* actually scanned — non-Python repos pay nothing. Returns undefined for non-`.py`
|
|
221
|
+
* files (they never carry Python mount prefixes).
|
|
222
|
+
*/
|
|
223
|
+
function makeMountPrefixLookup(repositoryPath) {
|
|
224
|
+
let resolved = null;
|
|
225
|
+
return (relative) => {
|
|
226
|
+
if (!relative.endsWith(".py"))
|
|
227
|
+
return undefined;
|
|
228
|
+
if (resolved === null)
|
|
229
|
+
resolved = resolvePythonMountPrefixes(repositoryPath);
|
|
230
|
+
return resolved.get(relative.replace(/\\/g, "/"));
|
|
231
|
+
};
|
|
232
|
+
}
|
|
234
233
|
export function scanAllRepoEndpoints(repositoryPath) {
|
|
234
|
+
// Normalize up-front so relative-path derivation (and mount-prefix lookups) don't
|
|
235
|
+
// depend on a caller-supplied trailing separator.
|
|
236
|
+
repositoryPath = path.resolve(repositoryPath);
|
|
235
237
|
const endpointMap = new Map();
|
|
238
|
+
const mountPrefixFor = makeMountPrefixLookup(repositoryPath);
|
|
236
239
|
const sourceFiles = globRecursive(repositoryPath, [".ts", ".tsx", ".js", ".jsx", ".py", ".java", ".kt", ".go", ".rb", ".php", ".rs", ".cs"]);
|
|
237
240
|
for (const file of sourceFiles) {
|
|
238
241
|
if (scanNextjsFile(file, repositoryPath, endpointMap))
|
|
@@ -244,7 +247,7 @@ export function scanAllRepoEndpoints(repositoryPath) {
|
|
|
244
247
|
const content = safeReadFile(file);
|
|
245
248
|
if (content === null)
|
|
246
249
|
continue;
|
|
247
|
-
for (const ep of parseFileEndpoints(content, relative)) {
|
|
250
|
+
for (const ep of parseFileEndpoints(content, relative, mountPrefixFor(relative))) {
|
|
248
251
|
addEndpointToMap(endpointMap, ep.path, ep.method, file, repositoryPath, ep.line);
|
|
249
252
|
}
|
|
250
253
|
}
|
|
@@ -256,6 +259,7 @@ export function scanAllRepoEndpoints(repositoryPath) {
|
|
|
256
259
|
}));
|
|
257
260
|
}
|
|
258
261
|
export function scanRelatedEndpoints(repositoryPath, changedFiles) {
|
|
262
|
+
repositoryPath = path.resolve(repositoryPath);
|
|
259
263
|
const relatedDirs = new Set();
|
|
260
264
|
for (const f of changedFiles) {
|
|
261
265
|
const absDir = path.dirname(path.join(repositoryPath, f));
|
|
@@ -265,6 +269,9 @@ export function scanRelatedEndpoints(repositoryPath, changedFiles) {
|
|
|
265
269
|
relatedDirs.add(parentDir);
|
|
266
270
|
}
|
|
267
271
|
const endpointMap = new Map();
|
|
272
|
+
// Mount prefixes are resolved over the whole repo (aggregators may live outside
|
|
273
|
+
// the related dirs) but lazily — only once a Python file is actually scanned.
|
|
274
|
+
const mountPrefixFor = makeMountPrefixLookup(repositoryPath);
|
|
268
275
|
for (const dir of relatedDirs) {
|
|
269
276
|
if (!fs.existsSync(dir))
|
|
270
277
|
continue;
|
|
@@ -279,7 +286,7 @@ export function scanRelatedEndpoints(repositoryPath, changedFiles) {
|
|
|
279
286
|
const fileContent = safeReadFile(file);
|
|
280
287
|
if (fileContent === null)
|
|
281
288
|
continue;
|
|
282
|
-
for (const ep of parseFileEndpoints(fileContent, relative)) {
|
|
289
|
+
for (const ep of parseFileEndpoints(fileContent, relative, mountPrefixFor(relative))) {
|
|
283
290
|
addEndpointToMap(endpointMap, ep.path, ep.method, file, repositoryPath, ep.line);
|
|
284
291
|
}
|
|
285
292
|
}
|
|
@@ -89,4 +89,102 @@ async def bulk_delete_deployments(): pass
|
|
|
89
89
|
{ path: "/bulk_delete", methods: ["POST"], sourceFile: "src/prefect/server/api/flows.py" },
|
|
90
90
|
]));
|
|
91
91
|
});
|
|
92
|
+
// FastAPI composes a route's full URL from prefixes applied at include_router()
|
|
93
|
+
// mount points that live in *other* files — the single-file parser can't see
|
|
94
|
+
// them. Mealie mounts its whole API under a top-level APIRouter(prefix="/api"),
|
|
95
|
+
// so a leaf router's own prefix ("/recipes") must be composed with the ancestor
|
|
96
|
+
// "/api" or generated tests hit the un-prefixed URL and 405.
|
|
97
|
+
it("composes cross-file include_router mount prefixes into FastAPI route paths", () => {
|
|
98
|
+
const repo = makeTempRepo();
|
|
99
|
+
const routesDir = path.join(repo, "mealie/routes");
|
|
100
|
+
const recipeDir = path.join(routesDir, "recipe");
|
|
101
|
+
fs.mkdirSync(recipeDir, { recursive: true });
|
|
102
|
+
// Top-level app: mounts the API router with no extra prefix.
|
|
103
|
+
fs.writeFileSync(path.join(repo, "mealie/app.py"), `
|
|
104
|
+
from fastapi import FastAPI
|
|
105
|
+
from mealie.routes import router
|
|
106
|
+
|
|
107
|
+
app = FastAPI()
|
|
108
|
+
app.include_router(router)
|
|
109
|
+
`);
|
|
110
|
+
// API aggregator: APIRouter(prefix="/api") includes each resource router.
|
|
111
|
+
fs.writeFileSync(path.join(routesDir, "__init__.py"), `
|
|
112
|
+
from fastapi import APIRouter
|
|
113
|
+
from . import recipe
|
|
114
|
+
|
|
115
|
+
router = APIRouter(prefix="/api")
|
|
116
|
+
router.include_router(recipe.router)
|
|
117
|
+
`);
|
|
118
|
+
// Recipe aggregator: no own prefix; mounts the CRUD router directly and the
|
|
119
|
+
// comments router under a "/recipes" mount prefix drawn from a local const.
|
|
120
|
+
fs.writeFileSync(path.join(recipeDir, "__init__.py"), `
|
|
121
|
+
from fastapi import APIRouter
|
|
122
|
+
from . import recipe_crud_routes, comments
|
|
123
|
+
|
|
124
|
+
prefix = "/recipes"
|
|
125
|
+
|
|
126
|
+
router = APIRouter()
|
|
127
|
+
router.include_router(recipe_crud_routes.router, tags=["Recipe: CRUD"])
|
|
128
|
+
router.include_router(comments.router, prefix=prefix, tags=["Recipe: Comments"])
|
|
129
|
+
`);
|
|
130
|
+
// Leaf CRUD router declared via a custom APIRouter *subclass* with its own
|
|
131
|
+
// "/recipes" prefix.
|
|
132
|
+
fs.writeFileSync(path.join(recipeDir, "recipe_crud_routes.py"), `
|
|
133
|
+
from mealie.routes._base.routers import UserAPIRouter
|
|
134
|
+
|
|
135
|
+
router = UserAPIRouter(prefix="/recipes")
|
|
136
|
+
|
|
137
|
+
@router.get("")
|
|
138
|
+
async def get_all(): pass
|
|
139
|
+
|
|
140
|
+
@router.get("/{slug}")
|
|
141
|
+
async def get_one(): pass
|
|
142
|
+
`);
|
|
143
|
+
// Comments router: no own prefix — its "/recipes" comes only from the mount.
|
|
144
|
+
fs.writeFileSync(path.join(recipeDir, "comments.py"), `
|
|
145
|
+
from fastapi import APIRouter
|
|
146
|
+
|
|
147
|
+
router = APIRouter()
|
|
148
|
+
|
|
149
|
+
@router.get("/{slug}/comments")
|
|
150
|
+
async def get_comments(): pass
|
|
151
|
+
`);
|
|
152
|
+
const endpoints = scanAllRepoEndpoints(repo);
|
|
153
|
+
// CRUD: /api (aggregator) + /recipes (leaf subclass own prefix)
|
|
154
|
+
expect(endpoints).toEqual(expect.arrayContaining([
|
|
155
|
+
expect.objectContaining({ path: "/api/recipes", methods: ["GET"], sourceFile: "mealie/routes/recipe/recipe_crud_routes.py" }),
|
|
156
|
+
expect.objectContaining({ path: "/api/recipes/{slug}", methods: ["GET"], sourceFile: "mealie/routes/recipe/recipe_crud_routes.py" }),
|
|
157
|
+
]));
|
|
158
|
+
// Comments: /api (aggregator) + /recipes (mount prefix from a local const) + own path
|
|
159
|
+
expect(endpoints).toEqual(expect.arrayContaining([
|
|
160
|
+
expect.objectContaining({ path: "/api/recipes/{slug}/comments", methods: ["GET"], sourceFile: "mealie/routes/recipe/comments.py" }),
|
|
161
|
+
]));
|
|
162
|
+
// The un-prefixed path must NOT appear (the 405 bug).
|
|
163
|
+
expect(endpoints).not.toContainEqual(expect.objectContaining({ path: "/recipes" }));
|
|
164
|
+
});
|
|
165
|
+
it("composes an app-level include_router(prefix=...) mount", () => {
|
|
166
|
+
const repo = makeTempRepo();
|
|
167
|
+
const apiDir = path.join(repo, "src/api");
|
|
168
|
+
fs.mkdirSync(apiDir, { recursive: true });
|
|
169
|
+
// Prefix applied at the app mount point rather than on the APIRouter itself.
|
|
170
|
+
fs.writeFileSync(path.join(repo, "src/main.py"), `
|
|
171
|
+
from fastapi import FastAPI
|
|
172
|
+
from src.api.users import router as users_router
|
|
173
|
+
|
|
174
|
+
app = FastAPI()
|
|
175
|
+
app.include_router(users_router, prefix="/api/v1")
|
|
176
|
+
`);
|
|
177
|
+
fs.writeFileSync(path.join(apiDir, "users.py"), `
|
|
178
|
+
from fastapi import APIRouter
|
|
179
|
+
|
|
180
|
+
router = APIRouter(prefix="/users")
|
|
181
|
+
|
|
182
|
+
@router.get("/{id}")
|
|
183
|
+
async def get_user(): pass
|
|
184
|
+
`);
|
|
185
|
+
const endpoints = scanAllRepoEndpoints(repo);
|
|
186
|
+
expect(endpoints).toEqual(expect.arrayContaining([
|
|
187
|
+
expect.objectContaining({ path: "/api/v1/users/{id}", methods: ["GET"], sourceFile: "src/api/users.py" }),
|
|
188
|
+
]));
|
|
189
|
+
});
|
|
92
190
|
});
|
|
@@ -27,8 +27,14 @@ export declare function parseRouteLine(line: string, sourceFile: string): Parsed
|
|
|
27
27
|
*
|
|
28
28
|
* Falls back to pure parseRouteLine for all other frameworks (Express inline, Gin,
|
|
29
29
|
* Rails, Laravel, Actix, Hapi, Go stdlib, etc.), which remain unaffected.
|
|
30
|
+
*
|
|
31
|
+
* `mountPrefixes` optionally supplies, per router variable, the **ancestor**
|
|
32
|
+
* prefix that router inherits from cross-file `include_router(...)` mount points
|
|
33
|
+
* (see `resolvePythonMountPrefixes`). It is composed *ahead* of the router's own
|
|
34
|
+
* in-file prefix, so a `router = APIRouter(prefix="/recipes")` mounted under an
|
|
35
|
+
* aggregator's `APIRouter(prefix="/api")` yields `/api/recipes`, not `/recipes`.
|
|
30
36
|
*/
|
|
31
|
-
export declare function parseFileEndpoints(content: string, sourceFile: string): ParsedDiffEndpoint[];
|
|
37
|
+
export declare function parseFileEndpoints(content: string, sourceFile: string, mountPrefixes?: Map<string, string>): ParsedDiffEndpoint[];
|
|
32
38
|
export declare const SKIP_PATH_SEGMENTS: Set<string>;
|
|
33
39
|
/**
|
|
34
40
|
* True for version/prefix path segments — "v1".."v99" and the literal "api".
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { joinRoutePrefix } from "./pythonMountPrefixes.js";
|
|
1
2
|
import { logger } from "./logger.js";
|
|
2
3
|
export function nextjsFileToApiPath(filePath) {
|
|
3
4
|
const pagesMatch = filePath.match(/(?:^|\/)pages\/(api\/.+)\.[jt]sx?$/);
|
|
@@ -183,13 +184,29 @@ export function parseRouteLine(line, sourceFile) {
|
|
|
183
184
|
*
|
|
184
185
|
* Falls back to pure parseRouteLine for all other frameworks (Express inline, Gin,
|
|
185
186
|
* Rails, Laravel, Actix, Hapi, Go stdlib, etc.), which remain unaffected.
|
|
187
|
+
*
|
|
188
|
+
* `mountPrefixes` optionally supplies, per router variable, the **ancestor**
|
|
189
|
+
* prefix that router inherits from cross-file `include_router(...)` mount points
|
|
190
|
+
* (see `resolvePythonMountPrefixes`). It is composed *ahead* of the router's own
|
|
191
|
+
* in-file prefix, so a `router = APIRouter(prefix="/recipes")` mounted under an
|
|
192
|
+
* aggregator's `APIRouter(prefix="/api")` yields `/api/recipes`, not `/recipes`.
|
|
186
193
|
*/
|
|
187
|
-
export function parseFileEndpoints(content, sourceFile) {
|
|
194
|
+
export function parseFileEndpoints(content, sourceFile, mountPrefixes) {
|
|
188
195
|
const results = [];
|
|
189
196
|
let classPrefix = ""; // active prefix for the current class body
|
|
190
197
|
let pendingPrefix = ""; // set by class-level annotation; activated on class declaration
|
|
191
198
|
// FastAPI APIRouter / Flask Blueprint: variable name → prefix
|
|
192
199
|
const routerPrefixes = new Map();
|
|
200
|
+
// Effective prefix for a router var = its cross-file ancestor mount prefix
|
|
201
|
+
// composed with its own in-file APIRouter(prefix=…). Returns undefined when
|
|
202
|
+
// the var has neither, so callers leave the path untouched.
|
|
203
|
+
const effectivePrefix = (varName) => {
|
|
204
|
+
const own = routerPrefixes.get(varName);
|
|
205
|
+
const mount = mountPrefixes?.get(varName);
|
|
206
|
+
if (own === undefined && mount === undefined)
|
|
207
|
+
return undefined;
|
|
208
|
+
return joinRoutePrefix(mount ?? "", own ?? "");
|
|
209
|
+
};
|
|
193
210
|
let pendingRouterVar = ""; // variable name being accumulated in a multi-line declaration
|
|
194
211
|
let pendingPythonDecorator = null;
|
|
195
212
|
let pendingExpressMethod = ""; // HTTP method from a multi-line Express route (e.g. "POST" from `router.post(\n`)
|
|
@@ -254,15 +271,16 @@ export function parseFileEndpoints(content, sourceFile) {
|
|
|
254
271
|
}
|
|
255
272
|
continue;
|
|
256
273
|
}
|
|
257
|
-
// Single-line: varName = APIRouter(prefix="/path") or APIRouter(tags=..., prefix="/path")
|
|
258
|
-
|
|
274
|
+
// Single-line: varName = APIRouter(prefix="/path") or APIRouter(tags=..., prefix="/path").
|
|
275
|
+
// `\w*APIRouter` also matches custom subclasses (e.g. Mealie's UserAPIRouter).
|
|
276
|
+
const singleM = trimmed.match(/^(\w+)\s*=\s*\w*APIRouter\s*\([^)]*prefix\s*=\s*["']([^"'?#]+)["']/) ||
|
|
259
277
|
trimmed.match(/^(\w+)\s*=\s*Blueprint\s*\([^)]*url_prefix\s*=\s*["']([^"'?#]+)["']/);
|
|
260
278
|
if (singleM) {
|
|
261
279
|
routerPrefixes.set(singleM[1], singleM[2]);
|
|
262
280
|
continue;
|
|
263
281
|
}
|
|
264
282
|
// Multi-line open: varName = APIRouter( or Blueprint( without closing ) on this line
|
|
265
|
-
if (/^(\w+)\s*=\s*(
|
|
283
|
+
if (/^(\w+)\s*=\s*(?:\w*APIRouter|Blueprint)\s*\(/.test(trimmed) && !trimmed.includes(")")) {
|
|
266
284
|
const openM = trimmed.match(/^(\w+)/);
|
|
267
285
|
if (openM)
|
|
268
286
|
pendingRouterVar = openM[1];
|
|
@@ -278,15 +296,13 @@ export function parseFileEndpoints(content, sourceFile) {
|
|
|
278
296
|
const pathMatch = trimmed.match(/^(?:path\s*=\s*)?["']([^"'?#]*)/) ||
|
|
279
297
|
trimmed.match(/^value\s*=\s*["']([^"'?#]*)/);
|
|
280
298
|
if (pathMatch) {
|
|
281
|
-
const
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
const path = prefix
|
|
285
|
-
? (suffix ? `${prefix}/${suffix}` : prefix)
|
|
299
|
+
const eff = effectivePrefix(pendingPythonDecorator.routerVar);
|
|
300
|
+
const composedPath = eff !== undefined
|
|
301
|
+
? joinRoutePrefix(eff, pathMatch[1])
|
|
286
302
|
: pathMatch[1];
|
|
287
303
|
results.push({
|
|
288
304
|
method: pendingPythonDecorator.method,
|
|
289
|
-
path,
|
|
305
|
+
path: composedPath,
|
|
290
306
|
sourceFile,
|
|
291
307
|
line: pendingPythonDecorator.startLine,
|
|
292
308
|
});
|
|
@@ -448,17 +464,15 @@ export function parseFileEndpoints(content, sourceFile) {
|
|
|
448
464
|
ep.path = `${prefix}/${suffix}`;
|
|
449
465
|
}
|
|
450
466
|
}
|
|
451
|
-
else if (routerPrefixes.size > 0) {
|
|
452
|
-
// Router-variable prefix (FastAPI, Flask Blueprint)
|
|
467
|
+
else if (routerPrefixes.size > 0 || (mountPrefixes && mountPrefixes.size > 0)) {
|
|
468
|
+
// Router-variable prefix (FastAPI, Flask Blueprint) — own prefix composed
|
|
469
|
+
// with any cross-file ancestor mount prefix.
|
|
453
470
|
const varM = trimmed.match(/@(\w+)\.(get|post|put|patch|delete|head|options)\s*\(/i)
|
|
454
471
|
|| trimmed.match(/@(\w+)\.route\s*\(/i);
|
|
455
472
|
if (varM) {
|
|
456
|
-
const
|
|
457
|
-
if (
|
|
458
|
-
|
|
459
|
-
const suffix = ep.path.replace(/^\//, "");
|
|
460
|
-
ep.path = suffix ? `${prefix}/${suffix}` : prefix;
|
|
461
|
-
}
|
|
473
|
+
const eff = effectivePrefix(varM[1]);
|
|
474
|
+
if (eff !== undefined)
|
|
475
|
+
ep.path = joinRoutePrefix(eff, ep.path);
|
|
462
476
|
}
|
|
463
477
|
}
|
|
464
478
|
ep.line = lineNo;
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
import * as fs from "fs";
|
|
31
31
|
import * as path from "path";
|
|
32
32
|
import * as ts from "typescript";
|
|
33
|
+
import { collectFiles } from "./fileWalk.js";
|
|
33
34
|
function makeParseCache() {
|
|
34
35
|
return { files: new Map(), tsconfigBaseUrl: new Map() };
|
|
35
36
|
}
|
|
@@ -123,49 +124,25 @@ function parseFile(filePath, cache) {
|
|
|
123
124
|
}
|
|
124
125
|
// ── File discovery ────────────────────────────────────────────────────────
|
|
125
126
|
function findRouterFiles(repoPath) {
|
|
126
|
-
const matches = [];
|
|
127
127
|
const targets = new Set(ROUTER_FILE_GLOBS.map((g) => g.toLowerCase()));
|
|
128
128
|
const indexExtensions = new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
for (const entry of entries) {
|
|
142
|
-
if (matches.length >= MAX_CANDIDATE_FILES)
|
|
143
|
-
return;
|
|
144
|
-
const full = path.join(dir, entry.name);
|
|
145
|
-
if (entry.isDirectory()) {
|
|
146
|
-
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith("."))
|
|
147
|
-
continue;
|
|
148
|
-
walk(full, depth + 1);
|
|
149
|
-
}
|
|
150
|
-
else if (entry.isFile()) {
|
|
151
|
-
const lower = entry.name.toLowerCase();
|
|
152
|
-
if (targets.has(lower)) {
|
|
153
|
-
matches.push(full);
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
// Directus-style modules: `…/modules/<name>/index.{ts,tsx,js,jsx}`
|
|
157
|
-
// hosts a `defineModule({ routes: [...] })` call.
|
|
158
|
-
if (lower.startsWith("index.")) {
|
|
159
|
-
const ext = path.extname(lower);
|
|
160
|
-
if (indexExtensions.has(ext) && full.includes(`${path.sep}modules${path.sep}`)) {
|
|
161
|
-
matches.push(full);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
129
|
+
return collectFiles(repoPath, {
|
|
130
|
+
match: (entry, full) => {
|
|
131
|
+
const lower = entry.name.toLowerCase();
|
|
132
|
+
if (targets.has(lower))
|
|
133
|
+
return true;
|
|
134
|
+
// Directus-style modules: `…/modules/<name>/index.{ts,tsx,js,jsx}`
|
|
135
|
+
// hosts a `defineModule({ routes: [...] })` call.
|
|
136
|
+
if (lower.startsWith("index.")) {
|
|
137
|
+
const ext = path.extname(lower);
|
|
138
|
+
return indexExtensions.has(ext) && full.includes(`${path.sep}modules${path.sep}`);
|
|
164
139
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
140
|
+
return false;
|
|
141
|
+
},
|
|
142
|
+
limit: MAX_CANDIDATE_FILES,
|
|
143
|
+
maxDepth: MAX_WALK_DEPTH,
|
|
144
|
+
shouldSkipDir: (entry) => SKIP_DIRS.has(entry.name) || entry.name.startsWith("."),
|
|
145
|
+
});
|
|
169
146
|
}
|
|
170
147
|
// ── Per-file extraction ────────────────────────────────────────────────────
|
|
171
148
|
function extractRoutesFromFile(filePath, cache) {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import * as fs from "fs";
|
|
6
6
|
import * as path from "path";
|
|
7
7
|
import { KNOWN_AUTH_HEADERS } from "./workspaceAuth.js";
|
|
8
|
+
import { walkDir } from "./fileWalk.js";
|
|
8
9
|
const FLOW_GAP_MS = 30_000;
|
|
9
10
|
// Redact-only headers: scrub these from traces but don't treat them as extractable auth headers.
|
|
10
11
|
// set-cookie carries session tokens; auth-token / client-secret / access-token are generic secrets.
|
|
@@ -168,27 +169,14 @@ const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", ".next", ".n
|
|
|
168
169
|
/** Known test-artifact directories where testbot-generated traces are written. */
|
|
169
170
|
const TRACE_SCAN_DIRS = [".skyramp", "tests", "test", "e2e", "playwright"];
|
|
170
171
|
/**
|
|
171
|
-
* Recursively scan a directory for files matching a predicate, up to maxDepth
|
|
172
|
+
* Recursively scan a directory for files matching a predicate, up to maxDepth
|
|
173
|
+
* levels below `dir` (maxDepth 0 = only files directly in `dir`).
|
|
172
174
|
*/
|
|
173
175
|
function scanDir(dir, predicate, maxDepth, results) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
179
|
-
}
|
|
180
|
-
catch {
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
for (const entry of entries) {
|
|
184
|
-
if (entry.isDirectory()) {
|
|
185
|
-
if (!SKIP_DIRS.has(entry.name)) {
|
|
186
|
-
scanDir(path.join(dir, entry.name), predicate, maxDepth - 1, results);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
else if (entry.isFile() && predicate(entry.name)) {
|
|
190
|
-
results.push(path.join(dir, entry.name));
|
|
191
|
-
}
|
|
176
|
+
const opts = { maxDepth, shouldSkipDir: (entry) => SKIP_DIRS.has(entry.name) };
|
|
177
|
+
for (const [entry, full] of walkDir(dir, opts)) {
|
|
178
|
+
if (predicate(entry.name))
|
|
179
|
+
results.push(full);
|
|
192
180
|
}
|
|
193
181
|
}
|
|
194
182
|
/**
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export declare const SKYRAMP_IMAGE_VERSION = "v1.3.
|
|
2
|
-
export declare const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.3.
|
|
3
|
-
export declare const WORKER_DOCKER_IMAGE = "skyramp/worker:v1.3.
|
|
1
|
+
export declare const SKYRAMP_IMAGE_VERSION = "v1.3.34";
|
|
2
|
+
export declare const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.3.34";
|
|
3
|
+
export declare const WORKER_DOCKER_IMAGE = "skyramp/worker:v1.3.34";
|
package/build/utils/versions.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|