botversion-sdk 1.0.5 → 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
@@ -29,14 +29,9 @@ function scanExpressRoutes(app, cwd) {
29
29
  }
30
30
  }
31
31
 
32
- console.log(`\n[DEBUG] ===== JS SCAN SUMMARY =====`);
33
32
  endpoints.forEach((ep) => {
34
33
  const status = ep.requestBody ? "✅" : "❌ NULL";
35
- console.log(
36
- `[DEBUG] ${status} ${ep.method.padEnd(6)} ${ep.path} → ${JSON.stringify(ep.requestBody)}`,
37
- );
38
34
  });
39
- console.log(`[DEBUG] ================================\n`);
40
35
 
41
36
  return endpoints;
42
37
  }
@@ -51,7 +46,6 @@ function scanExpressFileStatically(filePath, seen) {
51
46
  } catch {
52
47
  return endpoints;
53
48
  }
54
- console.log(`[DEBUG] Scanning file statically: ${filePath}`);
55
49
 
56
50
  // Match patterns like:
57
51
  // app.get('/path', ...)
@@ -76,10 +70,6 @@ function scanExpressFileStatically(filePath, seen) {
76
70
  const needsBody = ["POST", "PUT", "PATCH"].includes(effectiveMethod);
77
71
  const bodyFields = needsBody ? extractBodyFieldsFromFile(content) : null;
78
72
 
79
- console.log(
80
- `[DEBUG] ${effectiveMethod} ${routePath} → bodyFields: ${JSON.stringify(bodyFields)}`,
81
- );
82
-
83
73
  const routeParamMap = buildRouteParamMap(routePath, []);
84
74
 
85
75
  endpoints.push({
@@ -133,9 +123,6 @@ function extractRoutes(stack, prefix, endpoints, seen, bodyMap) {
133
123
  const fields = extractBodyFieldsFromFile(fnStr);
134
124
  if (fields) {
135
125
  requestBody = fields;
136
- console.log(
137
- `[DEBUG] Found body from inline handler for ${method} ${routePath}`,
138
- );
139
126
  break;
140
127
  }
141
128
  }
@@ -143,12 +130,8 @@ function extractRoutes(stack, prefix, endpoints, seen, bodyMap) {
143
130
  // Strategy 2: fall back to bodyMap using handler name
144
131
  if (!requestBody) {
145
132
  const handlerName = extractHandlerName(layer);
146
- console.log(
147
- `[DEBUG] ${method} ${routePath} → handlerName: ${handlerName}`,
148
- );
149
133
  if (handlerName && bodyMap[handlerName]) {
150
134
  requestBody = bodyMap[handlerName];
151
- console.log(`[DEBUG] Found body from bodyMap for ${handlerName}`);
152
135
  }
153
136
  }
154
137
  }
@@ -190,7 +173,6 @@ function scanNextJsRoutes(pagesDir) {
190
173
  const apiDir = path.join(pagesDir, "api");
191
174
 
192
175
  if (!fs.existsSync(apiDir)) {
193
- console.warn("[BotVersion SDK] No pages/api directory found at:", apiDir);
194
176
  return endpoints;
195
177
  }
196
178
 
@@ -345,6 +327,8 @@ function inferFieldType(fieldName, content) {
345
327
  new RegExp(`${fieldName}\\s*===?\\s*(true|false)`),
346
328
  new RegExp(`(true|false)\\s*===?\\s*${fieldName}`),
347
329
  new RegExp(`Boolean\\s*\\(\\s*${fieldName}`),
330
+ new RegExp(`typeof\\s+${fieldName}\\s*!==?\\s*["']boolean["']`),
331
+ new RegExp(`typeof\\s+${fieldName}\\s*===?\\s*["']boolean["']`),
348
332
  ];
349
333
  if (boolPatterns.some((p) => p.test(content))) return "boolean";
350
334
 
@@ -411,9 +395,6 @@ function extractBodyFieldsFromFile(content) {
411
395
  ? { type: "array", items: { type: "object" } }
412
396
  : { type };
413
397
  });
414
- console.log(
415
- `[DEBUG] extractBodyFieldsFromFile found fields: ${JSON.stringify([...fields])}`,
416
- );
417
398
 
418
399
  return { type: "object", properties };
419
400
  }
@@ -425,7 +406,6 @@ function scanNextJsAppRoutes(appDir) {
425
406
 
426
407
  const apiDir = path.join(appDir, "api");
427
408
  if (!fs.existsSync(apiDir)) {
428
- console.warn("[BotVersion SDK] No app/api directory found at:", apiDir);
429
409
  return endpoints;
430
410
  }
431
411
 
@@ -710,16 +690,8 @@ function scanAllExpressFiles(cwd) {
710
690
  );
711
691
 
712
692
  if (isExpressFile) {
713
- console.log(`[DEBUG] Found route file: ${fullPath}`); // already have this
714
693
  routeFiles.push(fullPath);
715
694
  }
716
-
717
- // ADD THIS — log files that are being SKIPPED
718
- else if (/\.(js|ts)$/.test(entry)) {
719
- console.log(
720
- `[DEBUG] Skipped (not detected as route file): ${fullPath}`,
721
- );
722
- }
723
695
  } catch {
724
696
  continue;
725
697
  }
@@ -795,23 +767,6 @@ function buildBodyMap(cwd) {
795
767
  /export\s+const\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(?:async\s*)?\([^)]*\)\s*=>\s*\{/g,
796
768
  ];
797
769
 
798
- if (content.includes("req.body")) {
799
- console.log(`[DEBUG] bodyMap: found req.body in ${fullPath}`);
800
- console.log(
801
- `[DEBUG] bodyMap: fnNames found: ${JSON.stringify([
802
- ...(() => {
803
- const names = new Set();
804
- for (const p of fnPatterns) {
805
- p.lastIndex = 0;
806
- let m;
807
- while ((m = p.exec(content)) !== null) names.add(m[1]);
808
- }
809
- return names;
810
- })(),
811
- ])}`,
812
- );
813
- }
814
-
815
770
  // Map every function name in this file to the whole file's body fields.
816
771
  // Brace-counting is unreliable across JS/TS variations, so we use
817
772
  // file-level field extraction here. Per-function accuracy is handled
@@ -835,7 +790,6 @@ function buildBodyMap(cwd) {
835
790
  }
836
791
 
837
792
  walk(cwd, 0);
838
- console.log(`[DEBUG] bodyMap keys: ${Object.keys(bodyMap)}`);
839
793
  return bodyMap;
840
794
  }
841
795
 
@@ -898,9 +852,8 @@ function scanConfigBasedRoutes(cwd) {
898
852
  const patterns = [];
899
853
  const seen = new Set();
900
854
 
901
- // Files to scan for route definitions
902
855
  const filesToCheck = [
903
- // React Router — common file names
856
+ // React Router
904
857
  path.join(cwd, "src", "App.jsx"),
905
858
  path.join(cwd, "src", "App.tsx"),
906
859
  path.join(cwd, "src", "App.js"),
@@ -920,8 +873,46 @@ function scanConfigBasedRoutes(cwd) {
920
873
  // Angular
921
874
  path.join(cwd, "src", "app", "app-routing.module.ts"),
922
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"),
923
880
  ];
924
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
+
925
916
  for (const filePath of filesToCheck) {
926
917
  if (!fs.existsSync(filePath)) continue;
927
918
 
@@ -932,8 +923,6 @@ function scanConfigBasedRoutes(cwd) {
932
923
  continue;
933
924
  }
934
925
 
935
- console.log("[BotVersion SDK] Scanning config-based routes in:", filePath);
936
-
937
926
  // Pattern 1 — React Router JSX: <Route path="/:projectId/dashboard" />
938
927
  const jsxRouteMatches = content.matchAll(
939
928
  /<Route[^>]+path=["']([^"']+)["']/g,
@@ -955,6 +944,32 @@ function scanConfigBasedRoutes(cwd) {
955
944
  for (const match of angularRouteMatches) {
956
945
  addConfigPattern(match[1], seen, patterns);
957
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
+ }
958
973
  }
959
974
 
960
975
  return patterns;
@@ -984,12 +999,266 @@ function addConfigPattern(routePath, seen, patterns) {
984
999
  if (Object.keys(paramMap).length === 0) return;
985
1000
 
986
1001
  patterns.push({ pattern: normalized, params: paramMap });
987
- console.log(
988
- "[BotVersion SDK] Found config-based route pattern:",
989
- normalized,
990
- "→",
991
- paramMap,
992
- );
1002
+ }
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;
993
1262
  }
994
1263
 
995
1264
  function scanFrontendRoutes(cwd) {
@@ -998,21 +1267,7 @@ function scanFrontendRoutes(cwd) {
998
1267
  const patterns = [];
999
1268
  const seen = new Set();
1000
1269
 
1001
- const dirsToScan = [
1002
- // Next.js Pages Router
1003
- path.join(cwd, "pages"),
1004
- path.join(cwd, "src", "pages"),
1005
- // Next.js App Router
1006
- path.join(cwd, "app"),
1007
- path.join(cwd, "src", "app"),
1008
- // Nuxt.js
1009
- path.join(cwd, "pages"),
1010
- // SvelteKit
1011
- path.join(cwd, "src", "routes"),
1012
- path.join(cwd, "routes"),
1013
- // Remix
1014
- path.join(cwd, "app", "routes"),
1015
- ];
1270
+ const candidateDirs = findAllFrontendDirs(cwd);
1016
1271
 
1017
1272
  function walkDir(dir, routeSegments) {
1018
1273
  if (!fs.existsSync(dir)) return;
@@ -1033,10 +1288,9 @@ function scanFrontendRoutes(cwd) {
1033
1288
  }
1034
1289
 
1035
1290
  if (stat.isDirectory()) {
1036
- if (file === "api") return; // skip API routes
1037
- if (file.startsWith("_")) return; // skip Next.js internals
1291
+ if (file === "api") return;
1292
+ if (file.startsWith("_")) return;
1038
1293
  if (/^\(.*\)$/.test(file)) {
1039
- // route groups like (marketing) — don't add to URL
1040
1294
  walkDir(fullPath, routeSegments);
1041
1295
  return;
1042
1296
  }
@@ -1046,12 +1300,11 @@ function scanFrontendRoutes(cwd) {
1046
1300
  return;
1047
1301
  }
1048
1302
 
1049
- if (!/\.(js|ts|jsx|tsx|vue|svelte)$/.test(file)) return;
1303
+ if (!/\.(js|ts|jsx|tsx|vue|svelte|astro)$/.test(file)) return;
1050
1304
  if (file.startsWith("_")) return;
1051
1305
 
1052
- const routeName = file.replace(/\.(js|ts|jsx|tsx|vue|svelte)$/, "");
1306
+ const routeName = file.replace(/\.(js|ts|jsx|tsx|vue|svelte|astro)$/, "");
1053
1307
 
1054
- // Skip non-page files
1055
1308
  if (
1056
1309
  ["layout", "loading", "error", "template", "not-found"].includes(
1057
1310
  routeName,
@@ -1059,16 +1312,13 @@ function scanFrontendRoutes(cwd) {
1059
1312
  )
1060
1313
  return;
1061
1314
 
1062
- // SvelteKit uses +page.svelte — skip +layout, +error etc.
1063
1315
  if (routeName.startsWith("+") && routeName !== "+page") return;
1064
1316
 
1065
- // Remix uses dots as path separators — e.g. $projectId.dashboard.tsx → /:projectId/dashboard
1066
1317
  const isRemixRoute =
1067
1318
  routeName.includes(".") && !routeName.startsWith("+");
1068
1319
  let finalSegments;
1069
1320
 
1070
1321
  if (isRemixRoute) {
1071
- // Split by dots and convert each segment
1072
1322
  const remixSegments = routeName
1073
1323
  .split(".")
1074
1324
  .map((s) => convertNextJsSegment(s) || s);
@@ -1090,38 +1340,969 @@ function scanFrontendRoutes(cwd) {
1090
1340
  seen.add(pattern);
1091
1341
 
1092
1342
  const paramMap = extractParamPositions(finalSegments);
1093
- if (Object.keys(paramMap).length === 0) return; // no dynamic params, skip static routes
1343
+ if (Object.keys(paramMap).length === 0) return;
1094
1344
 
1095
1345
  patterns.push({ pattern, params: paramMap });
1096
- console.log(
1097
- "[BotVersion SDK] Found frontend route pattern:",
1098
- pattern,
1099
- "→",
1100
- paramMap,
1101
- );
1102
1346
  });
1103
1347
  }
1104
1348
 
1105
- dirsToScan.forEach(function (dir) {
1106
- if (fs.existsSync(dir)) walkDir(dir, []);
1107
- });
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
+ ];
1108
1368
 
1109
- // Also scan config-based frameworks (React Router, Vue Router, Angular)
1110
- const configPatterns = scanConfigBasedRoutes(cwd);
1111
- configPatterns.forEach(function (p) {
1112
- if (!seen.has(p.pattern)) {
1113
- seen.add(p.pattern);
1114
- patterns.push(p);
1115
- }
1116
- });
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
+ }
1117
1383
 
1118
1384
  return patterns;
1119
1385
  }
1120
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
+
1121
2294
  module.exports = {
1122
2295
  scanExpressRoutes,
1123
2296
  scanNextJsRoutes,
1124
2297
  scanNextJsAppRoutes,
1125
2298
  extractPathParams,
1126
2299
  scanFrontendRoutes,
2300
+ scanFastifyRoutes,
2301
+ scanNestJsRoutes,
2302
+ scanKoaRoutes,
2303
+ scanHapiRoutes,
2304
+ scanAdonisRoutes,
2305
+ scanNuxtServerRoutes,
2306
+ scanSvelteKitServerRoutes,
2307
+ scanRemixServerRoutes,
1127
2308
  };