botversion-sdk 1.0.6 → 1.1.6

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/scanner.js CHANGED
@@ -852,9 +852,8 @@ function scanConfigBasedRoutes(cwd) {
852
852
  const patterns = [];
853
853
  const seen = new Set();
854
854
 
855
- // Files to scan for route definitions
856
855
  const filesToCheck = [
857
- // React Router — common file names
856
+ // React Router
858
857
  path.join(cwd, "src", "App.jsx"),
859
858
  path.join(cwd, "src", "App.tsx"),
860
859
  path.join(cwd, "src", "App.js"),
@@ -874,8 +873,46 @@ function scanConfigBasedRoutes(cwd) {
874
873
  // Angular
875
874
  path.join(cwd, "src", "app", "app-routing.module.ts"),
876
875
  path.join(cwd, "src", "app", "app.routes.ts"),
876
+ // TanStack Router
877
+ path.join(cwd, "src", "routes.tsx"),
878
+ path.join(cwd, "src", "routeTree.gen.ts"),
879
+ path.join(cwd, "src", "router.tsx"),
877
880
  ];
878
881
 
882
+ // Also scan any file named *routes* or *router* anywhere in src/
883
+ const srcDir = path.join(cwd, "src");
884
+ if (fs.existsSync(srcDir)) {
885
+ function findRouteFiles(dir, depth) {
886
+ if (depth > 3) return;
887
+ let entries;
888
+ try {
889
+ entries = fs.readdirSync(dir);
890
+ } catch {
891
+ return;
892
+ }
893
+ for (const entry of entries) {
894
+ if (["node_modules", ".git", "dist", "build"].includes(entry)) continue;
895
+ const fullPath = path.join(dir, entry);
896
+ let stat;
897
+ try {
898
+ stat = fs.statSync(fullPath);
899
+ } catch {
900
+ continue;
901
+ }
902
+ if (stat.isDirectory()) {
903
+ findRouteFiles(fullPath, depth + 1);
904
+ } else if (
905
+ /\.(js|ts|jsx|tsx)$/.test(entry) &&
906
+ /route|router/i.test(entry) &&
907
+ !filesToCheck.includes(fullPath)
908
+ ) {
909
+ filesToCheck.push(fullPath);
910
+ }
911
+ }
912
+ }
913
+ findRouteFiles(srcDir, 0);
914
+ }
915
+
879
916
  for (const filePath of filesToCheck) {
880
917
  if (!fs.existsSync(filePath)) continue;
881
918
 
@@ -907,6 +944,32 @@ function scanConfigBasedRoutes(cwd) {
907
944
  for (const match of angularRouteMatches) {
908
945
  addConfigPattern(match[1], seen, patterns);
909
946
  }
947
+
948
+ // Pattern 4 — TanStack Router: createRoute({ path: '/dashboard/:id' })
949
+ const tanstackMatches = content.matchAll(
950
+ /createRoute\s*\(\s*\{[^}]*path\s*:\s*["']([^"']+)["']/g,
951
+ );
952
+ for (const match of tanstackMatches) {
953
+ addConfigPattern(match[1], seen, patterns);
954
+ }
955
+
956
+ // Pattern 5 — TanStack Router file-based: createFileRoute('/dashboard/$id')
957
+ const tanstackFileMatches = content.matchAll(
958
+ /createFileRoute\s*\(\s*["']([^"']+)["']/g,
959
+ );
960
+ for (const match of tanstackFileMatches) {
961
+ // Convert TanStack $param to :param
962
+ const normalized = match[1].replace(/\$([a-zA-Z_][a-zA-Z0-9_]*)/g, ":$1");
963
+ addConfigPattern(normalized, seen, patterns);
964
+ }
965
+
966
+ // Pattern 6 — Vue Router with children
967
+ const vueChildrenMatches = content.matchAll(
968
+ /children\s*:\s*\[[^\]]*path\s*:\s*["']([^"']+)["']/g,
969
+ );
970
+ for (const match of vueChildrenMatches) {
971
+ addConfigPattern(match[1], seen, patterns);
972
+ }
910
973
  }
911
974
 
912
975
  return patterns;
@@ -938,27 +1001,273 @@ function addConfigPattern(routePath, seen, patterns) {
938
1001
  patterns.push({ pattern: normalized, params: paramMap });
939
1002
  }
940
1003
 
1004
+ function findAllFrontendDirs(cwd) {
1005
+ const fs = require("fs");
1006
+ const path = require("path");
1007
+
1008
+ const FRONTEND_INDICATORS = [
1009
+ "next.config.js",
1010
+ "next.config.ts",
1011
+ "react-router.config.ts",
1012
+ "react-router.config.js",
1013
+ "vite.config.ts",
1014
+ "vite.config.js",
1015
+ "nuxt.config.ts",
1016
+ "nuxt.config.js",
1017
+ "svelte.config.js",
1018
+ "svelte.config.ts",
1019
+ "remix.config.js",
1020
+ "remix.config.ts",
1021
+ "angular.json",
1022
+ "astro.config.mjs",
1023
+ "astro.config.ts",
1024
+ "astro.config.js",
1025
+ "app.config.ts",
1026
+ "qwik.config.ts",
1027
+ ];
1028
+
1029
+ const SKIP_DIRS = new Set([
1030
+ "node_modules",
1031
+ ".git",
1032
+ "dist",
1033
+ "build",
1034
+ ".next",
1035
+ ".nuxt",
1036
+ "coverage",
1037
+ ]);
1038
+
1039
+ const found = [];
1040
+
1041
+ function isFrontendDir(dir) {
1042
+ // Check 1 — indicator files (most reliable)
1043
+ const hasIndicator = FRONTEND_INDICATORS.some(function (indicator) {
1044
+ try {
1045
+ return fs.existsSync(path.join(dir, indicator));
1046
+ } catch {
1047
+ return false;
1048
+ }
1049
+ });
1050
+ if (hasIndicator) return true;
1051
+
1052
+ // Check 2 — fallback: check package.json for frontend frameworks
1053
+ try {
1054
+ const pkgPath = path.join(dir, "package.json");
1055
+ if (fs.existsSync(pkgPath)) {
1056
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
1057
+ const deps = Object.assign(
1058
+ {},
1059
+ pkg.dependencies || {},
1060
+ pkg.devDependencies || {},
1061
+ );
1062
+ const frontendPackages = [
1063
+ "react",
1064
+ "vue",
1065
+ "angular",
1066
+ "@angular/core",
1067
+ "svelte",
1068
+ "solid-js",
1069
+ "preact",
1070
+ "nuxt",
1071
+ "@remix-run/react",
1072
+ "next",
1073
+ "@sveltejs/kit",
1074
+ "astro",
1075
+ "gatsby",
1076
+ "@solidjs/start",
1077
+ "@builder.io/qwik",
1078
+ "@builder.io/qwik-city",
1079
+ ];
1080
+ if (
1081
+ frontendPackages.some(function (p) {
1082
+ return deps[p];
1083
+ })
1084
+ ) {
1085
+ return true;
1086
+ }
1087
+ }
1088
+ } catch {
1089
+ // silent fail
1090
+ }
1091
+
1092
+ return false;
1093
+ }
1094
+
1095
+ // Always check cwd itself first
1096
+ if (isFrontendDir(cwd)) {
1097
+ found.push(cwd);
1098
+ }
1099
+
1100
+ // ── Step 1: Scan subfolders inside cwd ──────────────────────────────
1101
+ let cwdEntries;
1102
+ try {
1103
+ cwdEntries = fs.readdirSync(cwd);
1104
+ } catch {
1105
+ cwdEntries = [];
1106
+ }
1107
+
1108
+ for (const entry of cwdEntries) {
1109
+ if (SKIP_DIRS.has(entry)) continue;
1110
+
1111
+ const sub = path.join(cwd, entry);
1112
+ let stat;
1113
+ try {
1114
+ stat = fs.statSync(sub);
1115
+ } catch {
1116
+ continue;
1117
+ }
1118
+ if (!stat.isDirectory()) continue;
1119
+
1120
+ // Level 1 — direct subfolders of cwd
1121
+ if (isFrontendDir(sub) && !found.includes(sub)) {
1122
+ found.push(sub);
1123
+ }
1124
+
1125
+ // Level 2 — one level deeper (e.g. packages/apps/frontend)
1126
+ let subEntries;
1127
+ try {
1128
+ subEntries = fs.readdirSync(sub);
1129
+ } catch {
1130
+ continue;
1131
+ }
1132
+
1133
+ for (const subEntry of subEntries) {
1134
+ if (SKIP_DIRS.has(subEntry)) continue;
1135
+ const subSub = path.join(sub, subEntry);
1136
+ let subStat;
1137
+ try {
1138
+ subStat = fs.statSync(subSub);
1139
+ } catch {
1140
+ continue;
1141
+ }
1142
+ if (!subStat.isDirectory()) continue;
1143
+ if (isFrontendDir(subSub) && !found.includes(subSub)) {
1144
+ found.push(subSub);
1145
+ }
1146
+ }
1147
+ }
1148
+
1149
+ // ── Step 2: Walk up exactly 1 level to find sibling folders ─────────
1150
+ // Handles the case where SDK is installed in backend/ but
1151
+ // frontend/ is a sibling folder at the same level
1152
+ const parentDir = path.dirname(cwd);
1153
+ if (parentDir !== cwd) {
1154
+ const UNSAFE_PARENTS = new Set([
1155
+ "Desktop",
1156
+ "Documents",
1157
+ "Downloads",
1158
+ "Pictures",
1159
+ "Videos",
1160
+ "Music",
1161
+ "home",
1162
+ "users",
1163
+ "Users",
1164
+ "var",
1165
+ "www",
1166
+ "srv",
1167
+ "opt",
1168
+ "tmp",
1169
+ "workspace",
1170
+ "Workspace",
1171
+ "projects",
1172
+ "Projects",
1173
+ "code",
1174
+ "Code",
1175
+ "sites",
1176
+ "Sites",
1177
+ "dev",
1178
+ "Dev",
1179
+ "work",
1180
+ "Work",
1181
+ ]);
1182
+
1183
+ if (UNSAFE_PARENTS.has(path.basename(parentDir))) {
1184
+ if (found.length === 0) found.push(cwd);
1185
+ return found;
1186
+ }
1187
+ // Safety check — only scan siblings if the parent folder
1188
+ // looks like a project root (has package.json OR common project folders)
1189
+ // This prevents scanning unrelated projects on the Desktop
1190
+ const parentHasPackageJson = fs.existsSync(
1191
+ path.join(parentDir, "package.json"),
1192
+ );
1193
+ const parentHasCommonProjectFiles =
1194
+ fs.existsSync(path.join(parentDir, "docker-compose.yml")) ||
1195
+ fs.existsSync(path.join(parentDir, "docker-compose.yaml")) ||
1196
+ fs.existsSync(path.join(parentDir, ".env")) ||
1197
+ fs.existsSync(path.join(parentDir, "turbo.json")) ||
1198
+ fs.existsSync(path.join(parentDir, "pnpm-workspace.yaml"));
1199
+
1200
+ // If parent has no signs of being a project root, skip sibling scanning
1201
+ if (!parentHasPackageJson && !parentHasCommonProjectFiles) {
1202
+ // skip — parent is probably just a random folder like Desktop
1203
+ } else {
1204
+ let parentEntries;
1205
+ try {
1206
+ parentEntries = fs.readdirSync(parentDir);
1207
+ } catch {
1208
+ parentEntries = [];
1209
+ }
1210
+
1211
+ for (const entry of parentEntries) {
1212
+ if (SKIP_DIRS.has(entry)) continue;
1213
+
1214
+ const sibling = path.join(parentDir, entry);
1215
+ if (sibling === cwd) continue; // skip cwd itself
1216
+
1217
+ let stat;
1218
+ try {
1219
+ stat = fs.statSync(sibling);
1220
+ } catch {
1221
+ continue;
1222
+ }
1223
+ if (!stat.isDirectory()) continue;
1224
+
1225
+ // Only add if it actually looks like a frontend project
1226
+ if (isFrontendDir(sibling) && !found.includes(sibling)) {
1227
+ found.push(sibling);
1228
+ }
1229
+
1230
+ // Also check one level inside the sibling
1231
+ let siblingEntries;
1232
+ try {
1233
+ siblingEntries = fs.readdirSync(sibling);
1234
+ } catch {
1235
+ continue;
1236
+ }
1237
+
1238
+ for (const siblingEntry of siblingEntries) {
1239
+ if (SKIP_DIRS.has(siblingEntry)) continue;
1240
+ const siblingChild = path.join(sibling, siblingEntry);
1241
+ let siblingChildStat;
1242
+ try {
1243
+ siblingChildStat = fs.statSync(siblingChild);
1244
+ } catch {
1245
+ continue;
1246
+ }
1247
+ if (!siblingChildStat.isDirectory()) continue;
1248
+ if (isFrontendDir(siblingChild) && !found.includes(siblingChild)) {
1249
+ found.push(siblingChild);
1250
+ }
1251
+ }
1252
+ }
1253
+ }
1254
+ }
1255
+
1256
+ // If nothing found, fall back to cwd
1257
+ if (found.length === 0) {
1258
+ found.push(cwd);
1259
+ }
1260
+
1261
+ return found;
1262
+ }
1263
+
941
1264
  function scanFrontendRoutes(cwd) {
942
1265
  const fs = require("fs");
943
1266
  const path = require("path");
944
1267
  const patterns = [];
945
1268
  const seen = new Set();
946
1269
 
947
- const dirsToScan = [
948
- // Next.js Pages Router
949
- path.join(cwd, "pages"),
950
- path.join(cwd, "src", "pages"),
951
- // Next.js App Router
952
- path.join(cwd, "app"),
953
- path.join(cwd, "src", "app"),
954
- // Nuxt.js
955
- path.join(cwd, "pages"),
956
- // SvelteKit
957
- path.join(cwd, "src", "routes"),
958
- path.join(cwd, "routes"),
959
- // Remix
960
- path.join(cwd, "app", "routes"),
961
- ];
1270
+ const candidateDirs = findAllFrontendDirs(cwd);
962
1271
 
963
1272
  function walkDir(dir, routeSegments) {
964
1273
  if (!fs.existsSync(dir)) return;
@@ -979,10 +1288,9 @@ function scanFrontendRoutes(cwd) {
979
1288
  }
980
1289
 
981
1290
  if (stat.isDirectory()) {
982
- if (file === "api") return; // skip API routes
983
- if (file.startsWith("_")) return; // skip Next.js internals
1291
+ if (file === "api") return;
1292
+ if (file.startsWith("_")) return;
984
1293
  if (/^\(.*\)$/.test(file)) {
985
- // route groups like (marketing) — don't add to URL
986
1294
  walkDir(fullPath, routeSegments);
987
1295
  return;
988
1296
  }
@@ -992,12 +1300,11 @@ function scanFrontendRoutes(cwd) {
992
1300
  return;
993
1301
  }
994
1302
 
995
- if (!/\.(js|ts|jsx|tsx|vue|svelte)$/.test(file)) return;
1303
+ if (!/\.(js|ts|jsx|tsx|vue|svelte|astro)$/.test(file)) return;
996
1304
  if (file.startsWith("_")) return;
997
1305
 
998
- const routeName = file.replace(/\.(js|ts|jsx|tsx|vue|svelte)$/, "");
1306
+ const routeName = file.replace(/\.(js|ts|jsx|tsx|vue|svelte|astro)$/, "");
999
1307
 
1000
- // Skip non-page files
1001
1308
  if (
1002
1309
  ["layout", "loading", "error", "template", "not-found"].includes(
1003
1310
  routeName,
@@ -1005,16 +1312,13 @@ function scanFrontendRoutes(cwd) {
1005
1312
  )
1006
1313
  return;
1007
1314
 
1008
- // SvelteKit uses +page.svelte — skip +layout, +error etc.
1009
1315
  if (routeName.startsWith("+") && routeName !== "+page") return;
1010
1316
 
1011
- // Remix uses dots as path separators — e.g. $projectId.dashboard.tsx → /:projectId/dashboard
1012
1317
  const isRemixRoute =
1013
1318
  routeName.includes(".") && !routeName.startsWith("+");
1014
1319
  let finalSegments;
1015
1320
 
1016
1321
  if (isRemixRoute) {
1017
- // Split by dots and convert each segment
1018
1322
  const remixSegments = routeName
1019
1323
  .split(".")
1020
1324
  .map((s) => convertNextJsSegment(s) || s);
@@ -1036,32 +1340,969 @@ function scanFrontendRoutes(cwd) {
1036
1340
  seen.add(pattern);
1037
1341
 
1038
1342
  const paramMap = extractParamPositions(finalSegments);
1039
- if (Object.keys(paramMap).length === 0) return; // no dynamic params, skip static routes
1343
+ if (Object.keys(paramMap).length === 0) return;
1040
1344
 
1041
1345
  patterns.push({ pattern, params: paramMap });
1042
1346
  });
1043
1347
  }
1044
1348
 
1045
- dirsToScan.forEach(function (dir) {
1046
- if (fs.existsSync(dir)) walkDir(dir, []);
1047
- });
1349
+ for (const candidate of candidateDirs) {
1350
+ const dirsToScan = [
1351
+ // Next.js
1352
+ path.join(candidate, "pages"),
1353
+ path.join(candidate, "src", "pages"),
1354
+ path.join(candidate, "app"),
1355
+ path.join(candidate, "src", "app"),
1356
+ // React Router / Remix
1357
+ path.join(candidate, "src", "routes"),
1358
+ path.join(candidate, "routes"),
1359
+ path.join(candidate, "app", "routes"),
1360
+ // SvelteKit
1361
+ path.join(candidate, "src", "routes"),
1362
+ // Nuxt
1363
+ path.join(candidate, "pages"),
1364
+ path.join(candidate, "src", "pages"),
1365
+ // Astro
1366
+ path.join(candidate, "src", "pages"),
1367
+ ];
1048
1368
 
1049
- // Also scan config-based frameworks (React Router, Vue Router, Angular)
1050
- const configPatterns = scanConfigBasedRoutes(cwd);
1051
- configPatterns.forEach(function (p) {
1052
- if (!seen.has(p.pattern)) {
1053
- seen.add(p.pattern);
1054
- patterns.push(p);
1055
- }
1056
- });
1369
+ dirsToScan.forEach(function (dir) {
1370
+ if (fs.existsSync(dir)) {
1371
+ walkDir(dir, []);
1372
+ }
1373
+ });
1374
+
1375
+ const configPatterns = scanConfigBasedRoutes(candidate);
1376
+ configPatterns.forEach(function (p) {
1377
+ if (!seen.has(p.pattern)) {
1378
+ seen.add(p.pattern);
1379
+ patterns.push(p);
1380
+ }
1381
+ });
1382
+ }
1057
1383
 
1058
1384
  return patterns;
1059
1385
  }
1060
1386
 
1387
+ /**
1388
+ * Scans Fastify routes from the codebase statically
1389
+ * Handles:
1390
+ * - fastify.get('/path', handler)
1391
+ * - fastify.post('/path', handler)
1392
+ * - fastify.route({ method: 'GET', url: '/path' })
1393
+ * - fastify.register(plugin, { prefix: '/api' })
1394
+ */
1395
+ function scanFastifyRoutes(cwd) {
1396
+ const fs = require("fs");
1397
+ const path = require("path");
1398
+ const endpoints = [];
1399
+ const seen = new Set();
1400
+
1401
+ const SKIP_DIRS = [
1402
+ "node_modules",
1403
+ ".git",
1404
+ ".next",
1405
+ "dist",
1406
+ "build",
1407
+ ".cache",
1408
+ "coverage",
1409
+ "out",
1410
+ ];
1411
+
1412
+ // Build body map from all files upfront
1413
+ const bodyMap = cwd ? buildBodyMap(cwd) : {};
1414
+
1415
+ function walk(dir, depth) {
1416
+ if (depth > 4) return;
1417
+ let entries;
1418
+ try {
1419
+ entries = fs.readdirSync(dir);
1420
+ } catch {
1421
+ return;
1422
+ }
1423
+
1424
+ for (const entry of entries) {
1425
+ if (SKIP_DIRS.includes(entry)) continue;
1426
+ const fullPath = path.join(dir, entry);
1427
+ let stat;
1428
+ try {
1429
+ stat = fs.statSync(fullPath);
1430
+ } catch {
1431
+ continue;
1432
+ }
1433
+
1434
+ if (stat.isDirectory()) {
1435
+ walk(fullPath, depth + 1);
1436
+ continue;
1437
+ }
1438
+
1439
+ if (!/\.(js|ts)$/.test(entry)) continue;
1440
+
1441
+ let content;
1442
+ try {
1443
+ content = fs.readFileSync(fullPath, "utf8");
1444
+ } catch {
1445
+ continue;
1446
+ }
1447
+
1448
+ // Only process files that look like Fastify route files
1449
+ const isFastifyFile =
1450
+ content.includes("fastify") ||
1451
+ content.includes("Fastify") ||
1452
+ /\.(get|post|put|delete|patch|route)\s*\(/.test(content);
1453
+
1454
+ if (!isFastifyFile) continue;
1455
+
1456
+ // Pattern 1 — fastify.get('/path', handler)
1457
+ // Pattern 2 — fastify.post('/path', handler)
1458
+ const shorthandPattern =
1459
+ /(?:fastify|app|server|router)\.(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
1460
+
1461
+ let match;
1462
+ while ((match = shorthandPattern.exec(content)) !== null) {
1463
+ const method = match[1].toUpperCase();
1464
+ const routePath = match[2];
1465
+ if (routePath.includes("*")) continue;
1466
+
1467
+ const key = method + ":" + routePath;
1468
+ if (seen.has(key)) continue;
1469
+ seen.add(key);
1470
+
1471
+ const needsBody = ["POST", "PUT", "PATCH"].includes(method);
1472
+ const bodyFields = needsBody
1473
+ ? extractBodyFieldsFromFile(content)
1474
+ : null;
1475
+ const routeParamMap = buildRouteParamMap(routePath, []);
1476
+
1477
+ endpoints.push({
1478
+ method,
1479
+ path: routePath,
1480
+ description: "",
1481
+ requestBody: bodyFields,
1482
+ routeParamMap,
1483
+ detectedBy: "static-scan-fastify",
1484
+ });
1485
+ }
1486
+
1487
+ // Pattern 3 — fastify.route({ method: 'GET', url: '/path' })
1488
+ const routeObjectPattern =
1489
+ /\.route\s*\(\s*\{[^}]*method\s*:\s*['"`]([^'"`]+)['"`][^}]*url\s*:\s*['"`]([^'"`]+)['"`]/gi;
1490
+
1491
+ while ((match = routeObjectPattern.exec(content)) !== null) {
1492
+ const method = match[1].toUpperCase();
1493
+ const routePath = match[2];
1494
+ if (routePath.includes("*")) continue;
1495
+
1496
+ const key = method + ":" + routePath;
1497
+ if (seen.has(key)) continue;
1498
+ seen.add(key);
1499
+
1500
+ const needsBody = ["POST", "PUT", "PATCH"].includes(method);
1501
+ const bodyFields = needsBody
1502
+ ? extractBodyFieldsFromFile(content)
1503
+ : null;
1504
+ const routeParamMap = buildRouteParamMap(routePath, []);
1505
+
1506
+ endpoints.push({
1507
+ method,
1508
+ path: routePath,
1509
+ description: "",
1510
+ requestBody: bodyFields,
1511
+ routeParamMap,
1512
+ detectedBy: "static-scan-fastify",
1513
+ });
1514
+ }
1515
+
1516
+ // Pattern 4 — fastify.route({ method: ['GET', 'POST'], url: '/path' })
1517
+ // handles array of methods
1518
+ const routeArrayPattern =
1519
+ /\.route\s*\(\s*\{[^}]*method\s*:\s*\[([^\]]+)\][^}]*url\s*:\s*['"`]([^'"`]+)['"`]/gi;
1520
+
1521
+ while ((match = routeArrayPattern.exec(content)) !== null) {
1522
+ const methodsRaw = match[1];
1523
+ const routePath = match[2];
1524
+ if (routePath.includes("*")) continue;
1525
+
1526
+ const methods = methodsRaw
1527
+ .split(",")
1528
+ .map((m) => m.trim().replace(/['"`]/g, "").toUpperCase())
1529
+ .filter(Boolean);
1530
+
1531
+ for (const method of methods) {
1532
+ const key = method + ":" + routePath;
1533
+ if (seen.has(key)) continue;
1534
+ seen.add(key);
1535
+
1536
+ const needsBody = ["POST", "PUT", "PATCH"].includes(method);
1537
+ const bodyFields = needsBody
1538
+ ? extractBodyFieldsFromFile(content)
1539
+ : null;
1540
+ const routeParamMap = buildRouteParamMap(routePath, []);
1541
+
1542
+ endpoints.push({
1543
+ method,
1544
+ path: routePath,
1545
+ description: "",
1546
+ requestBody: bodyFields,
1547
+ routeParamMap,
1548
+ detectedBy: "static-scan-fastify",
1549
+ });
1550
+ }
1551
+ }
1552
+ }
1553
+ }
1554
+
1555
+ walk(cwd, 0);
1556
+ return endpoints;
1557
+ }
1558
+
1559
+ /**
1560
+ * Scans NestJS routes from the codebase statically
1561
+ * Handles:
1562
+ * - @Controller('/users')
1563
+ * - @Get('/path')
1564
+ * - @Post('/path')
1565
+ * - @Put('/path')
1566
+ * - @Delete('/path')
1567
+ * - @Patch('/path')
1568
+ */
1569
+ function scanNestJsRoutes(cwd) {
1570
+ const fs = require("fs");
1571
+ const path = require("path");
1572
+ const endpoints = [];
1573
+ const seen = new Set();
1574
+
1575
+ const SKIP_DIRS = [
1576
+ "node_modules",
1577
+ ".git",
1578
+ ".next",
1579
+ "dist",
1580
+ "build",
1581
+ ".cache",
1582
+ "coverage",
1583
+ "out",
1584
+ ];
1585
+
1586
+ function walk(dir, depth) {
1587
+ if (depth > 4) return;
1588
+ let entries;
1589
+ try {
1590
+ entries = fs.readdirSync(dir);
1591
+ } catch {
1592
+ return;
1593
+ }
1594
+
1595
+ for (const entry of entries) {
1596
+ if (SKIP_DIRS.includes(entry)) continue;
1597
+ const fullPath = path.join(dir, entry);
1598
+ let stat;
1599
+ try {
1600
+ stat = fs.statSync(fullPath);
1601
+ } catch {
1602
+ continue;
1603
+ }
1604
+
1605
+ if (stat.isDirectory()) {
1606
+ walk(fullPath, depth + 1);
1607
+ continue;
1608
+ }
1609
+
1610
+ if (!/\.(ts|js)$/.test(entry)) continue;
1611
+
1612
+ let content;
1613
+ try {
1614
+ content = fs.readFileSync(fullPath, "utf8");
1615
+ } catch {
1616
+ continue;
1617
+ }
1618
+
1619
+ // Only process files that look like NestJS controller files
1620
+ const isNestFile =
1621
+ content.includes("@Controller") ||
1622
+ content.includes("@Get(") ||
1623
+ content.includes("@Post(") ||
1624
+ content.includes("@Put(") ||
1625
+ content.includes("@Delete(") ||
1626
+ content.includes("@Patch(");
1627
+
1628
+ if (!isNestFile) continue;
1629
+
1630
+ // Extract controller prefix — @Controller('/users') or @Controller('users')
1631
+ let controllerPrefix = "";
1632
+ const controllerMatch = content.match(
1633
+ /@Controller\s*\(\s*['"`]([^'"`]*)['"`]\s*\)/,
1634
+ );
1635
+ if (controllerMatch) {
1636
+ controllerPrefix = controllerMatch[1].startsWith("/")
1637
+ ? controllerMatch[1]
1638
+ : "/" + controllerMatch[1];
1639
+ }
1640
+
1641
+ // Extract all method decorators
1642
+ const methodPattern =
1643
+ /@(Get|Post|Put|Delete|Patch)\s*\(\s*['"`]?([^'"`\s\)]*?)['"`]?\s*\)/gi;
1644
+
1645
+ let match;
1646
+ while ((match = methodPattern.exec(content)) !== null) {
1647
+ const method = match[1].toUpperCase();
1648
+ const methodPath = match[2] || "";
1649
+
1650
+ const routeFull =
1651
+ controllerPrefix +
1652
+ (methodPath
1653
+ ? methodPath.startsWith("/")
1654
+ ? methodPath
1655
+ : "/" + methodPath
1656
+ : "");
1657
+
1658
+ const normalizedPath = routeFull.replace(
1659
+ /:([a-zA-Z_][a-zA-Z0-9_]*)/g,
1660
+ ":$1",
1661
+ );
1662
+
1663
+ const key = method + ":" + normalizedPath;
1664
+ if (seen.has(key)) continue;
1665
+ seen.add(key);
1666
+
1667
+ const needsBody = ["POST", "PUT", "PATCH"].includes(method);
1668
+ const bodyFields = needsBody
1669
+ ? extractBodyFieldsFromFile(content)
1670
+ : null;
1671
+ const routeParamMap = buildRouteParamMap(normalizedPath, []);
1672
+
1673
+ endpoints.push({
1674
+ method,
1675
+ path: normalizedPath,
1676
+ description: "",
1677
+ requestBody: bodyFields,
1678
+ routeParamMap,
1679
+ detectedBy: "static-scan-nestjs",
1680
+ });
1681
+ }
1682
+ }
1683
+ }
1684
+
1685
+ walk(cwd, 0);
1686
+ return endpoints;
1687
+ }
1688
+
1689
+ /**
1690
+ * Scans Koa routes from the codebase statically
1691
+ * Handles:
1692
+ * - router.get('/path', handler)
1693
+ * - router.post('/path', handler)
1694
+ * - router.put('/path', handler)
1695
+ * - router.delete('/path', handler)
1696
+ * - router.patch('/path', handler)
1697
+ */
1698
+ function scanKoaRoutes(cwd) {
1699
+ const fs = require("fs");
1700
+ const path = require("path");
1701
+ const endpoints = [];
1702
+ const seen = new Set();
1703
+
1704
+ const SKIP_DIRS = [
1705
+ "node_modules",
1706
+ ".git",
1707
+ ".next",
1708
+ "dist",
1709
+ "build",
1710
+ ".cache",
1711
+ "coverage",
1712
+ "out",
1713
+ ];
1714
+
1715
+ function walk(dir, depth) {
1716
+ if (depth > 4) return;
1717
+ let entries;
1718
+ try {
1719
+ entries = fs.readdirSync(dir);
1720
+ } catch {
1721
+ return;
1722
+ }
1723
+
1724
+ for (const entry of entries) {
1725
+ if (SKIP_DIRS.includes(entry)) continue;
1726
+ const fullPath = path.join(dir, entry);
1727
+ let stat;
1728
+ try {
1729
+ stat = fs.statSync(fullPath);
1730
+ } catch {
1731
+ continue;
1732
+ }
1733
+
1734
+ if (stat.isDirectory()) {
1735
+ walk(fullPath, depth + 1);
1736
+ continue;
1737
+ }
1738
+
1739
+ if (!/\.(js|ts)$/.test(entry)) continue;
1740
+
1741
+ let content;
1742
+ try {
1743
+ content = fs.readFileSync(fullPath, "utf8");
1744
+ } catch {
1745
+ continue;
1746
+ }
1747
+
1748
+ // Only process files that look like Koa route files
1749
+ const isKoaFile =
1750
+ content.includes("koa-router") ||
1751
+ content.includes("@koa/router") ||
1752
+ content.includes("new Router()") ||
1753
+ /router\.(get|post|put|delete|patch)\s*\(/.test(content);
1754
+
1755
+ if (!isKoaFile) continue;
1756
+
1757
+ const routePattern =
1758
+ /router\.(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
1759
+
1760
+ let match;
1761
+ while ((match = routePattern.exec(content)) !== null) {
1762
+ const method = match[1].toUpperCase();
1763
+ const routePath = match[2];
1764
+ if (routePath.includes("*")) continue;
1765
+
1766
+ const key = method + ":" + routePath;
1767
+ if (seen.has(key)) continue;
1768
+ seen.add(key);
1769
+
1770
+ const needsBody = ["POST", "PUT", "PATCH"].includes(method);
1771
+ const bodyFields = needsBody
1772
+ ? extractBodyFieldsFromFile(content)
1773
+ : null;
1774
+ const routeParamMap = buildRouteParamMap(routePath, []);
1775
+
1776
+ endpoints.push({
1777
+ method,
1778
+ path: routePath,
1779
+ description: "",
1780
+ requestBody: bodyFields,
1781
+ routeParamMap,
1782
+ detectedBy: "static-scan-koa",
1783
+ });
1784
+ }
1785
+ }
1786
+ }
1787
+
1788
+ walk(cwd, 0);
1789
+ return endpoints;
1790
+ }
1791
+
1792
+ /**
1793
+ * Scans Hapi routes from the codebase statically
1794
+ * Handles:
1795
+ * - server.route({ method: 'GET', path: '/path', handler })
1796
+ * - server.route([{ method: 'POST', path: '/path', handler }])
1797
+ */
1798
+ function scanHapiRoutes(cwd) {
1799
+ const fs = require("fs");
1800
+ const path = require("path");
1801
+ const endpoints = [];
1802
+ const seen = new Set();
1803
+
1804
+ const SKIP_DIRS = [
1805
+ "node_modules",
1806
+ ".git",
1807
+ ".next",
1808
+ "dist",
1809
+ "build",
1810
+ ".cache",
1811
+ "coverage",
1812
+ "out",
1813
+ ];
1814
+
1815
+ function walk(dir, depth) {
1816
+ if (depth > 4) return;
1817
+ let entries;
1818
+ try {
1819
+ entries = fs.readdirSync(dir);
1820
+ } catch {
1821
+ return;
1822
+ }
1823
+
1824
+ for (const entry of entries) {
1825
+ if (SKIP_DIRS.includes(entry)) continue;
1826
+ const fullPath = path.join(dir, entry);
1827
+ let stat;
1828
+ try {
1829
+ stat = fs.statSync(fullPath);
1830
+ } catch {
1831
+ continue;
1832
+ }
1833
+
1834
+ if (stat.isDirectory()) {
1835
+ walk(fullPath, depth + 1);
1836
+ continue;
1837
+ }
1838
+
1839
+ if (!/\.(js|ts)$/.test(entry)) continue;
1840
+
1841
+ let content;
1842
+ try {
1843
+ content = fs.readFileSync(fullPath, "utf8");
1844
+ } catch {
1845
+ continue;
1846
+ }
1847
+
1848
+ const isHapiFile =
1849
+ content.includes("@hapi/hapi") ||
1850
+ content.includes("require('hapi')") ||
1851
+ content.includes('require("hapi")') ||
1852
+ content.includes("server.route(");
1853
+
1854
+ if (!isHapiFile) continue;
1855
+
1856
+ // Pattern — method and path can be in any order inside the object
1857
+ const routePattern =
1858
+ /server\.route\s*\(\s*\{[^}]*method\s*:\s*['"`]([^'"`]+)['"`][^}]*path\s*:\s*['"`]([^'"`]+)['"`]/gi;
1859
+
1860
+ let match;
1861
+ while ((match = routePattern.exec(content)) !== null) {
1862
+ const method = match[1].toUpperCase();
1863
+ const routePath = match[2];
1864
+ if (routePath.includes("*")) continue;
1865
+
1866
+ const key = method + ":" + routePath;
1867
+ if (seen.has(key)) continue;
1868
+ seen.add(key);
1869
+
1870
+ const needsBody = ["POST", "PUT", "PATCH"].includes(method);
1871
+ const bodyFields = needsBody
1872
+ ? extractBodyFieldsFromFile(content)
1873
+ : null;
1874
+ const routeParamMap = buildRouteParamMap(routePath, []);
1875
+
1876
+ endpoints.push({
1877
+ method,
1878
+ path: routePath,
1879
+ description: "",
1880
+ requestBody: bodyFields,
1881
+ routeParamMap,
1882
+ detectedBy: "static-scan-hapi",
1883
+ });
1884
+ }
1885
+ }
1886
+ }
1887
+
1888
+ walk(cwd, 0);
1889
+ return endpoints;
1890
+ }
1891
+
1892
+ /**
1893
+ * Scans AdonisJS routes from the codebase statically
1894
+ * Handles:
1895
+ * - Route.get('/path', handler)
1896
+ * - Route.post('/path', handler)
1897
+ * - router.get('/path', handler)
1898
+ * - router.post('/path', handler)
1899
+ */
1900
+ function scanAdonisRoutes(cwd) {
1901
+ const fs = require("fs");
1902
+ const path = require("path");
1903
+ const endpoints = [];
1904
+ const seen = new Set();
1905
+
1906
+ const SKIP_DIRS = [
1907
+ "node_modules",
1908
+ ".git",
1909
+ ".next",
1910
+ "dist",
1911
+ "build",
1912
+ ".cache",
1913
+ "coverage",
1914
+ "out",
1915
+ ];
1916
+
1917
+ function walk(dir, depth) {
1918
+ if (depth > 4) return;
1919
+ let entries;
1920
+ try {
1921
+ entries = fs.readdirSync(dir);
1922
+ } catch {
1923
+ return;
1924
+ }
1925
+
1926
+ for (const entry of entries) {
1927
+ if (SKIP_DIRS.includes(entry)) continue;
1928
+ const fullPath = path.join(dir, entry);
1929
+ let stat;
1930
+ try {
1931
+ stat = fs.statSync(fullPath);
1932
+ } catch {
1933
+ continue;
1934
+ }
1935
+
1936
+ if (stat.isDirectory()) {
1937
+ walk(fullPath, depth + 1);
1938
+ continue;
1939
+ }
1940
+
1941
+ if (!/\.(js|ts)$/.test(entry)) continue;
1942
+
1943
+ let content;
1944
+ try {
1945
+ content = fs.readFileSync(fullPath, "utf8");
1946
+ } catch {
1947
+ continue;
1948
+ }
1949
+
1950
+ const isAdonisFile =
1951
+ content.includes("@adonisjs") ||
1952
+ content.includes("Route.get") ||
1953
+ content.includes("Route.post") ||
1954
+ /(?:Route|router)\.(get|post|put|delete|patch)\s*\(/.test(content);
1955
+
1956
+ if (!isAdonisFile) continue;
1957
+
1958
+ const routePattern =
1959
+ /(?:Route|router)\.(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
1960
+
1961
+ let match;
1962
+ while ((match = routePattern.exec(content)) !== null) {
1963
+ const method = match[1].toUpperCase();
1964
+ const routePath = match[2];
1965
+ if (routePath.includes("*")) continue;
1966
+
1967
+ const key = method + ":" + routePath;
1968
+ if (seen.has(key)) continue;
1969
+ seen.add(key);
1970
+
1971
+ const needsBody = ["POST", "PUT", "PATCH"].includes(method);
1972
+ const bodyFields = needsBody
1973
+ ? extractBodyFieldsFromFile(content)
1974
+ : null;
1975
+ const routeParamMap = buildRouteParamMap(routePath, []);
1976
+
1977
+ endpoints.push({
1978
+ method,
1979
+ path: routePath,
1980
+ description: "",
1981
+ requestBody: bodyFields,
1982
+ routeParamMap,
1983
+ detectedBy: "static-scan-adonis",
1984
+ });
1985
+ }
1986
+ }
1987
+ }
1988
+
1989
+ walk(cwd, 0);
1990
+ return endpoints;
1991
+ }
1992
+
1993
+ /**
1994
+ * Scans Nuxt server API routes
1995
+ * Handles file-based routing in server/api/ folder
1996
+ * e.g. server/api/users.get.ts → GET /api/users
1997
+ * e.g. server/api/users/[id].delete.ts → DELETE /api/users/:id
1998
+ */
1999
+ function scanNuxtServerRoutes(cwd) {
2000
+ const fs = require("fs");
2001
+ const path = require("path");
2002
+ const endpoints = [];
2003
+ const seen = new Set();
2004
+
2005
+ const possibleDirs = [
2006
+ path.join(cwd, "server", "api"),
2007
+ path.join(cwd, "src", "server", "api"),
2008
+ ];
2009
+
2010
+ function walkDir(dir, routePath) {
2011
+ if (!fs.existsSync(dir)) return;
2012
+ let entries;
2013
+ try {
2014
+ entries = fs.readdirSync(dir);
2015
+ } catch {
2016
+ return;
2017
+ }
2018
+
2019
+ entries.forEach(function (file) {
2020
+ const fullPath = path.join(dir, file);
2021
+ let stat;
2022
+ try {
2023
+ stat = fs.statSync(fullPath);
2024
+ } catch {
2025
+ return;
2026
+ }
2027
+
2028
+ if (stat.isDirectory()) {
2029
+ const segment = file.replace(/\[([^\]]+)\]/g, ":$1");
2030
+ walkDir(fullPath, routePath + "/" + segment);
2031
+ return;
2032
+ }
2033
+
2034
+ if (!/\.(js|ts)$/.test(file)) return;
2035
+
2036
+ // Nuxt convention: users.get.ts or users.post.ts or just users.ts
2037
+ // Strip extension first
2038
+ let fileName = file.replace(/\.(js|ts)$/, "");
2039
+
2040
+ // Check if method is embedded in filename like users.get or [id].delete
2041
+ const methodMatch = fileName.match(/\.?(get|post|put|delete|patch)$/i);
2042
+ let method = null;
2043
+ if (methodMatch) {
2044
+ method = methodMatch[1].toUpperCase();
2045
+ fileName = fileName.replace(/\.?(get|post|put|delete|patch)$/i, "");
2046
+ }
2047
+
2048
+ // Convert [id] to :id
2049
+ fileName = fileName.replace(/\[([^\]]+)\]/g, ":$1");
2050
+
2051
+ const finalPath =
2052
+ fileName === "index" ? routePath : routePath + "/" + fileName;
2053
+
2054
+ const methods = method ? [method] : ["GET"];
2055
+
2056
+ methods.forEach(function (m) {
2057
+ const key = m + ":" + finalPath;
2058
+ if (seen.has(key)) return;
2059
+ seen.add(key);
2060
+
2061
+ const routeParamMap = buildRouteParamMap(finalPath, []);
2062
+
2063
+ endpoints.push({
2064
+ method: m,
2065
+ path: finalPath,
2066
+ description: "",
2067
+ requestBody: null,
2068
+ routeParamMap,
2069
+ detectedBy: "static-scan-nuxt",
2070
+ });
2071
+ });
2072
+ });
2073
+ }
2074
+
2075
+ for (const dir of possibleDirs) {
2076
+ walkDir(dir, "/api");
2077
+ }
2078
+
2079
+ return endpoints;
2080
+ }
2081
+
2082
+ /**
2083
+ * Scans SvelteKit server routes
2084
+ * Handles +server.js / +server.ts files in src/routes/
2085
+ * e.g. src/routes/api/users/+server.ts with export function GET() → GET /api/users
2086
+ */
2087
+ function scanSvelteKitServerRoutes(cwd) {
2088
+ const fs = require("fs");
2089
+ const path = require("path");
2090
+ const endpoints = [];
2091
+ const seen = new Set();
2092
+
2093
+ const possibleDirs = [
2094
+ path.join(cwd, "src", "routes"),
2095
+ path.join(cwd, "routes"),
2096
+ ];
2097
+
2098
+ function walkDir(dir, routePath) {
2099
+ if (!fs.existsSync(dir)) return;
2100
+ let entries;
2101
+ try {
2102
+ entries = fs.readdirSync(dir);
2103
+ } catch {
2104
+ return;
2105
+ }
2106
+
2107
+ entries.forEach(function (file) {
2108
+ const fullPath = path.join(dir, file);
2109
+ let stat;
2110
+ try {
2111
+ stat = fs.statSync(fullPath);
2112
+ } catch {
2113
+ return;
2114
+ }
2115
+
2116
+ if (stat.isDirectory()) {
2117
+ // Convert (group) folders — these are layout groups, not route segments
2118
+ if (/^\(.*\)$/.test(file)) {
2119
+ walkDir(fullPath, routePath);
2120
+ return;
2121
+ }
2122
+ // Convert [param] to :param
2123
+ const segment = file.replace(/\[([^\]]+)\]/g, ":$1");
2124
+ walkDir(fullPath, routePath + "/" + segment);
2125
+ return;
2126
+ }
2127
+
2128
+ // Only process +server.js or +server.ts files
2129
+ if (!/^\+server\.(js|ts)$/.test(file)) return;
2130
+
2131
+ let content;
2132
+ try {
2133
+ content = fs.readFileSync(fullPath, "utf8");
2134
+ } catch {
2135
+ return;
2136
+ }
2137
+
2138
+ // Detect exported HTTP methods
2139
+ const methods = detectAppRouterMethods(content);
2140
+
2141
+ methods.forEach(function (method) {
2142
+ const key = method + ":" + routePath;
2143
+ if (seen.has(key)) return;
2144
+ seen.add(key);
2145
+
2146
+ const needsBody = ["POST", "PUT", "PATCH"].includes(method);
2147
+ const bodyFields = needsBody
2148
+ ? extractBodyFieldsFromFile(content)
2149
+ : null;
2150
+ const routeParamMap = buildRouteParamMap(routePath, []);
2151
+
2152
+ endpoints.push({
2153
+ method,
2154
+ path: routePath || "/",
2155
+ description: "",
2156
+ requestBody: bodyFields,
2157
+ routeParamMap,
2158
+ detectedBy: "static-scan-sveltekit",
2159
+ });
2160
+ });
2161
+ });
2162
+ }
2163
+
2164
+ for (const dir of possibleDirs) {
2165
+ walkDir(dir, "");
2166
+ }
2167
+
2168
+ return endpoints;
2169
+ }
2170
+
2171
+ /**
2172
+ * Scans Remix server routes
2173
+ * Handles loader (GET) and action (POST/PUT/DELETE/PATCH) exports
2174
+ * in app/routes/ folder
2175
+ * e.g. app/routes/projects.$id.tsx with export async function loader() → GET /projects/:id
2176
+ */
2177
+ function scanRemixServerRoutes(cwd) {
2178
+ const fs = require("fs");
2179
+ const path = require("path");
2180
+ const endpoints = [];
2181
+ const seen = new Set();
2182
+
2183
+ const possibleDirs = [
2184
+ path.join(cwd, "app", "routes"),
2185
+ path.join(cwd, "routes"),
2186
+ ];
2187
+
2188
+ function walkDir(dir, prefix) {
2189
+ if (!fs.existsSync(dir)) return;
2190
+ let entries;
2191
+ try {
2192
+ entries = fs.readdirSync(dir);
2193
+ } catch {
2194
+ return;
2195
+ }
2196
+
2197
+ entries.forEach(function (file) {
2198
+ const fullPath = path.join(dir, file);
2199
+ let stat;
2200
+ try {
2201
+ stat = fs.statSync(fullPath);
2202
+ } catch {
2203
+ return;
2204
+ }
2205
+
2206
+ if (stat.isDirectory()) {
2207
+ walkDir(fullPath, prefix + "/" + file);
2208
+ return;
2209
+ }
2210
+
2211
+ if (!/\.(js|ts|jsx|tsx)$/.test(file)) return;
2212
+
2213
+ let content;
2214
+ try {
2215
+ content = fs.readFileSync(fullPath, "utf8");
2216
+ } catch {
2217
+ return;
2218
+ }
2219
+
2220
+ // Only process files that have loader or action exports
2221
+ const hasLoader = /export\s+(?:async\s+)?function\s+loader\b/.test(
2222
+ content,
2223
+ );
2224
+ const hasAction = /export\s+(?:async\s+)?function\s+action\b/.test(
2225
+ content,
2226
+ );
2227
+
2228
+ if (!hasLoader && !hasAction) return;
2229
+
2230
+ // Convert Remix filename convention to route path
2231
+ // projects.$id.tsx → /projects/:id
2232
+ // _index.tsx → /
2233
+ let fileName = file.replace(/\.(js|ts|jsx|tsx)$/, "");
2234
+ if (fileName === "_index") fileName = "";
2235
+
2236
+ const routePath =
2237
+ (prefix + "/" + fileName)
2238
+ .replace(/\.\$/g, "/:") // .$param → /:param
2239
+ .replace(/\./g, "/") // remaining dots → slashes
2240
+ .replace(/\/+/g, "/") // clean double slashes
2241
+ .replace(/\/$/, "") || "/";
2242
+
2243
+ if (hasLoader) {
2244
+ const key = "GET:" + routePath;
2245
+ if (!seen.has(key)) {
2246
+ seen.add(key);
2247
+ endpoints.push({
2248
+ method: "GET",
2249
+ path: routePath,
2250
+ description: "",
2251
+ requestBody: null,
2252
+ routeParamMap: buildRouteParamMap(routePath, []),
2253
+ detectedBy: "static-scan-remix",
2254
+ });
2255
+ }
2256
+ }
2257
+
2258
+ if (hasAction) {
2259
+ // action handles POST/PUT/DELETE/PATCH — default to POST
2260
+ // try to detect specific method from content
2261
+ const actionMethods = ["POST", "PUT", "DELETE", "PATCH"].filter((m) =>
2262
+ new RegExp(`request\\.method\\s*===?\\s*['"]${m}['"]`).test(content),
2263
+ );
2264
+
2265
+ const methodsToAdd =
2266
+ actionMethods.length > 0 ? actionMethods : ["POST"];
2267
+
2268
+ methodsToAdd.forEach(function (method) {
2269
+ const key = method + ":" + routePath;
2270
+ if (seen.has(key)) return;
2271
+ seen.add(key);
2272
+
2273
+ const bodyFields = extractBodyFieldsFromFile(content);
2274
+ endpoints.push({
2275
+ method,
2276
+ path: routePath,
2277
+ description: "",
2278
+ requestBody: bodyFields,
2279
+ routeParamMap: buildRouteParamMap(routePath, []),
2280
+ detectedBy: "static-scan-remix",
2281
+ });
2282
+ });
2283
+ }
2284
+ });
2285
+ }
2286
+
2287
+ for (const dir of possibleDirs) {
2288
+ walkDir(dir, "");
2289
+ }
2290
+
2291
+ return endpoints;
2292
+ }
2293
+
1061
2294
  module.exports = {
1062
2295
  scanExpressRoutes,
1063
2296
  scanNextJsRoutes,
1064
2297
  scanNextJsAppRoutes,
1065
2298
  extractPathParams,
1066
2299
  scanFrontendRoutes,
2300
+ scanFastifyRoutes,
2301
+ scanNestJsRoutes,
2302
+ scanKoaRoutes,
2303
+ scanHapiRoutes,
2304
+ scanAdonisRoutes,
2305
+ scanNuxtServerRoutes,
2306
+ scanSvelteKitServerRoutes,
2307
+ scanRemixServerRoutes,
1067
2308
  };