codesight 1.0.0 → 1.1.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/README.md +200 -104
- package/dist/detectors/contracts.js +7 -0
- package/dist/detectors/graph.js +87 -23
- package/dist/detectors/routes.js +519 -13
- package/dist/formatter.js +20 -7
- package/dist/index.js +37 -1
- package/dist/scanner.js +125 -6
- package/dist/types.d.ts +3 -3
- package/package.json +2 -1
package/dist/detectors/routes.js
CHANGED
|
@@ -42,6 +42,27 @@ export async function detectRoutes(files, project) {
|
|
|
42
42
|
case "koa":
|
|
43
43
|
routes.push(...(await detectKoaRoutes(files, project)));
|
|
44
44
|
break;
|
|
45
|
+
case "nestjs":
|
|
46
|
+
routes.push(...(await detectNestJSRoutes(files, project)));
|
|
47
|
+
break;
|
|
48
|
+
case "elysia":
|
|
49
|
+
routes.push(...(await detectElysiaRoutes(files, project)));
|
|
50
|
+
break;
|
|
51
|
+
case "adonis":
|
|
52
|
+
routes.push(...(await detectAdonisRoutes(files, project)));
|
|
53
|
+
break;
|
|
54
|
+
case "trpc":
|
|
55
|
+
routes.push(...(await detectTRPCRoutes(files, project)));
|
|
56
|
+
break;
|
|
57
|
+
case "sveltekit":
|
|
58
|
+
routes.push(...(await detectSvelteKitRoutes(files, project)));
|
|
59
|
+
break;
|
|
60
|
+
case "remix":
|
|
61
|
+
routes.push(...(await detectRemixRoutes(files, project)));
|
|
62
|
+
break;
|
|
63
|
+
case "nuxt":
|
|
64
|
+
routes.push(...(await detectNuxtRoutes(files, project)));
|
|
65
|
+
break;
|
|
45
66
|
case "fastapi":
|
|
46
67
|
routes.push(...(await detectFastAPIRoutes(files, project)));
|
|
47
68
|
break;
|
|
@@ -54,8 +75,26 @@ export async function detectRoutes(files, project) {
|
|
|
54
75
|
case "gin":
|
|
55
76
|
case "go-net-http":
|
|
56
77
|
case "fiber":
|
|
78
|
+
case "echo":
|
|
79
|
+
case "chi":
|
|
57
80
|
routes.push(...(await detectGoRoutes(files, project, fw)));
|
|
58
81
|
break;
|
|
82
|
+
case "rails":
|
|
83
|
+
routes.push(...(await detectRailsRoutes(files, project)));
|
|
84
|
+
break;
|
|
85
|
+
case "phoenix":
|
|
86
|
+
routes.push(...(await detectPhoenixRoutes(files, project)));
|
|
87
|
+
break;
|
|
88
|
+
case "spring":
|
|
89
|
+
routes.push(...(await detectSpringRoutes(files, project)));
|
|
90
|
+
break;
|
|
91
|
+
case "actix":
|
|
92
|
+
case "axum":
|
|
93
|
+
routes.push(...(await detectRustRoutes(files, project, fw)));
|
|
94
|
+
break;
|
|
95
|
+
case "raw-http":
|
|
96
|
+
routes.push(...(await detectRawHttpRoutes(files, project)));
|
|
97
|
+
break;
|
|
59
98
|
}
|
|
60
99
|
}
|
|
61
100
|
return routes;
|
|
@@ -67,7 +106,6 @@ async function detectNextAppRoutes(files, project) {
|
|
|
67
106
|
for (const file of routeFiles) {
|
|
68
107
|
const content = await readFileSafe(file);
|
|
69
108
|
const rel = relative(project.root, file);
|
|
70
|
-
// Extract API path from file path
|
|
71
109
|
const pathMatch = rel.match(/(?:src\/)?app(.*)\/route\./);
|
|
72
110
|
const apiPath = pathMatch ? pathMatch[1] || "/" : "/";
|
|
73
111
|
for (const method of HTTP_METHODS) {
|
|
@@ -95,7 +133,6 @@ async function detectNextPagesApi(files, project) {
|
|
|
95
133
|
const pathMatch = rel.match(/(?:src\/)?pages(\/api\/.*)\.(?:ts|js|tsx|jsx)$/);
|
|
96
134
|
let apiPath = pathMatch ? pathMatch[1] : "/api";
|
|
97
135
|
apiPath = apiPath.replace(/\/index$/, "").replace(/\[([^\]]+)\]/g, ":$1");
|
|
98
|
-
// Detect methods from handler
|
|
99
136
|
const methods = [];
|
|
100
137
|
for (const method of HTTP_METHODS) {
|
|
101
138
|
if (content.includes(`req.method === '${method}'`) || content.includes(`req.method === "${method}"`)) {
|
|
@@ -125,12 +162,10 @@ async function detectHonoRoutes(files, project) {
|
|
|
125
162
|
if (!content.includes("hono") && !content.includes("Hono"))
|
|
126
163
|
continue;
|
|
127
164
|
const rel = relative(project.root, file);
|
|
128
|
-
// Match: app.get("/path", ...), router.post("/path", ...), .route("/base", ...)
|
|
129
165
|
const routePattern = /\.\s*(get|post|put|patch|delete|options|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
|
|
130
166
|
let match;
|
|
131
167
|
while ((match = routePattern.exec(content)) !== null) {
|
|
132
168
|
const path = match[2];
|
|
133
|
-
// Skip non-path strings (middleware keys like "user", "userId", etc.)
|
|
134
169
|
if (!path.startsWith("/") && !path.startsWith(":"))
|
|
135
170
|
continue;
|
|
136
171
|
routes.push({
|
|
@@ -153,7 +188,6 @@ async function detectExpressRoutes(files, project) {
|
|
|
153
188
|
if (!content.includes("express") && !content.includes("Router"))
|
|
154
189
|
continue;
|
|
155
190
|
const rel = relative(project.root, file);
|
|
156
|
-
// Match: app.get("/path", ...), router.post("/path", ...)
|
|
157
191
|
const routePattern = /(?:app|router|server)\s*\.\s*(get|post|put|patch|delete|options|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
|
|
158
192
|
let match;
|
|
159
193
|
while ((match = routePattern.exec(content)) !== null) {
|
|
@@ -177,7 +211,6 @@ async function detectFastifyRoutes(files, project) {
|
|
|
177
211
|
if (!content.includes("fastify"))
|
|
178
212
|
continue;
|
|
179
213
|
const rel = relative(project.root, file);
|
|
180
|
-
// Match: fastify.get("/path", ...) or server.route({ method: 'GET', url: '/path' })
|
|
181
214
|
const routePattern = /(?:fastify|server|app)\s*\.\s*(get|post|put|patch|delete|options|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
|
|
182
215
|
let match;
|
|
183
216
|
while ((match = routePattern.exec(content)) !== null) {
|
|
@@ -226,6 +259,233 @@ async function detectKoaRoutes(files, project) {
|
|
|
226
259
|
}
|
|
227
260
|
return routes;
|
|
228
261
|
}
|
|
262
|
+
// --- NestJS ---
|
|
263
|
+
async function detectNestJSRoutes(files, project) {
|
|
264
|
+
const tsFiles = files.filter((f) => f.match(/\.(ts|js)$/));
|
|
265
|
+
const routes = [];
|
|
266
|
+
for (const file of tsFiles) {
|
|
267
|
+
const content = await readFileSafe(file);
|
|
268
|
+
if (!content.includes("@Controller") && !content.includes("@Get") && !content.includes("@Post"))
|
|
269
|
+
continue;
|
|
270
|
+
const rel = relative(project.root, file);
|
|
271
|
+
// Extract controller base path: @Controller('users') or @Controller('/users')
|
|
272
|
+
const controllerMatch = content.match(/@Controller\s*\(\s*['"`]([^'"`]*)['"`]\s*\)/);
|
|
273
|
+
const basePath = controllerMatch ? "/" + controllerMatch[1].replace(/^\//, "") : "";
|
|
274
|
+
// Match method decorators: @Get(), @Post('/create'), @Put(':id'), etc.
|
|
275
|
+
const decoratorPattern = /@(Get|Post|Put|Patch|Delete|Options|Head|All)\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/gi;
|
|
276
|
+
let match;
|
|
277
|
+
while ((match = decoratorPattern.exec(content)) !== null) {
|
|
278
|
+
const method = match[1].toUpperCase();
|
|
279
|
+
const subPath = match[2] || "";
|
|
280
|
+
const fullPath = basePath + (subPath ? "/" + subPath.replace(/^\//, "") : "") || "/";
|
|
281
|
+
routes.push({
|
|
282
|
+
method,
|
|
283
|
+
path: fullPath,
|
|
284
|
+
file: rel,
|
|
285
|
+
tags: detectTags(content),
|
|
286
|
+
framework: "nestjs",
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return routes;
|
|
291
|
+
}
|
|
292
|
+
// --- Elysia (Bun) ---
|
|
293
|
+
async function detectElysiaRoutes(files, project) {
|
|
294
|
+
const tsFiles = files.filter((f) => f.match(/\.(ts|js|mjs)$/));
|
|
295
|
+
const routes = [];
|
|
296
|
+
for (const file of tsFiles) {
|
|
297
|
+
const content = await readFileSafe(file);
|
|
298
|
+
if (!content.includes("elysia") && !content.includes("Elysia"))
|
|
299
|
+
continue;
|
|
300
|
+
const rel = relative(project.root, file);
|
|
301
|
+
const routePattern = /\.\s*(get|post|put|patch|delete|options|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
|
|
302
|
+
let match;
|
|
303
|
+
while ((match = routePattern.exec(content)) !== null) {
|
|
304
|
+
const path = match[2];
|
|
305
|
+
if (!path.startsWith("/") && !path.startsWith(":"))
|
|
306
|
+
continue;
|
|
307
|
+
routes.push({
|
|
308
|
+
method: match[1].toUpperCase(),
|
|
309
|
+
path,
|
|
310
|
+
file: rel,
|
|
311
|
+
tags: detectTags(content),
|
|
312
|
+
framework: "elysia",
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return routes;
|
|
317
|
+
}
|
|
318
|
+
// --- AdonisJS ---
|
|
319
|
+
async function detectAdonisRoutes(files, project) {
|
|
320
|
+
// AdonisJS uses start/routes.ts with Route.get(), Route.post(), router.get(), etc.
|
|
321
|
+
const routeFiles = files.filter((f) => f.match(/routes\.(ts|js)$/) || f.match(/\/routes\/.*\.(ts|js)$/));
|
|
322
|
+
const routes = [];
|
|
323
|
+
for (const file of routeFiles) {
|
|
324
|
+
const content = await readFileSafe(file);
|
|
325
|
+
const rel = relative(project.root, file);
|
|
326
|
+
const routePattern = /(?:Route|router)\s*\.\s*(get|post|put|patch|delete|any)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
|
|
327
|
+
let match;
|
|
328
|
+
while ((match = routePattern.exec(content)) !== null) {
|
|
329
|
+
routes.push({
|
|
330
|
+
method: match[1].toUpperCase() === "ANY" ? "ALL" : match[1].toUpperCase(),
|
|
331
|
+
path: match[2],
|
|
332
|
+
file: rel,
|
|
333
|
+
tags: detectTags(content),
|
|
334
|
+
framework: "adonis",
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return routes;
|
|
339
|
+
}
|
|
340
|
+
// --- tRPC ---
|
|
341
|
+
async function detectTRPCRoutes(files, project) {
|
|
342
|
+
const tsFiles = files.filter((f) => f.match(/\.(ts|js)$/));
|
|
343
|
+
const routes = [];
|
|
344
|
+
for (const file of tsFiles) {
|
|
345
|
+
const content = await readFileSafe(file);
|
|
346
|
+
if (!content.includes("Procedure") && !content.includes("procedure") && !content.includes("router"))
|
|
347
|
+
continue;
|
|
348
|
+
if (!content.includes("trpc") && !content.includes("TRPC") && !content.includes("createTRPCRouter") && !content.includes("publicProcedure") && !content.includes("protectedProcedure"))
|
|
349
|
+
continue;
|
|
350
|
+
const rel = relative(project.root, file);
|
|
351
|
+
// Match tRPC procedure definitions like:
|
|
352
|
+
// list: publicProcedure.query(...)
|
|
353
|
+
// create: publicProcedure.input(schema).mutation(...)
|
|
354
|
+
// getById: t.procedure.input(z.object({...})).query(...)
|
|
355
|
+
// Strategy: find lines with "query(" or "mutation(" and extract the procedure name
|
|
356
|
+
const lines = content.split("\n");
|
|
357
|
+
for (const line of lines) {
|
|
358
|
+
const queryMatch = line.match(/^\s*(\w+)\s*:\s*.*\.(query)\s*\(/);
|
|
359
|
+
const mutationMatch = line.match(/^\s*(\w+)\s*:\s*.*\.(mutation)\s*\(/);
|
|
360
|
+
const m = queryMatch || mutationMatch;
|
|
361
|
+
if (m) {
|
|
362
|
+
const procName = m[1];
|
|
363
|
+
const isQuery = m[2] === "query";
|
|
364
|
+
if (!routes.some((r) => r.path === procName && r.file === rel)) {
|
|
365
|
+
routes.push({
|
|
366
|
+
method: isQuery ? "QUERY" : "MUTATION",
|
|
367
|
+
path: procName,
|
|
368
|
+
file: rel,
|
|
369
|
+
tags: detectTags(content),
|
|
370
|
+
framework: "trpc",
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return routes;
|
|
377
|
+
}
|
|
378
|
+
// --- SvelteKit ---
|
|
379
|
+
async function detectSvelteKitRoutes(files, project) {
|
|
380
|
+
// SvelteKit API routes: src/routes/**/+server.ts
|
|
381
|
+
const routeFiles = files.filter((f) => f.match(/\/routes\/.*\+server\.(ts|js)$/));
|
|
382
|
+
const routes = [];
|
|
383
|
+
for (const file of routeFiles) {
|
|
384
|
+
const content = await readFileSafe(file);
|
|
385
|
+
const rel = relative(project.root, file);
|
|
386
|
+
// Extract path from file structure: src/routes/api/users/+server.ts -> /api/users
|
|
387
|
+
const pathMatch = rel.match(/(?:src\/)?routes(.*)\/\+server\./);
|
|
388
|
+
let apiPath = pathMatch ? pathMatch[1] || "/" : "/";
|
|
389
|
+
// Convert [param] to :param
|
|
390
|
+
apiPath = apiPath.replace(/\[([^\]]+)\]/g, ":$1");
|
|
391
|
+
for (const method of HTTP_METHODS) {
|
|
392
|
+
const pattern = new RegExp(`export\\s+(?:async\\s+)?function\\s+${method}\\b`);
|
|
393
|
+
if (pattern.test(content)) {
|
|
394
|
+
routes.push({
|
|
395
|
+
method,
|
|
396
|
+
path: apiPath,
|
|
397
|
+
file: rel,
|
|
398
|
+
tags: detectTags(content),
|
|
399
|
+
framework: "sveltekit",
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
// Also detect: export const GET = ...
|
|
404
|
+
for (const method of HTTP_METHODS) {
|
|
405
|
+
const constPattern = new RegExp(`export\\s+const\\s+${method}\\s*[=:]`);
|
|
406
|
+
if (constPattern.test(content) && !routes.some((r) => r.method === method && r.path === apiPath)) {
|
|
407
|
+
routes.push({
|
|
408
|
+
method,
|
|
409
|
+
path: apiPath,
|
|
410
|
+
file: rel,
|
|
411
|
+
tags: detectTags(content),
|
|
412
|
+
framework: "sveltekit",
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return routes;
|
|
418
|
+
}
|
|
419
|
+
// --- Remix ---
|
|
420
|
+
async function detectRemixRoutes(files, project) {
|
|
421
|
+
// Remix routes: app/routes/*.tsx with loader/action exports
|
|
422
|
+
const routeFiles = files.filter((f) => f.match(/\/routes\/.*\.(ts|tsx|js|jsx)$/));
|
|
423
|
+
const routes = [];
|
|
424
|
+
for (const file of routeFiles) {
|
|
425
|
+
const content = await readFileSafe(file);
|
|
426
|
+
const rel = relative(project.root, file);
|
|
427
|
+
// Convert filename to route path
|
|
428
|
+
const pathMatch = rel.match(/(?:app\/)?routes\/(.+)\.(ts|tsx|js|jsx)$/);
|
|
429
|
+
if (!pathMatch)
|
|
430
|
+
continue;
|
|
431
|
+
let routePath = "/" + pathMatch[1]
|
|
432
|
+
.replace(/\./g, "/") // dots become path segments
|
|
433
|
+
.replace(/_index$/, "") // _index -> root of parent
|
|
434
|
+
.replace(/\$/g, ":") // $param -> :param
|
|
435
|
+
.replace(/\[([^\]]+)\]/g, ":$1");
|
|
436
|
+
if (content.match(/export\s+(?:async\s+)?function\s+loader\b/) || content.match(/export\s+const\s+loader\b/)) {
|
|
437
|
+
routes.push({
|
|
438
|
+
method: "GET",
|
|
439
|
+
path: routePath,
|
|
440
|
+
file: rel,
|
|
441
|
+
tags: detectTags(content),
|
|
442
|
+
framework: "remix",
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
if (content.match(/export\s+(?:async\s+)?function\s+action\b/) || content.match(/export\s+const\s+action\b/)) {
|
|
446
|
+
routes.push({
|
|
447
|
+
method: "POST",
|
|
448
|
+
path: routePath,
|
|
449
|
+
file: rel,
|
|
450
|
+
tags: detectTags(content),
|
|
451
|
+
framework: "remix",
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return routes;
|
|
456
|
+
}
|
|
457
|
+
// --- Nuxt ---
|
|
458
|
+
async function detectNuxtRoutes(files, project) {
|
|
459
|
+
// Nuxt server routes: server/api/**/*.ts
|
|
460
|
+
const routeFiles = files.filter((f) => f.match(/\/server\/(?:api|routes)\/.*\.(ts|js|mjs)$/));
|
|
461
|
+
const routes = [];
|
|
462
|
+
for (const file of routeFiles) {
|
|
463
|
+
const content = await readFileSafe(file);
|
|
464
|
+
const rel = relative(project.root, file);
|
|
465
|
+
// Extract path from file structure
|
|
466
|
+
const pathMatch = rel.match(/server\/((?:api|routes)\/.+)\.(ts|js|mjs)$/);
|
|
467
|
+
if (!pathMatch)
|
|
468
|
+
continue;
|
|
469
|
+
let routePath = "/" + pathMatch[1]
|
|
470
|
+
.replace(/\/index$/, "")
|
|
471
|
+
.replace(/\[([^\]]+)\]/g, ":$1");
|
|
472
|
+
// Detect method from filename (e.g., users.get.ts, users.post.ts)
|
|
473
|
+
const methodFromFile = basename(file).match(/\.(get|post|put|patch|delete)\.(ts|js|mjs)$/);
|
|
474
|
+
const method = methodFromFile ? methodFromFile[1].toUpperCase() : "ALL";
|
|
475
|
+
// Clean path: remove method suffix from path
|
|
476
|
+
if (methodFromFile) {
|
|
477
|
+
routePath = routePath.replace(new RegExp(`\\.${methodFromFile[1]}$`), "");
|
|
478
|
+
}
|
|
479
|
+
routes.push({
|
|
480
|
+
method,
|
|
481
|
+
path: routePath,
|
|
482
|
+
file: rel,
|
|
483
|
+
tags: detectTags(content),
|
|
484
|
+
framework: "nuxt",
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
return routes;
|
|
488
|
+
}
|
|
229
489
|
// --- FastAPI ---
|
|
230
490
|
async function detectFastAPIRoutes(files, project) {
|
|
231
491
|
const pyFiles = files.filter((f) => f.endsWith(".py"));
|
|
@@ -235,7 +495,6 @@ async function detectFastAPIRoutes(files, project) {
|
|
|
235
495
|
if (!content.includes("fastapi") && !content.includes("FastAPI") && !content.includes("APIRouter"))
|
|
236
496
|
continue;
|
|
237
497
|
const rel = relative(project.root, file);
|
|
238
|
-
// Match: @app.get("/path") or @router.post("/path") or @api_router.get("/path")
|
|
239
498
|
const routePattern = /@\w+\s*\.\s*(get|post|put|patch|delete|options)\s*\(\s*['"]([^'"]+)['"]/gi;
|
|
240
499
|
let match;
|
|
241
500
|
while ((match = routePattern.exec(content)) !== null) {
|
|
@@ -259,8 +518,7 @@ async function detectFlaskRoutes(files, project) {
|
|
|
259
518
|
if (!content.includes("flask") && !content.includes("Flask") && !content.includes("Blueprint"))
|
|
260
519
|
continue;
|
|
261
520
|
const rel = relative(project.root, file);
|
|
262
|
-
|
|
263
|
-
const routePattern = /@(?:app|bp|blueprint)\s*\.\s*route\s*\(\s*['"]([^'"]+)['"](?:\s*,\s*methods\s*=\s*\[([^\]]+)\])?\s*\)/gi;
|
|
521
|
+
const routePattern = /@(?:app|bp|blueprint|\w+)\s*\.\s*route\s*\(\s*['"]([^'"]+)['"](?:\s*,\s*methods\s*=\s*\[([^\]]+)\])?\s*\)/gi;
|
|
264
522
|
let match;
|
|
265
523
|
while ((match = routePattern.exec(content)) !== null) {
|
|
266
524
|
const path = match[1];
|
|
@@ -287,7 +545,7 @@ async function detectDjangoRoutes(files, project) {
|
|
|
287
545
|
for (const file of pyFiles) {
|
|
288
546
|
const content = await readFileSafe(file);
|
|
289
547
|
const rel = relative(project.root, file);
|
|
290
|
-
//
|
|
548
|
+
// path("api/v1/users/", views.UserView.as_view())
|
|
291
549
|
const pathPattern = /path\s*\(\s*['"]([^'"]*)['"]\s*,/g;
|
|
292
550
|
let match;
|
|
293
551
|
while ((match = pathPattern.exec(content)) !== null) {
|
|
@@ -302,7 +560,7 @@ async function detectDjangoRoutes(files, project) {
|
|
|
302
560
|
}
|
|
303
561
|
return routes;
|
|
304
562
|
}
|
|
305
|
-
// --- Go ---
|
|
563
|
+
// --- Go (net/http, Gin, Fiber, Echo, Chi) ---
|
|
306
564
|
async function detectGoRoutes(files, project, fw) {
|
|
307
565
|
const goFiles = files.filter((f) => f.endsWith(".go"));
|
|
308
566
|
const routes = [];
|
|
@@ -310,7 +568,6 @@ async function detectGoRoutes(files, project, fw) {
|
|
|
310
568
|
const content = await readFileSafe(file);
|
|
311
569
|
const rel = relative(project.root, file);
|
|
312
570
|
if (fw === "gin") {
|
|
313
|
-
// Match: r.GET("/path", handler)
|
|
314
571
|
const pattern = /\.\s*(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s*\(\s*["']([^"']+)["']/g;
|
|
315
572
|
let match;
|
|
316
573
|
while ((match = pattern.exec(content)) !== null) {
|
|
@@ -324,7 +581,34 @@ async function detectGoRoutes(files, project, fw) {
|
|
|
324
581
|
}
|
|
325
582
|
}
|
|
326
583
|
else if (fw === "fiber") {
|
|
327
|
-
|
|
584
|
+
const pattern = /\.\s*(Get|Post|Put|Patch|Delete|Options|Head)\s*\(\s*["']([^"']+)["']/g;
|
|
585
|
+
let match;
|
|
586
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
587
|
+
routes.push({
|
|
588
|
+
method: match[1].toUpperCase(),
|
|
589
|
+
path: match[2],
|
|
590
|
+
file: rel,
|
|
591
|
+
tags: detectTags(content),
|
|
592
|
+
framework: fw,
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
else if (fw === "echo") {
|
|
597
|
+
// e.GET("/path", handler) or g.POST("/path", handler)
|
|
598
|
+
const pattern = /\.\s*(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s*\(\s*["']([^"']+)["']/g;
|
|
599
|
+
let match;
|
|
600
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
601
|
+
routes.push({
|
|
602
|
+
method: match[1],
|
|
603
|
+
path: match[2],
|
|
604
|
+
file: rel,
|
|
605
|
+
tags: detectTags(content),
|
|
606
|
+
framework: fw,
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
else if (fw === "chi") {
|
|
611
|
+
// r.Get("/path", handler), r.Post("/path", handler)
|
|
328
612
|
const pattern = /\.\s*(Get|Post|Put|Patch|Delete|Options|Head)\s*\(\s*["']([^"']+)["']/g;
|
|
329
613
|
let match;
|
|
330
614
|
while ((match = pattern.exec(content)) !== null) {
|
|
@@ -354,3 +638,225 @@ async function detectGoRoutes(files, project, fw) {
|
|
|
354
638
|
}
|
|
355
639
|
return routes;
|
|
356
640
|
}
|
|
641
|
+
// --- Rails ---
|
|
642
|
+
async function detectRailsRoutes(files, project) {
|
|
643
|
+
const routeFiles = files.filter((f) => f.match(/routes\.rb$/));
|
|
644
|
+
const routes = [];
|
|
645
|
+
for (const file of routeFiles) {
|
|
646
|
+
const content = await readFileSafe(file);
|
|
647
|
+
const rel = relative(project.root, file);
|
|
648
|
+
// get '/users', to: 'users#index'
|
|
649
|
+
const routePattern = /\b(get|post|put|patch|delete)\s+['"]([^'"]+)['"]/gi;
|
|
650
|
+
let match;
|
|
651
|
+
while ((match = routePattern.exec(content)) !== null) {
|
|
652
|
+
routes.push({
|
|
653
|
+
method: match[1].toUpperCase(),
|
|
654
|
+
path: match[2],
|
|
655
|
+
file: rel,
|
|
656
|
+
tags: detectTags(content),
|
|
657
|
+
framework: "rails",
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
// resources :users (generates RESTful routes)
|
|
661
|
+
const resourcePattern = /resources?\s+:(\w+)/g;
|
|
662
|
+
while ((match = resourcePattern.exec(content)) !== null) {
|
|
663
|
+
const name = match[1];
|
|
664
|
+
for (const [method, suffix] of [
|
|
665
|
+
["GET", ""], ["GET", "/:id"], ["POST", ""],
|
|
666
|
+
["PUT", "/:id"], ["PATCH", "/:id"], ["DELETE", "/:id"],
|
|
667
|
+
]) {
|
|
668
|
+
routes.push({
|
|
669
|
+
method,
|
|
670
|
+
path: `/${name}${suffix}`,
|
|
671
|
+
file: rel,
|
|
672
|
+
tags: detectTags(content),
|
|
673
|
+
framework: "rails",
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return routes;
|
|
679
|
+
}
|
|
680
|
+
// --- Phoenix (Elixir) ---
|
|
681
|
+
async function detectPhoenixRoutes(files, project) {
|
|
682
|
+
const routeFiles = files.filter((f) => f.match(/router\.ex$/));
|
|
683
|
+
const routes = [];
|
|
684
|
+
for (const file of routeFiles) {
|
|
685
|
+
const content = await readFileSafe(file);
|
|
686
|
+
const rel = relative(project.root, file);
|
|
687
|
+
// get "/users", UserController, :index
|
|
688
|
+
const routePattern = /\b(get|post|put|patch|delete)\s+["']([^"']+)["']/gi;
|
|
689
|
+
let match;
|
|
690
|
+
while ((match = routePattern.exec(content)) !== null) {
|
|
691
|
+
routes.push({
|
|
692
|
+
method: match[1].toUpperCase(),
|
|
693
|
+
path: match[2],
|
|
694
|
+
file: rel,
|
|
695
|
+
tags: detectTags(content),
|
|
696
|
+
framework: "phoenix",
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
// resources "/users", UserController
|
|
700
|
+
const resourcePattern = /resources\s+["']([^"']+)["']/g;
|
|
701
|
+
while ((match = resourcePattern.exec(content)) !== null) {
|
|
702
|
+
const basePath = match[1];
|
|
703
|
+
for (const [method, suffix] of [
|
|
704
|
+
["GET", ""], ["GET", "/:id"], ["POST", ""],
|
|
705
|
+
["PUT", "/:id"], ["PATCH", "/:id"], ["DELETE", "/:id"],
|
|
706
|
+
]) {
|
|
707
|
+
routes.push({
|
|
708
|
+
method,
|
|
709
|
+
path: `${basePath}${suffix}`,
|
|
710
|
+
file: rel,
|
|
711
|
+
tags: detectTags(content),
|
|
712
|
+
framework: "phoenix",
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
return routes;
|
|
718
|
+
}
|
|
719
|
+
// --- Spring Boot (Java/Kotlin) ---
|
|
720
|
+
async function detectSpringRoutes(files, project) {
|
|
721
|
+
const javaFiles = files.filter((f) => f.match(/\.(java|kt)$/));
|
|
722
|
+
const routes = [];
|
|
723
|
+
for (const file of javaFiles) {
|
|
724
|
+
const content = await readFileSafe(file);
|
|
725
|
+
if (!content.includes("@RestController") && !content.includes("@Controller") && !content.includes("@RequestMapping"))
|
|
726
|
+
continue;
|
|
727
|
+
const rel = relative(project.root, file);
|
|
728
|
+
// Extract class-level @RequestMapping
|
|
729
|
+
const classMapping = content.match(/@RequestMapping\s*\(\s*(?:value\s*=\s*)?["']([^"']+)["']/);
|
|
730
|
+
const basePath = classMapping ? classMapping[1] : "";
|
|
731
|
+
// @GetMapping("/path"), @PostMapping("/path"), etc.
|
|
732
|
+
const mappingPattern = /@(Get|Post|Put|Patch|Delete)Mapping\s*\(\s*(?:value\s*=\s*)?(?:["']([^"']*)["'])?\s*\)/gi;
|
|
733
|
+
let match;
|
|
734
|
+
while ((match = mappingPattern.exec(content)) !== null) {
|
|
735
|
+
const method = match[1].toUpperCase();
|
|
736
|
+
const subPath = match[2] || "";
|
|
737
|
+
routes.push({
|
|
738
|
+
method,
|
|
739
|
+
path: basePath + subPath || "/",
|
|
740
|
+
file: rel,
|
|
741
|
+
tags: detectTags(content),
|
|
742
|
+
framework: "spring",
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
// @RequestMapping(method = RequestMethod.GET, value = "/path")
|
|
746
|
+
const reqMappingPattern = /@RequestMapping\s*\([^)]*method\s*=\s*RequestMethod\.(\w+)[^)]*value\s*=\s*["']([^"']+)["']/gi;
|
|
747
|
+
while ((match = reqMappingPattern.exec(content)) !== null) {
|
|
748
|
+
routes.push({
|
|
749
|
+
method: match[1].toUpperCase(),
|
|
750
|
+
path: basePath + match[2],
|
|
751
|
+
file: rel,
|
|
752
|
+
tags: detectTags(content),
|
|
753
|
+
framework: "spring",
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
return routes;
|
|
758
|
+
}
|
|
759
|
+
// --- Rust (Actix-web, Axum) ---
|
|
760
|
+
async function detectRustRoutes(files, project, fw) {
|
|
761
|
+
const rsFiles = files.filter((f) => f.endsWith(".rs"));
|
|
762
|
+
const routes = [];
|
|
763
|
+
for (const file of rsFiles) {
|
|
764
|
+
const content = await readFileSafe(file);
|
|
765
|
+
const rel = relative(project.root, file);
|
|
766
|
+
if (fw === "actix") {
|
|
767
|
+
// #[get("/path")], #[post("/path")], etc.
|
|
768
|
+
const attrPattern = /#\[(get|post|put|patch|delete)\s*\(\s*"([^"]+)"\s*\)\s*\]/gi;
|
|
769
|
+
let match;
|
|
770
|
+
while ((match = attrPattern.exec(content)) !== null) {
|
|
771
|
+
routes.push({
|
|
772
|
+
method: match[1].toUpperCase(),
|
|
773
|
+
path: match[2],
|
|
774
|
+
file: rel,
|
|
775
|
+
tags: detectTags(content),
|
|
776
|
+
framework: "actix",
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
// .route("/path", web::get().to(handler))
|
|
780
|
+
const routePattern = /\.route\s*\(\s*"([^"]+)"\s*,\s*web::(get|post|put|patch|delete)\s*\(\s*\)/gi;
|
|
781
|
+
while ((match = routePattern.exec(content)) !== null) {
|
|
782
|
+
routes.push({
|
|
783
|
+
method: match[2].toUpperCase(),
|
|
784
|
+
path: match[1],
|
|
785
|
+
file: rel,
|
|
786
|
+
tags: detectTags(content),
|
|
787
|
+
framework: "actix",
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
else if (fw === "axum") {
|
|
792
|
+
// .route("/path", get(handler)) or .route("/path", post(handler).get(handler))
|
|
793
|
+
const routePattern = /\.route\s*\(\s*"([^"]+)"\s*,\s*(get|post|put|patch|delete)\s*\(/gi;
|
|
794
|
+
let match;
|
|
795
|
+
while ((match = routePattern.exec(content)) !== null) {
|
|
796
|
+
routes.push({
|
|
797
|
+
method: match[2].toUpperCase(),
|
|
798
|
+
path: match[1],
|
|
799
|
+
file: rel,
|
|
800
|
+
tags: detectTags(content),
|
|
801
|
+
framework: "axum",
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
return routes;
|
|
807
|
+
}
|
|
808
|
+
// --- Raw HTTP (Node.js http.createServer, Deno, Bun.serve) ---
|
|
809
|
+
async function detectRawHttpRoutes(files, project) {
|
|
810
|
+
const tsFiles = files.filter((f) => f.match(/\.(ts|js|mjs|cjs)$/));
|
|
811
|
+
const routes = [];
|
|
812
|
+
const globalSeen = new Set();
|
|
813
|
+
for (const file of tsFiles) {
|
|
814
|
+
const content = await readFileSafe(file);
|
|
815
|
+
// Only scan files that handle HTTP requests
|
|
816
|
+
if (!content.match(/(?:createServer|http\.|req\.|request\.|url|pathname|Bun\.serve|Deno\.serve)/))
|
|
817
|
+
continue;
|
|
818
|
+
const rel = relative(project.root, file);
|
|
819
|
+
const patterns = [
|
|
820
|
+
// Direct comparison: url === "/path" or pathname === "/path"
|
|
821
|
+
/(?:url|pathname|parsedUrl\.pathname)\s*===?\s*['"`](\/[a-zA-Z0-9/_:.\-]+)['"`]/g,
|
|
822
|
+
// startsWith: url.startsWith("/api")
|
|
823
|
+
/(?:url|pathname)\s*\.startsWith\s*\(\s*['"`](\/[a-zA-Z0-9/_:.\-]+)['"`]\s*\)/g,
|
|
824
|
+
// Switch case: case "/path":
|
|
825
|
+
/case\s+['"`](\/[a-zA-Z0-9/_:.\-]+)['"`]\s*:/g,
|
|
826
|
+
];
|
|
827
|
+
const fileTags = detectTags(content);
|
|
828
|
+
for (const pattern of patterns) {
|
|
829
|
+
let match;
|
|
830
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
831
|
+
const path = match[1];
|
|
832
|
+
// Skip paths that are clearly not routes
|
|
833
|
+
if (path.includes("\\") || path.length > 100 || path.includes(".."))
|
|
834
|
+
continue;
|
|
835
|
+
// Skip file extensions
|
|
836
|
+
if (path.match(/\.\w{2,4}$/))
|
|
837
|
+
continue;
|
|
838
|
+
const key = `${rel}:${path}`;
|
|
839
|
+
if (globalSeen.has(key))
|
|
840
|
+
continue;
|
|
841
|
+
globalSeen.add(key);
|
|
842
|
+
// Try to detect method from surrounding context (within 300 chars)
|
|
843
|
+
const surroundingStart = Math.max(0, match.index - 300);
|
|
844
|
+
const surroundingEnd = Math.min(content.length, match.index + 300);
|
|
845
|
+
const surrounding = content.substring(surroundingStart, surroundingEnd);
|
|
846
|
+
let method = "ALL";
|
|
847
|
+
const methodMatch = surrounding.match(/method\s*===?\s*['"`](GET|POST|PUT|PATCH|DELETE)['"`]/i);
|
|
848
|
+
if (methodMatch) {
|
|
849
|
+
method = methodMatch[1].toUpperCase();
|
|
850
|
+
}
|
|
851
|
+
routes.push({
|
|
852
|
+
method,
|
|
853
|
+
path,
|
|
854
|
+
file: rel,
|
|
855
|
+
tags: fileTags,
|
|
856
|
+
framework: "raw-http",
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
return routes;
|
|
862
|
+
}
|
package/dist/formatter.js
CHANGED
|
@@ -253,14 +253,27 @@ function formatCombined(result, sections) {
|
|
|
253
253
|
}
|
|
254
254
|
function filterNotableDeps(deps) {
|
|
255
255
|
const notable = new Set([
|
|
256
|
+
// Frameworks
|
|
256
257
|
"next", "react", "vue", "svelte", "hono", "express", "fastify", "koa",
|
|
257
|
-
"
|
|
258
|
-
"
|
|
259
|
-
|
|
260
|
-
"
|
|
261
|
-
"
|
|
262
|
-
|
|
263
|
-
"
|
|
258
|
+
"@nestjs/core", "@nestjs/common", "elysia", "@adonisjs/core",
|
|
259
|
+
"@sveltejs/kit", "@remix-run/node", "@remix-run/react", "nuxt",
|
|
260
|
+
// ORMs & DB
|
|
261
|
+
"drizzle-orm", "prisma", "@prisma/client", "typeorm", "mongoose", "sequelize",
|
|
262
|
+
"pg", "mysql2", "better-sqlite3", "knex",
|
|
263
|
+
// Auth
|
|
264
|
+
"better-auth", "@clerk/nextjs", "next-auth", "lucia", "passport", "@auth/core",
|
|
265
|
+
// Payments
|
|
266
|
+
"stripe", "@polar-sh/sdk", "resend", "@lemonsqueezy/lemonsqueezy.js",
|
|
267
|
+
// Infrastructure
|
|
268
|
+
"bullmq", "redis", "ioredis", "tailwindcss",
|
|
269
|
+
// API
|
|
270
|
+
"zod", "@trpc/server", "graphql", "@apollo/server",
|
|
271
|
+
// AI
|
|
272
|
+
"@anthropic-ai/sdk", "openai", "ai", "langchain", "@google/generative-ai",
|
|
273
|
+
// Services
|
|
274
|
+
"supabase", "@supabase/supabase-js", "firebase", "@firebase/app",
|
|
275
|
+
// Testing/tools
|
|
276
|
+
"playwright", "puppeteer", "socket.io",
|
|
264
277
|
]);
|
|
265
278
|
return Object.entries(deps)
|
|
266
279
|
.filter(([name]) => notable.has(name))
|