codedev-mcp 3.2.2 → 3.2.5

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.
@@ -100,24 +100,172 @@ function parseGraphQL(content, file) {
100
100
  }
101
101
  /**
102
102
  * Parse Express/Fastify route definitions.
103
+ * Supports multiple Express patterns:
104
+ * - router.get('/path', handler)
105
+ * - app.post('/path', handler)
106
+ * - router.route('/path').get(handler).post(handler)
107
+ * - express.Router().get('/path', handler)
108
+ * - Routes with variables: router.get(pathVar, handler)
109
+ * - Routes with template literals: router.get(`/api/${version}/users`, handler)
103
110
  * @param content - The file content to parse.
104
111
  * @param file - The file path.
105
112
  * @returns Parsed API endpoints.
106
113
  */
107
114
  function parseExpressRoutes(content, file) {
108
115
  const endpoints = [];
109
- const routeRegex = /(?:app|router)\.(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
116
+ // Pattern 1: Named Router variables with routes
117
+ // Matches: export const studentSectorPriorityRoute = express.Router();
118
+ // studentSectorPriorityRoute.get("/student/:studentId", handler);
119
+ // Also matches: const router = express.Router(); router.get(...)
120
+ // First, find all Router() variable declarations (more flexible - not just *Route*)
121
+ const routerVarRegex = /(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*express\.Router\(\)/gi;
122
+ const routerVars = new Map();
123
+ let routerMatch;
124
+ while ((routerMatch = routerVarRegex.exec(content)) !== null) {
125
+ routerVars.set(routerMatch[1], routerMatch.index);
126
+ }
127
+ // Now find routes using these router variables
128
+ for (const [routerVar] of routerVars) {
129
+ // Escape special regex characters in routerVar name
130
+ const escapedVar = routerVar.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
131
+ const routerVarRegex2 = new RegExp(`${escapedVar}\\.(get|post|put|delete|patch|all|use)\\s*\\(\\s*['"\`]([^'"\`]+)['"\`]`, 'gi');
132
+ let routeMatch;
133
+ while ((routeMatch = routerVarRegex2.exec(content)) !== null) {
134
+ const line = content.substring(0, routeMatch.index).split('\n').length;
135
+ const method = routeMatch[1].toUpperCase();
136
+ const path = routeMatch[2];
137
+ if (method === 'USE' && !path.match(/^\/[^/]/))
138
+ continue;
139
+ endpoints.push({
140
+ method: method === 'ALL' ? 'ANY' : method,
141
+ path: path,
142
+ file,
143
+ line,
144
+ source: 'express',
145
+ });
146
+ }
147
+ }
148
+ // Pattern 1b: Standard router.get/post/put/delete/patch('/path', ...)
149
+ // Matches: router.get('/api/users', handler) or app.post('/api/users', handler)
150
+ // Also matches: router.get("/api/users", handler) with double quotes
151
+ // Also matches: router.get(`/api/users`, handler) with template literals
152
+ // Also matches: const router = express.Router(); router.get(...)
153
+ const standardRouteRegex = /(?:app|router|express\.Router\(\)|express\(\))\s*\.(get|post|put|delete|patch|all|use)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
110
154
  let match;
111
- while ((match = routeRegex.exec(content)) !== null) {
155
+ while ((match = standardRouteRegex.exec(content)) !== null) {
156
+ const line = content.substring(0, match.index).split('\n').length;
157
+ const method = match[1].toUpperCase();
158
+ const path = match[2];
159
+ // Skip 'use' and 'all' methods unless they have specific paths
160
+ if (method === 'USE' && !path.match(/^\/[^/]/))
161
+ continue;
162
+ endpoints.push({
163
+ method: method === 'ALL' ? 'ANY' : method,
164
+ path: path,
165
+ file,
166
+ line,
167
+ source: 'express',
168
+ });
169
+ }
170
+ // Pattern 2: router.route('/path').get(...).post(...)
171
+ const routeChainRegex = /(?:app|router)\.route\s*\(\s*['"`]([^'"`]+)['"`]\s*\)\s*\.(get|post|put|delete|patch)\s*\(/gi;
172
+ while ((match = routeChainRegex.exec(content)) !== null) {
173
+ const line = content.substring(0, match.index).split('\n').length;
174
+ endpoints.push({
175
+ method: match[2].toUpperCase(),
176
+ path: match[1],
177
+ file,
178
+ line,
179
+ source: 'express',
180
+ });
181
+ }
182
+ // Pattern 3: Routes with variables (router.get(pathVar, handler))
183
+ // Try to find path variables defined earlier in the file
184
+ const pathVarRegex = /(?:const|let|var)\s+(\w+Path)\s*=\s*['"`]([^'"`]+)['"`]/g;
185
+ const pathVars = new Map();
186
+ let pathMatch;
187
+ while ((pathMatch = pathVarRegex.exec(content)) !== null) {
188
+ pathVars.set(pathMatch[1], pathMatch[2]);
189
+ }
190
+ // Pattern 4: Routes using path variables
191
+ const varRouteRegex = /(?:app|router)\.(get|post|put|delete|patch)\s*\(\s*(\w+Path)/gi;
192
+ while ((match = varRouteRegex.exec(content)) !== null) {
193
+ const pathVar = match[2];
194
+ const pathValue = pathVars.get(pathVar);
195
+ if (pathValue) {
196
+ const line = content.substring(0, match.index).split('\n').length;
197
+ endpoints.push({
198
+ method: match[1].toUpperCase(),
199
+ path: pathValue,
200
+ file,
201
+ line,
202
+ source: 'express',
203
+ });
204
+ }
205
+ }
206
+ // Pattern 5: Template literal routes: router.get(`/api/${version}/users`, ...)
207
+ const templateRouteRegex = /(?:app|router)\.(get|post|put|delete|patch)\s*\(\s*`([^`]+)`/gi;
208
+ while ((match = templateRouteRegex.exec(content)) !== null) {
112
209
  const line = content.substring(0, match.index).split('\n').length;
210
+ // Extract static parts of template literal (remove ${...} parts)
211
+ const path = match[2].replace(/\$\{[^}]+\}/g, '*');
113
212
  endpoints.push({
114
213
  method: match[1].toUpperCase(),
115
- path: match[2],
214
+ path: path,
116
215
  file,
117
216
  line,
118
217
  source: 'express',
119
218
  });
120
219
  }
220
+ // Pattern 6: Routes exported as arrays or objects
221
+ // Matches: export default [{ method: 'GET', path: '/api/users', handler }]
222
+ // Matches: export const routes = [{ method: 'GET', path: '/api/users' }]
223
+ const exportedRoutesRegex = /export\s+(?:default\s+)?(?:const|let|var)?\s*\w*\s*=\s*\[([\s\S]*?)\]/g;
224
+ let exportedMatch;
225
+ while ((exportedMatch = exportedRoutesRegex.exec(content)) !== null) {
226
+ const routesArray = exportedMatch[1];
227
+ // Try to extract route objects from the array
228
+ const routeObjRegex = /\{\s*(?:method|path|route|url)\s*:\s*['"`]([^'"`]+)['"`]\s*,\s*(?:method|path|route|url)\s*:\s*['"`]([^'"`]+)['"`]/gi;
229
+ let routeObjMatch;
230
+ while ((routeObjMatch = routeObjRegex.exec(routesArray)) !== null) {
231
+ const line = content.substring(0, exportedMatch.index).split('\n').length;
232
+ // Determine which is method and which is path
233
+ const first = routeObjMatch[1];
234
+ const second = routeObjMatch[2];
235
+ const method = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].includes(first.toUpperCase())
236
+ ? first.toUpperCase()
237
+ : second.toUpperCase();
238
+ const path = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].includes(first.toUpperCase()) ? second : first;
239
+ if (method && path && path.startsWith('/')) {
240
+ endpoints.push({ method, path, file, line, source: 'express' });
241
+ }
242
+ }
243
+ }
244
+ // Pattern 7: Express Router instances: const router = express.Router(); router.get(...)
245
+ // This is already covered by Pattern 1, but let's also check for mounted routers
246
+ const mountedRouterRegex = /(?:app|router)\.use\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*(\w+Router|\w+Routes)/gi;
247
+ while ((match = mountedRouterRegex.exec(content)) !== null) {
248
+ const basePath = match[1];
249
+ const routerName = match[2];
250
+ // Try to find routes in the router definition
251
+ const routerDefRegex = new RegExp(`(?:const|let|var)\\s+${routerName}\\s*=\\s*express\\.Router\\(\\)[\\s\\S]*?`, 'i');
252
+ const routerDef = content.match(routerDefRegex);
253
+ if (routerDef) {
254
+ const routerContent = routerDef[0];
255
+ const routerRouteRegex = new RegExp(`(?:router|${routerName})\\.(get|post|put|delete|patch)\\s*\\(\\s*['"\`]([^'"\`]+)['"\`]`, 'gi');
256
+ const routerRoutes = routerContent.matchAll(routerRouteRegex);
257
+ for (const routeMatch of routerRoutes) {
258
+ const line = content.substring(0, match.index).split('\n').length;
259
+ endpoints.push({
260
+ method: routeMatch[1].toUpperCase(),
261
+ path: `${basePath}${routeMatch[2]}`.replace(/\/+/g, '/'),
262
+ file,
263
+ line,
264
+ source: 'express',
265
+ });
266
+ }
267
+ }
268
+ }
121
269
  return endpoints;
122
270
  }
123
271
  /**
@@ -180,6 +328,503 @@ function parseNestJSRoutes(content, file) {
180
328
  }
181
329
  return endpoints;
182
330
  }
331
+ /**
332
+ * Parse Flask route decorators.
333
+ * @param content - The file content to parse.
334
+ * @param file - The file path.
335
+ * @returns Parsed API endpoints.
336
+ */
337
+ function parseFlaskRoutes(content, file) {
338
+ const endpoints = [];
339
+ const routeRegex = /@(?:app|blueprint|router)\.(route|get|post|put|delete|patch)\s*\(\s*['"]([^'"]+)['"]/gi;
340
+ let match;
341
+ while ((match = routeRegex.exec(content)) !== null) {
342
+ const line = content.substring(0, match.index).split('\n').length;
343
+ const method = match[1].toUpperCase() === 'ROUTE' ? 'GET' : match[1].toUpperCase();
344
+ const path = match[2];
345
+ endpoints.push({
346
+ method,
347
+ path,
348
+ file,
349
+ line,
350
+ source: 'flask',
351
+ });
352
+ }
353
+ return endpoints;
354
+ }
355
+ /**
356
+ * Parse Django REST Framework viewsets and views.
357
+ * @param content - The file content to parse.
358
+ * @param file - The file path.
359
+ * @returns Parsed API endpoints.
360
+ */
361
+ function parseDjangoRoutes(content, file) {
362
+ const endpoints = [];
363
+ // Django REST Framework ViewSet with router
364
+ if (/class\s+\w+ViewSet/.test(content) || /from\s+rest_framework/.test(content)) {
365
+ const viewsetMatch = content.match(/class\s+(\w+ViewSet)/);
366
+ if (viewsetMatch) {
367
+ // Common ViewSet actions
368
+ const actions = ['list', 'create', 'retrieve', 'update', 'partial_update', 'destroy'];
369
+ for (const action of actions) {
370
+ if (new RegExp(`def\\s+${action}`).test(content)) {
371
+ endpoints.push({
372
+ method: action === 'list' || action === 'retrieve'
373
+ ? 'GET'
374
+ : action === 'create'
375
+ ? 'POST'
376
+ : action === 'destroy'
377
+ ? 'DELETE'
378
+ : 'PUT',
379
+ path: `/${action}`,
380
+ file,
381
+ line: 0,
382
+ source: 'django',
383
+ });
384
+ }
385
+ }
386
+ }
387
+ }
388
+ // Django function-based views with decorators
389
+ const decoratorRegex = /@(?:api_view|action)\s*\([^)]*\)\s*(?:@\w+\s*\([^)]*\)\s*)*def\s+(\w+)\s*\(/gi;
390
+ let match;
391
+ while ((match = decoratorRegex.exec(content)) !== null) {
392
+ const line = content.substring(0, match.index).split('\n').length;
393
+ endpoints.push({
394
+ method: 'GET',
395
+ path: `/${match[1]}`,
396
+ file,
397
+ line,
398
+ source: 'django',
399
+ });
400
+ }
401
+ return endpoints;
402
+ }
403
+ /**
404
+ * Parse Rails routes.rb file.
405
+ * @param content - The file content to parse.
406
+ * @param file - The file path.
407
+ * @returns Parsed API endpoints.
408
+ */
409
+ function parseRailsRoutes(content, file) {
410
+ const endpoints = [];
411
+ // Rails route syntax: get '/users', to: 'users#index'
412
+ const routeRegex = /(get|post|put|patch|delete|resources?)\s+['"]([^'"]+)['"]/gi;
413
+ let match;
414
+ while ((match = routeRegex.exec(content)) !== null) {
415
+ const line = content.substring(0, match.index).split('\n').length;
416
+ const method = match[1].toUpperCase();
417
+ const path = match[2];
418
+ if (method === 'RESOURCES' || method === 'RESOURCE') {
419
+ // RESTful resource routes
420
+ const resourceName = path.replace(/^\//, '').replace(/\/$/, '');
421
+ endpoints.push({ method: 'GET', path: `/${resourceName}`, file, line, source: 'rails' }, { method: 'POST', path: `/${resourceName}`, file, line, source: 'rails' }, { method: 'GET', path: `/${resourceName}/:id`, file, line, source: 'rails' }, { method: 'PUT', path: `/${resourceName}/:id`, file, line, source: 'rails' }, { method: 'DELETE', path: `/${resourceName}/:id`, file, line, source: 'rails' });
422
+ }
423
+ else {
424
+ endpoints.push({
425
+ method: method === 'PATCH' ? 'PUT' : method,
426
+ path,
427
+ file,
428
+ line,
429
+ source: 'rails',
430
+ });
431
+ }
432
+ }
433
+ return endpoints;
434
+ }
435
+ /**
436
+ * Parse Sinatra routes.
437
+ * @param content - The file content to parse.
438
+ * @param file - The file path.
439
+ * @returns Parsed API endpoints.
440
+ */
441
+ function parseSinatraRoutes(content, file) {
442
+ const endpoints = [];
443
+ const routeRegex = /(get|post|put|delete|patch)\s+['"]([^'"]+)['"]/gi;
444
+ let match;
445
+ while ((match = routeRegex.exec(content)) !== null) {
446
+ const line = content.substring(0, match.index).split('\n').length;
447
+ endpoints.push({
448
+ method: match[1].toUpperCase(),
449
+ path: match[2],
450
+ file,
451
+ line,
452
+ source: 'sinatra',
453
+ });
454
+ }
455
+ return endpoints;
456
+ }
457
+ /**
458
+ * Parse Laravel routes.
459
+ * @param content - The file content to parse.
460
+ * @param file - The file path.
461
+ * @returns Parsed API endpoints.
462
+ */
463
+ function parseLaravelRoutes(content, file) {
464
+ const endpoints = [];
465
+ // Laravel route syntax: Route::get('/users', [UserController::class, 'index']);
466
+ const routeRegex = /Route::(get|post|put|delete|patch|any|match)\s*\(\s*['"]([^'"]+)['"]/gi;
467
+ let match;
468
+ while ((match = routeRegex.exec(content)) !== null) {
469
+ const line = content.substring(0, match.index).split('\n').length;
470
+ const method = match[1].toUpperCase();
471
+ endpoints.push({
472
+ method: method === 'ANY' || method === 'MATCH' ? 'ANY' : method,
473
+ path: match[2],
474
+ file,
475
+ line,
476
+ source: 'laravel',
477
+ });
478
+ }
479
+ // Laravel resource routes: Route::resource('users', UserController::class);
480
+ const resourceRegex = /Route::resource\s*\(\s*['"]([^'"]+)['"]/gi;
481
+ while ((match = resourceRegex.exec(content)) !== null) {
482
+ const line = content.substring(0, match.index).split('\n').length;
483
+ const resourceName = match[1];
484
+ endpoints.push({ method: 'GET', path: `/${resourceName}`, file, line, source: 'laravel' }, { method: 'POST', path: `/${resourceName}`, file, line, source: 'laravel' }, { method: 'GET', path: `/${resourceName}/{id}`, file, line, source: 'laravel' }, { method: 'PUT', path: `/${resourceName}/{id}`, file, line, source: 'laravel' }, { method: 'DELETE', path: `/${resourceName}/{id}`, file, line, source: 'laravel' });
485
+ }
486
+ return endpoints;
487
+ }
488
+ /**
489
+ * Parse Spring Boot @RequestMapping annotations.
490
+ * @param content - The file content to parse.
491
+ * @param file - The file path.
492
+ * @returns Parsed API endpoints.
493
+ */
494
+ function parseSpringRoutes(content, file) {
495
+ const endpoints = [];
496
+ if (!/@(?:RestController|Controller)/.test(content))
497
+ return endpoints;
498
+ const classPath = content.match(/@(?:RequestMapping|GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping)\s*\(\s*value\s*=\s*['"]([^'"]+)['"]/)?.[1] ||
499
+ content.match(/@RequestMapping\s*\(\s*['"]([^'"]+)['"]/)?.[1] ||
500
+ '';
501
+ const methodRegex = /@(?:GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping|RequestMapping)\s*\(\s*(?:value\s*=\s*)?['"]([^'"]*)['"]/gi;
502
+ let match;
503
+ while ((match = methodRegex.exec(content)) !== null) {
504
+ const line = content.substring(0, match.index).split('\n').length;
505
+ const methodPath = match[1];
506
+ const fullAnnotation = content.substring(match.index, content.indexOf(')', match.index) + 1);
507
+ let method = 'GET';
508
+ if (/GetMapping/.test(fullAnnotation))
509
+ method = 'GET';
510
+ else if (/PostMapping/.test(fullAnnotation))
511
+ method = 'POST';
512
+ else if (/PutMapping/.test(fullAnnotation))
513
+ method = 'PUT';
514
+ else if (/DeleteMapping/.test(fullAnnotation))
515
+ method = 'DELETE';
516
+ else if (/PatchMapping/.test(fullAnnotation))
517
+ method = 'PATCH';
518
+ else if (/RequestMapping/.test(fullAnnotation)) {
519
+ const methodMatch = fullAnnotation.match(/method\s*=\s*RequestMethod\.(\w+)/);
520
+ if (methodMatch)
521
+ method = methodMatch[1].toUpperCase();
522
+ }
523
+ endpoints.push({
524
+ method,
525
+ path: `/${classPath}/${methodPath}`.replace(/\/+/g, '/'),
526
+ file,
527
+ line,
528
+ source: 'spring',
529
+ });
530
+ }
531
+ return endpoints;
532
+ }
533
+ /**
534
+ * Parse JAX-RS annotations.
535
+ * @param content - The file content to parse.
536
+ * @param file - The file path.
537
+ * @returns Parsed API endpoints.
538
+ */
539
+ function parseJAXRSRoutes(content, file) {
540
+ const endpoints = [];
541
+ if (!/@Path/.test(content))
542
+ return endpoints;
543
+ const classPath = content.match(/@Path\s*\(\s*['"]([^'"]+)['"]/)?.[1] || '';
544
+ const methodRegex = /@(GET|POST|PUT|DELETE|PATCH|Path)\s*\(\s*(?:value\s*=\s*)?['"]([^'"]*)['"]/gi;
545
+ let match;
546
+ while ((match = methodRegex.exec(content)) !== null) {
547
+ const line = content.substring(0, match.index).split('\n').length;
548
+ const annotation = match[1];
549
+ const path = match[2] || '';
550
+ let method = 'GET';
551
+ if (annotation === 'GET')
552
+ method = 'GET';
553
+ else if (annotation === 'POST')
554
+ method = 'POST';
555
+ else if (annotation === 'PUT')
556
+ method = 'PUT';
557
+ else if (annotation === 'DELETE')
558
+ method = 'DELETE';
559
+ else if (annotation === 'PATCH')
560
+ method = 'PATCH';
561
+ endpoints.push({
562
+ method,
563
+ path: `/${classPath}/${path}`.replace(/\/+/g, '/'),
564
+ file,
565
+ line,
566
+ source: 'jaxrs',
567
+ });
568
+ }
569
+ return endpoints;
570
+ }
571
+ /**
572
+ * Parse Go Gin routes.
573
+ * @param content - The file content to parse.
574
+ * @param file - The file path.
575
+ * @returns Parsed API endpoints.
576
+ */
577
+ function parseGinRoutes(content, file) {
578
+ const endpoints = [];
579
+ const routeRegex = /(?:router|r|engine)\.(GET|POST|PUT|DELETE|PATCH|Any)\s*\(\s*['"]([^'"]+)['"]/gi;
580
+ let match;
581
+ while ((match = routeRegex.exec(content)) !== null) {
582
+ const line = content.substring(0, match.index).split('\n').length;
583
+ endpoints.push({
584
+ method: match[1] === 'Any' ? 'ANY' : match[1],
585
+ path: match[2],
586
+ file,
587
+ line,
588
+ source: 'gin',
589
+ });
590
+ }
591
+ return endpoints;
592
+ }
593
+ /**
594
+ * Parse Go Echo routes.
595
+ * @param content - The file content to parse.
596
+ * @param file - The file path.
597
+ * @returns Parsed API endpoints.
598
+ */
599
+ function parseEchoRoutes(content, file) {
600
+ const endpoints = [];
601
+ const routeRegex = /(?:e|app|router)\.(GET|POST|PUT|DELETE|PATCH|Any)\s*\(\s*['"]([^'"]+)['"]/gi;
602
+ let match;
603
+ while ((match = routeRegex.exec(content)) !== null) {
604
+ const line = content.substring(0, match.index).split('\n').length;
605
+ endpoints.push({
606
+ method: match[1] === 'Any' ? 'ANY' : match[1],
607
+ path: match[2],
608
+ file,
609
+ line,
610
+ source: 'echo',
611
+ });
612
+ }
613
+ return endpoints;
614
+ }
615
+ /**
616
+ * Parse Go Fiber routes.
617
+ * @param content - The file content to parse.
618
+ * @param file - The file path.
619
+ * @returns Parsed API endpoints.
620
+ */
621
+ function parseFiberRoutes(content, file) {
622
+ const endpoints = [];
623
+ const routeRegex = /(?:app|router)\.(Get|Post|Put|Delete|Patch|All)\s*\(\s*['"]([^'"]+)['"]/gi;
624
+ let match;
625
+ while ((match = routeRegex.exec(content)) !== null) {
626
+ const line = content.substring(0, match.index).split('\n').length;
627
+ endpoints.push({
628
+ method: match[1] === 'All' ? 'ANY' : match[1].toUpperCase(),
629
+ path: match[2],
630
+ file,
631
+ line,
632
+ source: 'fiber',
633
+ });
634
+ }
635
+ return endpoints;
636
+ }
637
+ /**
638
+ * Parse Go Chi routes.
639
+ * @param content - The file content to parse.
640
+ * @param file - The file path.
641
+ * @returns Parsed API endpoints.
642
+ */
643
+ function parseChiRoutes(content, file) {
644
+ const endpoints = [];
645
+ const routeRegex = /(?:r|router|mux)\.(Get|Post|Put|Delete|Patch|Method)\s*\(\s*['"]([^'"]+)['"]/gi;
646
+ let match;
647
+ while ((match = routeRegex.exec(content)) !== null) {
648
+ const line = content.substring(0, match.index).split('\n').length;
649
+ endpoints.push({
650
+ method: match[1] === 'Method' ? 'ANY' : match[1].toUpperCase(),
651
+ path: match[2],
652
+ file,
653
+ line,
654
+ source: 'chi',
655
+ });
656
+ }
657
+ return endpoints;
658
+ }
659
+ /**
660
+ * Parse Rust Actix-web routes.
661
+ * @param content - The file content to parse.
662
+ * @param file - The file path.
663
+ * @returns Parsed API endpoints.
664
+ */
665
+ function parseActixRoutes(content, file) {
666
+ const endpoints = [];
667
+ const routeRegex = /\.(route|get|post|put|delete)\s*\(\s*['"]([^'"]+)['"]/gi;
668
+ let match;
669
+ while ((match = routeRegex.exec(content)) !== null) {
670
+ const line = content.substring(0, match.index).split('\n').length;
671
+ const method = match[1].toUpperCase() === 'ROUTE' ? 'GET' : match[1].toUpperCase();
672
+ endpoints.push({
673
+ method,
674
+ path: match[2],
675
+ file,
676
+ line,
677
+ source: 'actix',
678
+ });
679
+ }
680
+ return endpoints;
681
+ }
682
+ /**
683
+ * Parse Rust Rocket routes.
684
+ * @param content - The file content to parse.
685
+ * @param file - The file path.
686
+ * @returns Parsed API endpoints.
687
+ */
688
+ function parseRocketRoutes(content, file) {
689
+ const endpoints = [];
690
+ const routeRegex = /#\[(get|post|put|delete|patch|head|options)\s*\(['"]([^'"]+)['"]/gi;
691
+ let match;
692
+ while ((match = routeRegex.exec(content)) !== null) {
693
+ const line = content.substring(0, match.index).split('\n').length;
694
+ endpoints.push({
695
+ method: match[1].toUpperCase(),
696
+ path: match[2],
697
+ file,
698
+ line,
699
+ source: 'rocket',
700
+ });
701
+ }
702
+ return endpoints;
703
+ }
704
+ /**
705
+ * Parse Rust Axum routes.
706
+ * @param content - The file content to parse.
707
+ * @param file - The file path.
708
+ * @returns Parsed API endpoints.
709
+ */
710
+ function parseAxumRoutes(content, file) {
711
+ const endpoints = [];
712
+ const routeRegex = /\.(route|get|post|put|delete|patch)\s*\(\s*['"]([^'"]+)['"]/gi;
713
+ let match;
714
+ while ((match = routeRegex.exec(content)) !== null) {
715
+ const line = content.substring(0, match.index).split('\n').length;
716
+ const method = match[1].toUpperCase() === 'ROUTE' ? 'GET' : match[1].toUpperCase();
717
+ endpoints.push({
718
+ method,
719
+ path: match[2],
720
+ file,
721
+ line,
722
+ source: 'axum',
723
+ });
724
+ }
725
+ return endpoints;
726
+ }
727
+ /**
728
+ * Parse ASP.NET Core controllers.
729
+ * @param content - The file content to parse.
730
+ * @param file - The file path.
731
+ * @returns Parsed API endpoints.
732
+ */
733
+ function parseAspNetRoutes(content, file) {
734
+ const endpoints = [];
735
+ if (!/\[ApiController\]/.test(content) && !/class\s+\w+Controller/.test(content))
736
+ return endpoints;
737
+ const routePrefix = content.match(/\[Route\s*\(\s*['"]([^'"]+)['"]/)?.[1] || '';
738
+ const methodRegex = /\[(HttpGet|HttpPost|HttpPut|HttpDelete|HttpPatch)\s*(?:\(\s*['"]([^'"]*)['"]\s*)?\)\]/gi;
739
+ let match;
740
+ while ((match = methodRegex.exec(content)) !== null) {
741
+ const line = content.substring(0, match.index).split('\n').length;
742
+ const method = match[1].replace('Http', '').toUpperCase();
743
+ const path = match[2] || '';
744
+ endpoints.push({
745
+ method,
746
+ path: `/${routePrefix}/${path}`.replace(/\/+/g, '/'),
747
+ file,
748
+ line,
749
+ source: 'aspnet',
750
+ });
751
+ }
752
+ return endpoints;
753
+ }
754
+ /**
755
+ * Parse Koa routes.
756
+ * @param content - The file content to parse.
757
+ * @param file - The file path.
758
+ * @returns Parsed API endpoints.
759
+ */
760
+ function parseKoaRoutes(content, file) {
761
+ const endpoints = [];
762
+ const routeRegex = /(?:router|app)\.(get|post|put|delete|patch|all)\s*\(\s*['"]([^'"]+)['"]/gi;
763
+ let match;
764
+ while ((match = routeRegex.exec(content)) !== null) {
765
+ const line = content.substring(0, match.index).split('\n').length;
766
+ endpoints.push({
767
+ method: match[1].toUpperCase() === 'ALL' ? 'ANY' : match[1].toUpperCase(),
768
+ path: match[2],
769
+ file,
770
+ line,
771
+ source: 'koa',
772
+ });
773
+ }
774
+ return endpoints;
775
+ }
776
+ /**
777
+ * Parse Fastify routes.
778
+ * @param content - The file content to parse.
779
+ * @param file - The file path.
780
+ * @returns Parsed API endpoints.
781
+ */
782
+ function parseFastifyRoutes(content, file) {
783
+ const endpoints = [];
784
+ const routeRegex = /(?:fastify|app)\.(get|post|put|delete|patch)\s*\(\s*['"]([^'"]+)['"]/gi;
785
+ let match;
786
+ while ((match = routeRegex.exec(content)) !== null) {
787
+ const line = content.substring(0, match.index).split('\n').length;
788
+ endpoints.push({
789
+ method: match[1].toUpperCase(),
790
+ path: match[2],
791
+ file,
792
+ line,
793
+ source: 'fastify',
794
+ });
795
+ }
796
+ return endpoints;
797
+ }
798
+ /**
799
+ * Parse Hapi routes.
800
+ * @param content - The file content to parse.
801
+ * @param file - The file path.
802
+ * @returns Parsed API endpoints.
803
+ */
804
+ function parseHapiRoutes(content, file) {
805
+ const endpoints = [];
806
+ const routeRegex = /(?:method|path):\s*['"](GET|POST|PUT|DELETE|PATCH|get|post|put|delete|patch)['"]|path:\s*['"]([^'"]+)['"]/gi;
807
+ let match;
808
+ let currentMethod = 'GET';
809
+ let currentPath = '';
810
+ while ((match = routeRegex.exec(content)) !== null) {
811
+ const line = content.substring(0, match.index).split('\n').length;
812
+ if (match[1]) {
813
+ currentMethod = match[1].toUpperCase();
814
+ }
815
+ else if (match[2]) {
816
+ currentPath = match[2];
817
+ endpoints.push({
818
+ method: currentMethod,
819
+ path: currentPath,
820
+ file,
821
+ line,
822
+ source: 'hapi',
823
+ });
824
+ }
825
+ }
826
+ return endpoints;
827
+ }
183
828
  /**
184
829
  * Main API contract analysis function.
185
830
  * @param cwd - The working directory to scan.
@@ -194,6 +839,12 @@ export async function analyzeApiContracts(cwd) {
194
839
  const gqlFiles = await listFiles(cwd, { glob: '**/*.{graphql,gql}' });
195
840
  const tsFiles = await listFiles(cwd, { glob: '**/*.{ts,tsx,js,jsx}' });
196
841
  const pyFiles = await listFiles(cwd, { glob: '**/*.py' });
842
+ const rbFiles = await listFiles(cwd, { glob: '**/*.rb' });
843
+ const phpFiles = await listFiles(cwd, { glob: '**/*.php' });
844
+ const javaFiles = await listFiles(cwd, { glob: '**/*.java' });
845
+ const goFiles = await listFiles(cwd, { glob: '**/*.go' });
846
+ const rsFiles = await listFiles(cwd, { glob: '**/*.rs' });
847
+ const csFiles = await listFiles(cwd, { glob: '**/*.cs' });
197
848
  // OpenAPI specs
198
849
  for (const f of jsonFiles.filter((f) => /swagger|openapi/i.test(f)).slice(0, 10)) {
199
850
  try {
@@ -205,8 +856,8 @@ export async function analyzeApiContracts(cwd) {
205
856
  sources.add('openapi');
206
857
  }
207
858
  }
208
- catch (error) {
209
- logger.debug(`Failed to parse possible OpenAPI spec: ${f}`, { error });
859
+ catch {
860
+ // Skip invalid OpenAPI specs
210
861
  }
211
862
  }
212
863
  // GraphQL schemas
@@ -220,15 +871,25 @@ export async function analyzeApiContracts(cwd) {
220
871
  sources.add('graphql');
221
872
  }
222
873
  }
223
- catch (error) {
224
- logger.debug(`Failed to parse possible OpenAPI spec: ${f}`, { error });
874
+ catch {
875
+ // Skip invalid OpenAPI specs
225
876
  }
226
877
  }
227
- // Express/NestJS routes
228
- for (const f of tsFiles.slice(0, 300)) {
878
+ // Express/NestJS routes - prioritize route files
879
+ const routeFiles = tsFiles.filter((f) => /routes?|controllers?|api|endpoints?/i.test(f) || /\.route\.(ts|js)$/i.test(f) || /_routes?\.(ts|js)$/i.test(f));
880
+ const otherTsFiles = tsFiles.filter((f) => !routeFiles.includes(f));
881
+ // Check route files first (more likely to contain routes)
882
+ // Increased limit from 500 to 2000 to handle large projects
883
+ for (const f of [...routeFiles, ...otherTsFiles].slice(0, 2000)) {
229
884
  try {
230
885
  const content = await readFile(path.join(cwd, f), 'utf-8');
231
- if (/(?:app|router)\.(get|post|put|delete|patch)\s*\(/i.test(content)) {
886
+ // Enhanced Express detection - check for multiple patterns
887
+ // Also check for named Router variables (e.g., export const studentSectorPriorityRoute = express.Router())
888
+ if (/(?:app|router|express\.Router)\.(get|post|put|delete|patch|all|use|route)\s*\(/i.test(content) ||
889
+ /express\.Router\(\)/i.test(content) ||
890
+ /from\s+['"]express['"]/i.test(content) ||
891
+ /require\s*\(['"]express['"]\)/i.test(content) ||
892
+ /(?:export\s+)?(?:const|let|var)\s+\w+\s*=\s*express\.Router\(\)/i.test(content)) {
232
893
  const eps = parseExpressRoutes(content, f);
233
894
  if (eps.length > 0) {
234
895
  allEndpoints.push(...eps);
@@ -236,6 +897,17 @@ export async function analyzeApiContracts(cwd) {
236
897
  sources.add('express');
237
898
  }
238
899
  }
900
+ // Also check for aggregator files with route arrays
901
+ // Matches: const defaultRoutes = [{ path: '/jobDescriptions', route: jobDescRoutes }]
902
+ if (/const\s+\w+Routes\s*=\s*\[[\s\S]*?\{[\s\S]*?path:[\s\S]*?route:/i.test(content)) {
903
+ const eps = parseExpressRoutes(content, f);
904
+ if (eps.length > 0) {
905
+ allEndpoints.push(...eps);
906
+ specFiles.push(f);
907
+ sources.add('express');
908
+ }
909
+ }
910
+ // NestJS detection
239
911
  if (/@Controller/.test(content)) {
240
912
  const eps = parseNestJSRoutes(content, f);
241
913
  if (eps.length > 0) {
@@ -244,16 +916,45 @@ export async function analyzeApiContracts(cwd) {
244
916
  sources.add('nestjs');
245
917
  }
246
918
  }
919
+ // Koa detection
920
+ if (/from\s+['"]koa['"]|require\s*\(['"]koa['"]/.test(content) || /router\.(get|post|put|delete)/.test(content)) {
921
+ const eps = parseKoaRoutes(content, f);
922
+ if (eps.length > 0) {
923
+ allEndpoints.push(...eps);
924
+ specFiles.push(f);
925
+ sources.add('koa');
926
+ }
927
+ }
928
+ // Fastify detection
929
+ if (/from\s+['"]fastify['"]|require\s*\(['"]fastify['"]/.test(content) ||
930
+ /fastify\.(get|post|put|delete)/.test(content)) {
931
+ const eps = parseFastifyRoutes(content, f);
932
+ if (eps.length > 0) {
933
+ allEndpoints.push(...eps);
934
+ specFiles.push(f);
935
+ sources.add('fastify');
936
+ }
937
+ }
938
+ // Hapi detection
939
+ if (/from\s+['"]@hapi\/hapi['"]|require\s*\(['"]@hapi\/hapi['"]/.test(content) || /server\.route/.test(content)) {
940
+ const eps = parseHapiRoutes(content, f);
941
+ if (eps.length > 0) {
942
+ allEndpoints.push(...eps);
943
+ specFiles.push(f);
944
+ sources.add('hapi');
945
+ }
946
+ }
247
947
  }
248
948
  catch (error) {
249
- logger.debug(`Failed to parse possible OpenAPI spec: ${f}`, { error });
949
+ logger.debug(`Failed to parse possible route file: ${f}`, { error });
250
950
  }
251
951
  }
252
- // FastAPI routes
253
- for (const f of pyFiles.slice(0, 200)) {
952
+ // Python frameworks: FastAPI, Flask, Django
953
+ for (const f of pyFiles.slice(0, 300)) {
254
954
  try {
255
955
  const content = await readFile(path.join(cwd, f), 'utf-8');
256
- if (/@(?:app|router)\.(get|post|put|delete)/.test(content)) {
956
+ // FastAPI routes
957
+ if (/@(?:app|router)\.(get|post|put|delete)/.test(content) || /from\s+fastapi/.test(content)) {
257
958
  const eps = parseFastAPIRoutes(content, f);
258
959
  if (eps.length > 0) {
259
960
  allEndpoints.push(...eps);
@@ -261,9 +962,199 @@ export async function analyzeApiContracts(cwd) {
261
962
  sources.add('fastapi');
262
963
  }
263
964
  }
965
+ // Flask routes
966
+ if (/@(?:app|blueprint|router)\.(route|get|post|put|delete|patch)/.test(content) ||
967
+ /from\s+flask/.test(content)) {
968
+ const eps = parseFlaskRoutes(content, f);
969
+ if (eps.length > 0) {
970
+ allEndpoints.push(...eps);
971
+ specFiles.push(f);
972
+ sources.add('flask');
973
+ }
974
+ }
975
+ // Django REST Framework
976
+ if (/from\s+rest_framework/.test(content) || /class\s+\w+ViewSet/.test(content)) {
977
+ const eps = parseDjangoRoutes(content, f);
978
+ if (eps.length > 0) {
979
+ allEndpoints.push(...eps);
980
+ specFiles.push(f);
981
+ sources.add('django');
982
+ }
983
+ }
984
+ }
985
+ catch (error) {
986
+ logger.debug(`Failed to parse possible Python route file: ${f}`, { error });
987
+ }
988
+ }
989
+ // Ruby frameworks: Rails, Sinatra
990
+ const railsRouteFiles = rbFiles.filter((f) => /routes\.rb|config\/routes/.test(f));
991
+ const otherRbFiles = rbFiles.filter((f) => !railsRouteFiles.includes(f));
992
+ for (const f of [...railsRouteFiles, ...otherRbFiles].slice(0, 100)) {
993
+ try {
994
+ const content = await readFile(path.join(cwd, f), 'utf-8');
995
+ // Rails routes
996
+ if (/Rails\.application\.routes\.draw|resources?|get\s+['"]/.test(content)) {
997
+ const eps = parseRailsRoutes(content, f);
998
+ if (eps.length > 0) {
999
+ allEndpoints.push(...eps);
1000
+ specFiles.push(f);
1001
+ sources.add('rails');
1002
+ }
1003
+ }
1004
+ // Sinatra routes
1005
+ if (/require\s+['"]sinatra['"]|class\s+\w+\s*<\s*Sinatra/.test(content)) {
1006
+ const eps = parseSinatraRoutes(content, f);
1007
+ if (eps.length > 0) {
1008
+ allEndpoints.push(...eps);
1009
+ specFiles.push(f);
1010
+ sources.add('sinatra');
1011
+ }
1012
+ }
1013
+ }
1014
+ catch (error) {
1015
+ logger.debug(`Failed to parse possible Ruby route file: ${f}`, { error });
1016
+ }
1017
+ }
1018
+ // PHP frameworks: Laravel
1019
+ for (const f of phpFiles.filter((f) => /routes|Route::/.test(f)).slice(0, 50)) {
1020
+ try {
1021
+ const content = await readFile(path.join(cwd, f), 'utf-8');
1022
+ if (/Route::(get|post|put|delete|resource)/.test(content)) {
1023
+ const eps = parseLaravelRoutes(content, f);
1024
+ if (eps.length > 0) {
1025
+ allEndpoints.push(...eps);
1026
+ specFiles.push(f);
1027
+ sources.add('laravel');
1028
+ }
1029
+ }
1030
+ }
1031
+ catch (error) {
1032
+ logger.debug(`Failed to parse possible Laravel route file: ${f}`, { error });
1033
+ }
1034
+ }
1035
+ // Java frameworks: Spring Boot, JAX-RS
1036
+ for (const f of javaFiles.filter((f) => /controller|Controller|RestController|@Path/.test(f)).slice(0, 200)) {
1037
+ try {
1038
+ const content = await readFile(path.join(cwd, f), 'utf-8');
1039
+ // Spring Boot
1040
+ if (/@(?:RestController|Controller)/.test(content)) {
1041
+ const eps = parseSpringRoutes(content, f);
1042
+ if (eps.length > 0) {
1043
+ allEndpoints.push(...eps);
1044
+ specFiles.push(f);
1045
+ sources.add('spring');
1046
+ }
1047
+ }
1048
+ // JAX-RS
1049
+ if (/@Path/.test(content) && /javax\.ws\.rs|jakarta\.ws\.rs/.test(content)) {
1050
+ const eps = parseJAXRSRoutes(content, f);
1051
+ if (eps.length > 0) {
1052
+ allEndpoints.push(...eps);
1053
+ specFiles.push(f);
1054
+ sources.add('jaxrs');
1055
+ }
1056
+ }
264
1057
  }
265
1058
  catch (error) {
266
- logger.debug(`Failed to parse possible OpenAPI spec: ${f}`, { error });
1059
+ logger.debug(`Failed to parse possible Java route file: ${f}`, { error });
1060
+ }
1061
+ }
1062
+ // Go frameworks: Gin, Echo, Fiber, Chi
1063
+ for (const f of goFiles.filter((f) => /routes?|handlers?|api/.test(f) || !/test/.test(f)).slice(0, 200)) {
1064
+ try {
1065
+ const content = await readFile(path.join(cwd, f), 'utf-8');
1066
+ // Gin
1067
+ if (/github\.com\/gin-gonic\/gin/.test(content) || /router\.(GET|POST|PUT|DELETE)/.test(content)) {
1068
+ const eps = parseGinRoutes(content, f);
1069
+ if (eps.length > 0) {
1070
+ allEndpoints.push(...eps);
1071
+ specFiles.push(f);
1072
+ sources.add('gin');
1073
+ }
1074
+ }
1075
+ // Echo
1076
+ if (/github\.com\/labstack\/echo/.test(content) || /e\.(GET|POST|PUT|DELETE)/.test(content)) {
1077
+ const eps = parseEchoRoutes(content, f);
1078
+ if (eps.length > 0) {
1079
+ allEndpoints.push(...eps);
1080
+ specFiles.push(f);
1081
+ sources.add('echo');
1082
+ }
1083
+ }
1084
+ // Fiber
1085
+ if (/github\.com\/gofiber\/fiber/.test(content) || /app\.(Get|Post|Put|Delete)/.test(content)) {
1086
+ const eps = parseFiberRoutes(content, f);
1087
+ if (eps.length > 0) {
1088
+ allEndpoints.push(...eps);
1089
+ specFiles.push(f);
1090
+ sources.add('fiber');
1091
+ }
1092
+ }
1093
+ // Chi
1094
+ if (/github\.com\/go-chi\/chi/.test(content) || /r\.(Get|Post|Put|Delete)/.test(content)) {
1095
+ const eps = parseChiRoutes(content, f);
1096
+ if (eps.length > 0) {
1097
+ allEndpoints.push(...eps);
1098
+ specFiles.push(f);
1099
+ sources.add('chi');
1100
+ }
1101
+ }
1102
+ }
1103
+ catch (error) {
1104
+ logger.debug(`Failed to parse possible Go route file: ${f}`, { error });
1105
+ }
1106
+ }
1107
+ // Rust frameworks: Actix-web, Rocket, Axum
1108
+ for (const f of rsFiles.filter((f) => /routes?|handlers?|api|main/.test(f) || !/test/.test(f)).slice(0, 200)) {
1109
+ try {
1110
+ const content = await readFile(path.join(cwd, f), 'utf-8');
1111
+ // Actix-web
1112
+ if (/actix_web/.test(content) || /\.route\(/.test(content)) {
1113
+ const eps = parseActixRoutes(content, f);
1114
+ if (eps.length > 0) {
1115
+ allEndpoints.push(...eps);
1116
+ specFiles.push(f);
1117
+ sources.add('actix');
1118
+ }
1119
+ }
1120
+ // Rocket
1121
+ if (/rocket/.test(content) || /#\[(get|post|put|delete)/.test(content)) {
1122
+ const eps = parseRocketRoutes(content, f);
1123
+ if (eps.length > 0) {
1124
+ allEndpoints.push(...eps);
1125
+ specFiles.push(f);
1126
+ sources.add('rocket');
1127
+ }
1128
+ }
1129
+ // Axum
1130
+ if (/axum/.test(content) || /Router::new/.test(content)) {
1131
+ const eps = parseAxumRoutes(content, f);
1132
+ if (eps.length > 0) {
1133
+ allEndpoints.push(...eps);
1134
+ specFiles.push(f);
1135
+ sources.add('axum');
1136
+ }
1137
+ }
1138
+ }
1139
+ catch (error) {
1140
+ logger.debug(`Failed to parse possible Rust route file: ${f}`, { error });
1141
+ }
1142
+ }
1143
+ // C# frameworks: ASP.NET Core
1144
+ for (const f of csFiles.filter((f) => /controller|Controller/.test(f)).slice(0, 200)) {
1145
+ try {
1146
+ const content = await readFile(path.join(cwd, f), 'utf-8');
1147
+ if (/\[ApiController\]|class\s+\w+Controller/.test(content)) {
1148
+ const eps = parseAspNetRoutes(content, f);
1149
+ if (eps.length > 0) {
1150
+ allEndpoints.push(...eps);
1151
+ specFiles.push(f);
1152
+ sources.add('aspnet');
1153
+ }
1154
+ }
1155
+ }
1156
+ catch (error) {
1157
+ logger.debug(`Failed to parse possible C# route file: ${f}`, { error });
267
1158
  }
268
1159
  }
269
1160
  // Also check for GraphQL in TS/JS files
@@ -279,8 +1170,8 @@ export async function analyzeApiContracts(cwd) {
279
1170
  }
280
1171
  }
281
1172
  }
282
- catch (error) {
283
- logger.debug(`Failed to parse possible OpenAPI spec: ${f}`, { error });
1173
+ catch {
1174
+ // Skip invalid OpenAPI specs
284
1175
  }
285
1176
  }
286
1177
  const summary = { get: 0, post: 0, put: 0, delete: 0, query: 0, mutation: 0, other: 0 };
@@ -304,8 +1195,15 @@ export async function analyzeApiContracts(cwd) {
304
1195
  const scannedPatterns = [
305
1196
  '**/*.{json,yaml,yml} (OpenAPI/Swagger containing swagger|openapi)',
306
1197
  '**/*.{graphql,gql} (GraphQL schemas)',
307
- '**/*.{ts,tsx,js,jsx} (Express/NestJS routes with router.get/post/decorators)',
308
- '**/*.py (FastAPI routes with @app.get/post)',
1198
+ '**/routes/**/*.{ts,tsx,js,jsx} (Express route files - prioritized)',
1199
+ '**/*.{ts,tsx,js,jsx} (Express/NestJS/Koa/Fastify/Hapi routes)',
1200
+ '**/*.py (FastAPI/Flask/Django REST Framework routes)',
1201
+ '**/*.rb (Rails routes.rb, Sinatra routes)',
1202
+ '**/*.php (Laravel Route::get/post/resource)',
1203
+ '**/*.java (Spring Boot @RequestMapping, JAX-RS @Path)',
1204
+ '**/*.go (Gin/Echo/Fiber/Chi routes)',
1205
+ '**/*.rs (Actix-web/Rocket/Axum routes)',
1206
+ '**/*.cs (ASP.NET Core [ApiController])',
309
1207
  ];
310
1208
  return {
311
1209
  endpoints: allEndpoints,