botversion-sdk 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/scanner.js CHANGED
@@ -4,31 +4,84 @@
4
4
  /**
5
5
  * Scans an Express app object and extracts all registered routes
6
6
  */
7
- function scanExpressRoutes(app) {
7
+ function scanExpressRoutes(app, cwd) {
8
8
  const endpoints = [];
9
9
  const seen = new Set();
10
10
 
11
- // Force Express to initialize its router if it hasn't yet
12
- if (app.lazyrouter) app.lazyrouter();
13
- const router = app._router || app.router || (app.stack ? app : null);
11
+ // Build body map from ALL files upfront
12
+ const bodyMap = cwd ? buildBodyMap(cwd) : {};
14
13
 
15
- if (!router) {
16
- console.warn(
17
- "[BotVersion SDK] No router found — trying app._events fallback",
18
- );
14
+ if (app) {
15
+ if (app.lazyrouter) app.lazyrouter();
16
+ const router = app._router || app.router || (app.stack ? app : null);
17
+ if (router) {
18
+ const stack = router.stack || [];
19
+ extractRoutes(stack, "", endpoints, seen, bodyMap);
20
+ }
21
+ }
19
22
 
20
- if (app._events && app._events.request) {
21
- const handler = app._events.request;
22
- if (handler && handler.stack) {
23
- extractRoutes(handler.stack, "", endpoints, seen);
24
- return endpoints;
25
- }
23
+ // ALSO scan all JS/TS files statically for route definitions
24
+ if (cwd) {
25
+ const allFiles = scanAllExpressFiles(cwd);
26
+ for (const file of allFiles) {
27
+ const fileEndpoints = scanExpressFileStatically(file, seen);
28
+ endpoints.push(...fileEndpoints);
26
29
  }
30
+ }
31
+
32
+ endpoints.forEach((ep) => {
33
+ const status = ep.requestBody ? "✅" : "❌ NULL";
34
+ });
35
+
36
+ return endpoints;
37
+ }
38
+
39
+ function scanExpressFileStatically(filePath, seen) {
40
+ const fs = require("fs");
41
+ const endpoints = [];
42
+
43
+ let content;
44
+ try {
45
+ content = fs.readFileSync(filePath, "utf8");
46
+ } catch {
27
47
  return endpoints;
28
48
  }
29
49
 
30
- const stack = router.stack || [];
31
- extractRoutes(stack, "", endpoints, seen);
50
+ // Match patterns like:
51
+ // app.get('/path', ...)
52
+ // router.post('/path', ...)
53
+ // app.all('/curd.php', ...)
54
+ const routePattern =
55
+ /\.(get|post|put|delete|patch|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
56
+
57
+ let match;
58
+ while ((match = routePattern.exec(content)) !== null) {
59
+ const method = match[1].toUpperCase();
60
+ const routePath = match[2];
61
+
62
+ // Skip middleware patterns
63
+ if (routePath.includes("*")) continue;
64
+
65
+ const key = method + ":" + routePath;
66
+ if (seen.has(key)) continue;
67
+ seen.add(key);
68
+
69
+ const effectiveMethod = method === "ALL" ? "GET" : method;
70
+ const needsBody = ["POST", "PUT", "PATCH"].includes(effectiveMethod);
71
+ const bodyFields = needsBody ? extractBodyFieldsFromFile(content) : null;
72
+
73
+ const routeParamMap = buildRouteParamMap(routePath, []);
74
+
75
+ endpoints.push({
76
+ method: effectiveMethod,
77
+ path: routePath,
78
+ description: "",
79
+ requestBody: bodyFields,
80
+ routeParamMap: routeParamMap,
81
+ detectedBy: "static-scan-file",
82
+ });
83
+ }
84
+
32
85
  return endpoints;
33
86
  }
34
87
 
@@ -40,7 +93,7 @@ function scanExpressRoutes(app) {
40
93
  * - a router layer → layer.name === 'router', has its own .handle.stack
41
94
  * - middleware → everything else (body-parser, cors, etc.) — skip these
42
95
  */
43
- function extractRoutes(stack, prefix, endpoints, seen) {
96
+ function extractRoutes(stack, prefix, endpoints, seen, bodyMap) {
44
97
  prefix = prefix || "";
45
98
 
46
99
  stack.forEach(function (layer) {
@@ -53,28 +106,47 @@ function extractRoutes(stack, prefix, endpoints, seen) {
53
106
 
54
107
  methods.forEach(function (method) {
55
108
  method = method.toUpperCase();
56
-
57
- // Skip Express internals
58
109
  if (method === "_ALL") return;
59
110
 
60
111
  var key = method + ":" + routePath;
61
112
  if (seen.has(key)) return;
62
113
  seen.add(key);
63
114
 
64
- var params = extractPathParams(routePath);
115
+ const needsBody = ["POST", "PUT", "PATCH"].includes(method);
116
+ let requestBody = null;
117
+
118
+ if (needsBody) {
119
+ // Strategy 1: scan inline handler directly via fn.toString() — most accurate
120
+ for (const handler of layer.route.stack) {
121
+ const fn = handler.handle || handler;
122
+ const fnStr = fn.toString();
123
+ const fields = extractBodyFieldsFromFile(fnStr);
124
+ if (fields) {
125
+ requestBody = fields;
126
+ break;
127
+ }
128
+ }
129
+
130
+ // Strategy 2: fall back to bodyMap using handler name
131
+ if (!requestBody) {
132
+ const handlerName = extractHandlerName(layer);
133
+ if (handlerName && bodyMap[handlerName]) {
134
+ requestBody = bodyMap[handlerName];
135
+ }
136
+ }
137
+ }
138
+
139
+ const routeParamMap = buildRouteParamMap(routePath, []);
65
140
 
66
141
  endpoints.push({
67
142
  method: method,
68
143
  path: routePath,
69
144
  description: "",
70
- requestBody:
71
- method !== "GET" && params.length > 0
72
- ? buildParamSchema(params)
73
- : null,
145
+ requestBody: requestBody,
146
+ routeParamMap: routeParamMap,
74
147
  detectedBy: "static-scan",
75
148
  });
76
149
  });
77
-
78
150
  return;
79
151
  }
80
152
 
@@ -82,7 +154,7 @@ function extractRoutes(stack, prefix, endpoints, seen) {
82
154
  if (layer.name === "router" && layer.handle && layer.handle.stack) {
83
155
  // Extract the mount path from the regexp
84
156
  var mountPath = prefix + regexpToPath(layer.regexp, layer.keys);
85
- extractRoutes(layer.handle.stack, mountPath, endpoints, seen);
157
+ extractRoutes(layer.handle.stack, mountPath, endpoints, seen, bodyMap);
86
158
  return;
87
159
  }
88
160
 
@@ -101,7 +173,6 @@ function scanNextJsRoutes(pagesDir) {
101
173
  const apiDir = path.join(pagesDir, "api");
102
174
 
103
175
  if (!fs.existsSync(apiDir)) {
104
- console.warn("[BotVersion SDK] No pages/api directory found at:", apiDir);
105
176
  return endpoints;
106
177
  }
107
178
 
@@ -139,11 +210,14 @@ function scanNextJsRoutes(pagesDir) {
139
210
  bodyFields ||
140
211
  (method === "DELETE" && queryFields ? queryFields : null);
141
212
 
213
+ const routeParamMap = buildRouteParamMap(normalizedPath, []);
214
+
142
215
  endpoints.push({
143
216
  method: method,
144
217
  path: normalizedPath,
145
218
  description: "",
146
219
  requestBody: effectiveRequestBody,
220
+ routeParamMap: routeParamMap,
147
221
  detectedBy: "static-scan",
148
222
  });
149
223
  });
@@ -230,6 +304,37 @@ function buildParamSchema(params) {
230
304
  return schema;
231
305
  }
232
306
 
307
+ function inferFieldType(fieldName, content) {
308
+ const arrayPatterns = [
309
+ new RegExp(
310
+ `${fieldName}\\s*\\.\\s*(map|filter|forEach|push|reduce|find|some|every|includes|join|slice|splice|length)\\b`,
311
+ ),
312
+ new RegExp(`Array\\.isArray\\s*\\(\\s*${fieldName}\\s*\\)`),
313
+ new RegExp(`for\\s*\\(.*of\\s+${fieldName}\\b`),
314
+ new RegExp(`\\[\\s*\\.\\.\\.${fieldName}\\s*\\]`),
315
+ ];
316
+ if (arrayPatterns.some((p) => p.test(content))) return "array";
317
+
318
+ const numberPatterns = [
319
+ new RegExp(`${fieldName}\\s*[+\\-*/%]\\s*\\d`),
320
+ new RegExp(`parseInt\\s*\\(\\s*${fieldName}`),
321
+ new RegExp(`parseFloat\\s*\\(\\s*${fieldName}`),
322
+ new RegExp(`Number\\s*\\(\\s*${fieldName}`),
323
+ ];
324
+ if (numberPatterns.some((p) => p.test(content))) return "number";
325
+
326
+ const boolPatterns = [
327
+ new RegExp(`${fieldName}\\s*===?\\s*(true|false)`),
328
+ new RegExp(`(true|false)\\s*===?\\s*${fieldName}`),
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["']`),
332
+ ];
333
+ if (boolPatterns.some((p) => p.test(content))) return "boolean";
334
+
335
+ return "string";
336
+ }
337
+
233
338
  function extractBodyFieldsFromFile(content) {
234
339
  const fields = new Set();
235
340
 
@@ -284,12 +389,188 @@ function extractBodyFieldsFromFile(content) {
284
389
 
285
390
  const properties = {};
286
391
  fields.forEach(function (field) {
287
- properties[field] = { type: "string" };
392
+ const type = inferFieldType(field, content);
393
+ properties[field] =
394
+ type === "array"
395
+ ? { type: "array", items: { type: "object" } }
396
+ : { type };
397
+ });
398
+
399
+ return { type: "object", properties };
400
+ }
401
+
402
+ function scanNextJsAppRoutes(appDir) {
403
+ const fs = require("fs");
404
+ const path = require("path");
405
+ const endpoints = [];
406
+
407
+ const apiDir = path.join(appDir, "api");
408
+ if (!fs.existsSync(apiDir)) {
409
+ return endpoints;
410
+ }
411
+
412
+ function walkDir(dir, routePath) {
413
+ routePath = routePath || "/api";
414
+ const files = fs.readdirSync(dir);
415
+
416
+ files.forEach(function (file) {
417
+ const fullPath = path.join(dir, file);
418
+ const stat = fs.statSync(fullPath);
419
+
420
+ if (stat.isDirectory()) {
421
+ // Convert [param] → :param
422
+ const segment = file.replace(/\[([^\]]+)\]/g, ":$1");
423
+ walkDir(fullPath, routePath + "/" + segment);
424
+ return;
425
+ }
426
+
427
+ // Only process route.ts / route.js
428
+ if (!/^route\.(js|ts)$/.test(file)) return;
429
+
430
+ const content = fs.readFileSync(fullPath, "utf8");
431
+ const methods = detectAppRouterMethods(content);
432
+
433
+ methods.forEach(function (method) {
434
+ const bodyFields =
435
+ method !== "GET" ? extractAppRouterBodyFields(content) : null;
436
+ const queryFields = extractQueryFieldsFromFile(content);
437
+
438
+ const routeParamMap = buildRouteParamMap(routePath, []);
439
+
440
+ endpoints.push({
441
+ method: method,
442
+ path: routePath,
443
+ description: "",
444
+ requestBody:
445
+ bodyFields ||
446
+ (method === "DELETE" && queryFields ? queryFields : null),
447
+ routeParamMap: routeParamMap,
448
+ detectedBy: "static-scan",
449
+ });
450
+ });
451
+ });
452
+ }
453
+
454
+ walkDir(apiDir);
455
+ return endpoints;
456
+ }
457
+
458
+ function detectAppRouterMethods(content) {
459
+ const methods = [];
460
+ const patterns = [
461
+ { pattern: /export\s+async\s+function\s+GET\b/, method: "GET" },
462
+ { pattern: /export\s+async\s+function\s+POST\b/, method: "POST" },
463
+ { pattern: /export\s+async\s+function\s+PUT\b/, method: "PUT" },
464
+ { pattern: /export\s+async\s+function\s+DELETE\b/, method: "DELETE" },
465
+ { pattern: /export\s+async\s+function\s+PATCH\b/, method: "PATCH" },
466
+ // named exports too: export { POST }
467
+ { pattern: /export\s+function\s+GET\b/, method: "GET" },
468
+ { pattern: /export\s+function\s+POST\b/, method: "POST" },
469
+ { pattern: /export\s+function\s+PUT\b/, method: "PUT" },
470
+ { pattern: /export\s+function\s+DELETE\b/, method: "DELETE" },
471
+ { pattern: /export\s+function\s+PATCH\b/, method: "PATCH" },
472
+ ];
473
+
474
+ patterns.forEach(function (p) {
475
+ if (p.pattern.test(content)) methods.push(p.method);
476
+ });
477
+
478
+ return methods.length > 0 ? methods : ["GET"];
479
+ }
480
+
481
+ function extractAppRouterBodyFields(content) {
482
+ const fields = new Set();
483
+
484
+ // Pattern 1: const { userId, tokens } = await request.json()
485
+ const destructureMatches = content.matchAll(
486
+ /const\s*\{([^}]+)\}\s*=\s*await\s+\w+\.json\(\)/g,
487
+ );
488
+ for (const match of destructureMatches) {
489
+ match[1].split(",").forEach(function (f) {
490
+ const clean = f.trim().split(":")[0].trim();
491
+ if (clean) fields.add(clean);
492
+ });
493
+ }
494
+
495
+ // Pattern 2: const body = await request.json() then body.userId
496
+ const bodyVarMatch = content.match(
497
+ /const\s+(\w+)\s*=\s*await\s+\w+\.json\(\)/,
498
+ );
499
+ if (bodyVarMatch) {
500
+ const varName = bodyVarMatch[1];
501
+ const varMatches = content.matchAll(
502
+ new RegExp(`${varName}\\.([a-zA-Z_][a-zA-Z0-9_]*)`, "g"),
503
+ );
504
+ for (const match of varMatches) {
505
+ fields.add(match[1]);
506
+ }
507
+ }
508
+
509
+ // Pattern 3: (await request.json()).userId
510
+ const inlineMatches = content.matchAll(
511
+ /\(await\s+\w+\.json\(\)\)\.([a-zA-Z_][a-zA-Z0-9_]*)/g,
512
+ );
513
+ for (const match of inlineMatches) {
514
+ fields.add(match[1]);
515
+ }
516
+
517
+ if (fields.size === 0) return null;
518
+
519
+ const properties = {};
520
+ fields.forEach(function (field) {
521
+ const type = inferFieldType(field, content);
522
+ properties[field] =
523
+ type === "array"
524
+ ? { type: "array", items: { type: "object" } }
525
+ : { type };
288
526
  });
289
527
 
290
528
  return { type: "object", properties };
291
529
  }
292
530
 
531
+ function buildRouteParamMap(routePath, segments) {
532
+ const paramMap = {};
533
+ const parts = routePath.split("/").filter(Boolean);
534
+
535
+ for (let i = 0; i < parts.length; i++) {
536
+ const part = parts[i];
537
+
538
+ // Skip catch-all [...slug] and optional [[...slug]]
539
+ if (/^\[?\[\.\.\./.test(part)) continue;
540
+
541
+ // Check if this segment is a dynamic param
542
+ // Handles both Next.js style [id] and Express style :id
543
+ const nextParamMatch = part.match(/^\[([^\]]+)\]$/);
544
+ const expressParamMatch = part.match(/^:([a-zA-Z_][a-zA-Z0-9_]*)$/);
545
+ const paramMatch = nextParamMatch || expressParamMatch;
546
+ if (!paramMatch) continue;
547
+
548
+ const paramName = paramMatch[1];
549
+
550
+ // If param already has a descriptive name like [projectId], use it as-is
551
+ if (paramName !== "id" && paramName !== "slug" && paramName !== "param") {
552
+ paramMap[paramName] = paramName;
553
+ continue;
554
+ }
555
+
556
+ // If param is just [id], look at the parent folder name to figure out type
557
+ // e.g. projects/[id] → projectId
558
+ const parentSegment = parts[i - 1];
559
+ if (parentSegment) {
560
+ // Remove any dynamic brackets from parent if it's also a param
561
+ const cleanParent = parentSegment.replace(/^\[([^\]]+)\]$/, "$1");
562
+ // Singularize simple plural names: projects → project, users → user
563
+ const singular = cleanParent.replace(/ies$/, "y").replace(/s$/, "");
564
+ paramMap[paramName] = singular + "Id";
565
+ } else {
566
+ // No parent segment, just call it "id"
567
+ paramMap[paramName] = "id";
568
+ }
569
+ }
570
+
571
+ return paramMap;
572
+ }
573
+
293
574
  function extractQueryFieldsFromFile(content) {
294
575
  const fields = new Set();
295
576
 
@@ -355,8 +636,432 @@ function regexpToPath(regexp, keys) {
355
636
  }
356
637
  }
357
638
 
639
+ function scanAllExpressFiles(cwd) {
640
+ const fs = require("fs");
641
+ const path = require("path");
642
+
643
+ const SKIP_DIRS = [
644
+ "node_modules",
645
+ ".git",
646
+ ".next",
647
+ "dist",
648
+ "build",
649
+ ".cache",
650
+ "coverage",
651
+ "out",
652
+ ];
653
+
654
+ const routeFiles = [];
655
+
656
+ function walk(dir, depth) {
657
+ if (depth > 4) return;
658
+
659
+ let entries;
660
+ try {
661
+ entries = fs.readdirSync(dir);
662
+ } catch {
663
+ return;
664
+ }
665
+
666
+ for (const entry of entries) {
667
+ if (SKIP_DIRS.includes(entry)) continue;
668
+
669
+ const fullPath = path.join(dir, entry);
670
+ let stat;
671
+ try {
672
+ stat = fs.statSync(fullPath);
673
+ } catch {
674
+ continue;
675
+ }
676
+
677
+ if (stat.isDirectory()) {
678
+ walk(fullPath, depth + 1);
679
+ } else if (/\.(js|ts)$/.test(entry)) {
680
+ try {
681
+ const content = fs.readFileSync(fullPath, "utf8");
682
+ // Check if file contains Express route definitions
683
+ const isExpressFile =
684
+ content.includes("express()") ||
685
+ content.includes("express.Router()") ||
686
+ content.includes("Router()") ||
687
+ // Match only route-like patterns: app.get('/...) or router.post('/...)
688
+ /(?:app|router|server)\.(get|post|put|delete|patch|all)\s*\(\s*['"`]\//.test(
689
+ content,
690
+ );
691
+
692
+ if (isExpressFile) {
693
+ routeFiles.push(fullPath);
694
+ }
695
+ } catch {
696
+ continue;
697
+ }
698
+ }
699
+ }
700
+ }
701
+
702
+ walk(cwd, 0);
703
+ return routeFiles;
704
+ }
705
+
706
+ function buildBodyMap(cwd) {
707
+ const fs = require("fs");
708
+ const path = require("path");
709
+ const bodyMap = {}; // { functionName: { type: "object", properties: {...} } }
710
+
711
+ const SKIP_DIRS = [
712
+ "node_modules",
713
+ ".git",
714
+ ".next",
715
+ "dist",
716
+ "build",
717
+ ".cache",
718
+ "coverage",
719
+ "out",
720
+ ];
721
+
722
+ function walk(dir, depth) {
723
+ if (depth > 4) return;
724
+ let entries;
725
+ try {
726
+ entries = fs.readdirSync(dir);
727
+ } catch {
728
+ return;
729
+ }
730
+
731
+ for (const entry of entries) {
732
+ if (SKIP_DIRS.includes(entry)) continue;
733
+ const fullPath = path.join(dir, entry);
734
+ let stat;
735
+ try {
736
+ stat = fs.statSync(fullPath);
737
+ } catch {
738
+ continue;
739
+ }
740
+
741
+ if (stat.isDirectory()) {
742
+ walk(fullPath, depth + 1);
743
+ continue;
744
+ }
745
+
746
+ if (!/\.(js|ts)$/.test(entry)) continue;
747
+
748
+ let content;
749
+ try {
750
+ content = fs.readFileSync(fullPath, "utf8");
751
+ } catch {
752
+ continue;
753
+ }
754
+
755
+ // Skip files with no req.body at all
756
+ // Split file into individual function chunks more reliably
757
+ // by finding each function and extracting a reasonable chunk after it
758
+
759
+ const fnPatterns = [
760
+ // function loginUser(req, res) {
761
+ /(?:export\s+)?(?:async\s+)?function\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\([^)]*\)\s*\{/g,
762
+ // const loginUser = async (req, res) => {
763
+ /const\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(?:async\s*)?\([^)]*\)\s*=>\s*\{/g,
764
+ // exports.loginUser = async (req, res) => {
765
+ /exports\.([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(?:async\s*)?\([^)]*\)\s*=>\s*\{/g,
766
+ // export const loginUser = async (req, res) => {
767
+ /export\s+const\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(?:async\s*)?\([^)]*\)\s*=>\s*\{/g,
768
+ ];
769
+
770
+ // Map every function name in this file to the whole file's body fields.
771
+ // Brace-counting is unreliable across JS/TS variations, so we use
772
+ // file-level field extraction here. Per-function accuracy is handled
773
+ // at runtime by Strategy 2 (fn.toString()) in extractRoutes.
774
+ const fileFields = extractBodyFieldsFromFile(content);
775
+ if (fileFields) {
776
+ const fnNames = new Set();
777
+ for (const pattern of fnPatterns) {
778
+ pattern.lastIndex = 0; // reset regex state before each use
779
+ let m;
780
+ while ((m = pattern.exec(content)) !== null) fnNames.add(m[1]);
781
+ }
782
+ for (const name of fnNames) {
783
+ if (!bodyMap[name]) {
784
+ // don't overwrite a more precise entry
785
+ bodyMap[name] = fileFields;
786
+ }
787
+ }
788
+ }
789
+ }
790
+ }
791
+
792
+ walk(cwd, 0);
793
+ return bodyMap;
794
+ }
795
+
796
+ function extractHandlerName(layer) {
797
+ const handlers = layer.route.stack.map((h) => h.handle || h);
798
+
799
+ const SKIP_NAMES = new Set([
800
+ "anonymous",
801
+ "",
802
+ "bound dispatch",
803
+ "middleware",
804
+ "protect",
805
+ "admin",
806
+ "auth",
807
+ "verify",
808
+ "validate",
809
+ "isAuth",
810
+ "isAdmin",
811
+ "checkAuth",
812
+ "authenticate",
813
+ ]);
814
+
815
+ // Try from last to first, skip known middleware names
816
+ for (let i = handlers.length - 1; i >= 0; i--) {
817
+ const fn = handlers[i];
818
+ const name = fn.name || "";
819
+ if (name && !SKIP_NAMES.has(name) && !name.startsWith("bound ")) {
820
+ return name;
821
+ }
822
+ }
823
+ return null;
824
+ }
825
+
826
+ function convertNextJsSegment(segment) {
827
+ // Skip catch-all [...slug] and optional [[...slug]]
828
+ if (/^\[?\[\.\.\./.test(segment)) return null;
829
+ // Convert [projectId] → :projectId (Next.js / Nuxt / SvelteKit)
830
+ if (/^\[([^\]]+)\]$/.test(segment))
831
+ return segment.replace(/^\[([^\]]+)\]$/, ":$1");
832
+ // Convert $projectId → :projectId (Remix)
833
+ if (/^\$([a-zA-Z_][a-zA-Z0-9_]*)$/.test(segment))
834
+ return segment.replace(/^\$/, ":");
835
+ return segment;
836
+ }
837
+
838
+ function extractParamPositions(segments) {
839
+ const paramMap = {};
840
+ segments.forEach(function (segment, index) {
841
+ if (segment && segment.startsWith(":")) {
842
+ const paramName = segment.slice(1);
843
+ paramMap[paramName] = index;
844
+ }
845
+ });
846
+ return paramMap;
847
+ }
848
+
849
+ function scanConfigBasedRoutes(cwd) {
850
+ const fs = require("fs");
851
+ const path = require("path");
852
+ const patterns = [];
853
+ const seen = new Set();
854
+
855
+ // Files to scan for route definitions
856
+ const filesToCheck = [
857
+ // React Router — common file names
858
+ path.join(cwd, "src", "App.jsx"),
859
+ path.join(cwd, "src", "App.tsx"),
860
+ path.join(cwd, "src", "App.js"),
861
+ path.join(cwd, "src", "router.jsx"),
862
+ path.join(cwd, "src", "router.tsx"),
863
+ path.join(cwd, "src", "router.js"),
864
+ path.join(cwd, "src", "routes.jsx"),
865
+ path.join(cwd, "src", "routes.tsx"),
866
+ path.join(cwd, "src", "routes.js"),
867
+ path.join(cwd, "src", "Router.jsx"),
868
+ path.join(cwd, "src", "Router.tsx"),
869
+ // Vue Router
870
+ path.join(cwd, "src", "router", "index.js"),
871
+ path.join(cwd, "src", "router", "index.ts"),
872
+ path.join(cwd, "src", "router.js"),
873
+ path.join(cwd, "src", "router.ts"),
874
+ // Angular
875
+ path.join(cwd, "src", "app", "app-routing.module.ts"),
876
+ path.join(cwd, "src", "app", "app.routes.ts"),
877
+ ];
878
+
879
+ for (const filePath of filesToCheck) {
880
+ if (!fs.existsSync(filePath)) continue;
881
+
882
+ let content;
883
+ try {
884
+ content = fs.readFileSync(filePath, "utf8");
885
+ } catch {
886
+ continue;
887
+ }
888
+
889
+ // Pattern 1 — React Router JSX: <Route path="/:projectId/dashboard" />
890
+ const jsxRouteMatches = content.matchAll(
891
+ /<Route[^>]+path=["']([^"']+)["']/g,
892
+ );
893
+ for (const match of jsxRouteMatches) {
894
+ addConfigPattern(match[1], seen, patterns);
895
+ }
896
+
897
+ // Pattern 2 — React Router / Vue Router object: { path: '/:projectId/dashboard' }
898
+ const objectRouteMatches = content.matchAll(/path\s*:\s*["']([^"']+)["']/g);
899
+ for (const match of objectRouteMatches) {
900
+ addConfigPattern(match[1], seen, patterns);
901
+ }
902
+
903
+ // Pattern 3 — Angular: { path: ':projectId/dashboard' }
904
+ const angularRouteMatches = content.matchAll(
905
+ /\{\s*path\s*:\s*["']([^"']+)["']/g,
906
+ );
907
+ for (const match of angularRouteMatches) {
908
+ addConfigPattern(match[1], seen, patterns);
909
+ }
910
+ }
911
+
912
+ return patterns;
913
+ }
914
+
915
+ function addConfigPattern(routePath, seen, patterns) {
916
+ // Skip empty, wildcard and catch-all routes
917
+ if (!routePath || routePath === "*" || routePath === "**") return;
918
+ // Skip routes with no dynamic params
919
+ if (!routePath.includes(":") && !routePath.includes("$")) return;
920
+
921
+ // Normalize — ensure leading slash
922
+ const normalized = routePath.startsWith("/") ? routePath : "/" + routePath;
923
+
924
+ if (seen.has(normalized)) return;
925
+ seen.add(normalized);
926
+
927
+ // Extract params and their positions
928
+ const segments = normalized.split("/").filter(Boolean);
929
+ const paramMap = {};
930
+ segments.forEach(function (segment, index) {
931
+ if (segment.startsWith(":")) {
932
+ paramMap[segment.slice(1)] = index;
933
+ }
934
+ });
935
+
936
+ if (Object.keys(paramMap).length === 0) return;
937
+
938
+ patterns.push({ pattern: normalized, params: paramMap });
939
+ }
940
+
941
+ function scanFrontendRoutes(cwd) {
942
+ const fs = require("fs");
943
+ const path = require("path");
944
+ const patterns = [];
945
+ const seen = new Set();
946
+
947
+ const dirsToScan = [
948
+ // Next.js Pages Router
949
+ path.join(cwd, "pages"),
950
+ path.join(cwd, "src", "pages"),
951
+ // Next.js App Router
952
+ path.join(cwd, "app"),
953
+ path.join(cwd, "src", "app"),
954
+ // Nuxt.js
955
+ path.join(cwd, "pages"),
956
+ // SvelteKit
957
+ path.join(cwd, "src", "routes"),
958
+ path.join(cwd, "routes"),
959
+ // Remix
960
+ path.join(cwd, "app", "routes"),
961
+ ];
962
+
963
+ function walkDir(dir, routeSegments) {
964
+ if (!fs.existsSync(dir)) return;
965
+ let entries;
966
+ try {
967
+ entries = fs.readdirSync(dir);
968
+ } catch {
969
+ return;
970
+ }
971
+
972
+ entries.forEach(function (file) {
973
+ const fullPath = path.join(dir, file);
974
+ let stat;
975
+ try {
976
+ stat = fs.statSync(fullPath);
977
+ } catch {
978
+ return;
979
+ }
980
+
981
+ if (stat.isDirectory()) {
982
+ if (file === "api") return; // skip API routes
983
+ if (file.startsWith("_")) return; // skip Next.js internals
984
+ if (/^\(.*\)$/.test(file)) {
985
+ // route groups like (marketing) — don't add to URL
986
+ walkDir(fullPath, routeSegments);
987
+ return;
988
+ }
989
+ const segment = convertNextJsSegment(file);
990
+ if (!segment) return;
991
+ walkDir(fullPath, routeSegments.concat(segment));
992
+ return;
993
+ }
994
+
995
+ if (!/\.(js|ts|jsx|tsx|vue|svelte)$/.test(file)) return;
996
+ if (file.startsWith("_")) return;
997
+
998
+ const routeName = file.replace(/\.(js|ts|jsx|tsx|vue|svelte)$/, "");
999
+
1000
+ // Skip non-page files
1001
+ if (
1002
+ ["layout", "loading", "error", "template", "not-found"].includes(
1003
+ routeName,
1004
+ )
1005
+ )
1006
+ return;
1007
+
1008
+ // SvelteKit uses +page.svelte — skip +layout, +error etc.
1009
+ if (routeName.startsWith("+") && routeName !== "+page") return;
1010
+
1011
+ // Remix uses dots as path separators — e.g. $projectId.dashboard.tsx → /:projectId/dashboard
1012
+ const isRemixRoute =
1013
+ routeName.includes(".") && !routeName.startsWith("+");
1014
+ let finalSegments;
1015
+
1016
+ if (isRemixRoute) {
1017
+ // Split by dots and convert each segment
1018
+ const remixSegments = routeName
1019
+ .split(".")
1020
+ .map((s) => convertNextJsSegment(s) || s);
1021
+ finalSegments = routeSegments.concat(remixSegments);
1022
+ } else if (
1023
+ routeName === "index" ||
1024
+ routeName === "page" ||
1025
+ routeName === "+page"
1026
+ ) {
1027
+ finalSegments = routeSegments;
1028
+ } else {
1029
+ finalSegments = routeSegments.concat(
1030
+ convertNextJsSegment(routeName) || routeName,
1031
+ );
1032
+ }
1033
+
1034
+ const pattern = "/" + finalSegments.filter(Boolean).join("/");
1035
+ if (seen.has(pattern)) return;
1036
+ seen.add(pattern);
1037
+
1038
+ const paramMap = extractParamPositions(finalSegments);
1039
+ if (Object.keys(paramMap).length === 0) return; // no dynamic params, skip static routes
1040
+
1041
+ patterns.push({ pattern, params: paramMap });
1042
+ });
1043
+ }
1044
+
1045
+ dirsToScan.forEach(function (dir) {
1046
+ if (fs.existsSync(dir)) walkDir(dir, []);
1047
+ });
1048
+
1049
+ // Also scan config-based frameworks (React Router, Vue Router, Angular)
1050
+ const configPatterns = scanConfigBasedRoutes(cwd);
1051
+ configPatterns.forEach(function (p) {
1052
+ if (!seen.has(p.pattern)) {
1053
+ seen.add(p.pattern);
1054
+ patterns.push(p);
1055
+ }
1056
+ });
1057
+
1058
+ return patterns;
1059
+ }
1060
+
358
1061
  module.exports = {
359
1062
  scanExpressRoutes,
360
1063
  scanNextJsRoutes,
1064
+ scanNextJsAppRoutes,
361
1065
  extractPathParams,
1066
+ scanFrontendRoutes,
362
1067
  };