codedev-mcp 3.2.1 → 3.2.3

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