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/client.js +32 -4
- package/index.js +285 -68
- package/interceptor.js +411 -15
- package/package.json +1 -1
- package/scanner.js +1279 -38
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
|
|
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
|
|
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;
|
|
983
|
-
if (file.startsWith("_")) return;
|
|
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;
|
|
1343
|
+
if (Object.keys(paramMap).length === 0) return;
|
|
1040
1344
|
|
|
1041
1345
|
patterns.push({ pattern, params: paramMap });
|
|
1042
1346
|
});
|
|
1043
1347
|
}
|
|
1044
1348
|
|
|
1045
|
-
|
|
1046
|
-
|
|
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
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
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
|
};
|