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/bin/init.js +3 -3
- package/cli/generator.js +3 -3
- package/cli/writer.js +1 -1
- package/client.js +38 -223
- package/index.js +285 -211
- package/interceptor.js +451 -118
- package/package.json +4 -3
- package/scanner.js +1281 -100
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
|
|
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
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
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
|
|
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;
|
|
1037
|
-
if (file.startsWith("_")) return;
|
|
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;
|
|
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
|
-
|
|
1106
|
-
|
|
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
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
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
|
};
|