@contractspec/lib.source-extractors 2.7.6 → 2.7.7

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.
@@ -317,101 +317,54 @@ class BaseExtractor {
317
317
  ctx.ir.schemas.push(schema);
318
318
  }
319
319
  }
320
- // src/extractors/nestjs/extractor.ts
320
+ // src/extractors/elysia/extractor.ts
321
321
  var PATTERNS = {
322
- controller: /@Controller\s*\(\s*['"`]([^'"`]*)['"`]\s*\)/g,
323
- route: /@(Get|Post|Put|Patch|Delete|Head|Options)\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/g,
324
- body: /@Body\s*\(\s*\)/g,
325
- param: /@Param\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/g,
326
- query: /@Query\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/g,
327
- dto: /class\s+(\w+(?:Dto|DTO|Request|Response|Input|Output))\s*\{/g,
328
- classValidator: /@(IsString|IsNumber|IsBoolean|IsArray|IsOptional|IsNotEmpty|Min|Max|Length|Matches)/g
322
+ route: /\.(get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
323
+ tSchema: /body:\s*t\.\w+/g
329
324
  };
330
325
 
331
- class NestJsExtractor extends BaseExtractor {
332
- id = "nestjs";
333
- name = "NestJS Extractor";
334
- frameworks = ["nestjs"];
335
- priority = 20;
326
+ class ElysiaExtractor extends BaseExtractor {
327
+ id = "elysia";
328
+ name = "Elysia Extractor";
329
+ frameworks = ["elysia"];
330
+ priority = 15;
336
331
  async doExtract(ctx) {
337
332
  const { project, options, fs } = ctx;
338
333
  const pattern = options.scope?.length ? options.scope.map((s) => `${s}/**/*.ts`).join(",") : "**/*.ts";
339
334
  const files = await fs.glob(pattern, { cwd: project.rootPath });
340
335
  ctx.ir.stats.filesScanned = files.length;
341
336
  for (const file of files) {
342
- if (file.includes("node_modules") || file.includes(".spec.") || file.includes(".test.")) {
337
+ if (file.includes("node_modules") || file.includes(".test."))
343
338
  continue;
344
- }
345
339
  const fullPath = `${project.rootPath}/${file}`;
346
340
  const content = await fs.readFile(fullPath);
347
- await this.extractControllers(ctx, file, content);
348
- await this.extractDtos(ctx, file, content);
349
- }
350
- }
351
- async extractControllers(ctx, file, content) {
352
- const controllerMatches = [...content.matchAll(PATTERNS.controller)];
353
- for (const controllerMatch of controllerMatches) {
354
- const basePath = controllerMatch[1] || "";
355
- const controllerIndex = controllerMatch.index ?? 0;
356
- const afterDecorator = content.slice(controllerIndex);
357
- const classMatch = afterDecorator.match(/class\s+(\w+)/);
358
- const controllerName = classMatch?.[1] ?? "UnknownController";
359
- const nextController = content.indexOf("@Controller", controllerIndex + 1);
360
- const controllerBlock = nextController > 0 ? content.slice(controllerIndex, nextController) : content.slice(controllerIndex);
361
- const routeMatches = [...controllerBlock.matchAll(PATTERNS.route)];
362
- for (const routeMatch of routeMatches) {
363
- const method = routeMatch[1]?.toUpperCase();
364
- const routePath = routeMatch[2] || "";
365
- const fullPath = this.normalizePath(`/${basePath}/${routePath}`);
366
- const afterRoute = controllerBlock.slice(routeMatch.index ?? 0);
367
- const methodMatch = afterRoute.match(/(?:async\s+)?(\w+)\s*\([^)]*\)\s*(?::\s*\w+(?:<[^>]+>)?)?\s*\{/);
368
- const handlerName = methodMatch?.[1] ?? "unknownHandler";
369
- const absoluteIndex = controllerIndex + (routeMatch.index ?? 0);
370
- const lineNumber = content.slice(0, absoluteIndex).split(`
371
- `).length;
372
- const hasBody = PATTERNS.body.test(afterRoute.slice(0, 200));
373
- const hasParams = PATTERNS.param.test(afterRoute.slice(0, 200));
374
- const hasQuery = PATTERNS.query.test(afterRoute.slice(0, 200));
375
- const endpoint = {
376
- id: this.generateEndpointId(method, fullPath, handlerName),
377
- method,
378
- path: fullPath,
379
- kind: this.methodToOpKind(method),
380
- handlerName,
381
- controllerName,
382
- source: this.createLocation(file, lineNumber, lineNumber + 10),
383
- confidence: this.createConfidence("medium", "decorator-hints"),
384
- frameworkMeta: {
385
- hasBody,
386
- hasParams,
387
- hasQuery
388
- }
389
- };
390
- this.addEndpoint(ctx, endpoint);
391
- }
341
+ if (!content.includes("elysia"))
342
+ continue;
343
+ await this.extractRoutes(ctx, file, content);
392
344
  }
393
345
  }
394
- async extractDtos(ctx, file, content) {
395
- const dtoMatches = [...content.matchAll(PATTERNS.dto)];
396
- for (const match of dtoMatches) {
397
- const name = match[1] ?? "UnknownDto";
346
+ async extractRoutes(ctx, file, content) {
347
+ const matches = [...content.matchAll(PATTERNS.route)];
348
+ for (const match of matches) {
349
+ const method = match[1]?.toUpperCase() ?? "GET";
350
+ const path = match[2] ?? "/";
398
351
  const index = match.index ?? 0;
399
352
  const lineNumber = content.slice(0, index).split(`
400
353
  `).length;
401
- const hasClassValidator = content.includes("class-validator") || content.includes("@IsString") || content.includes("@IsNumber");
402
- const schema = {
403
- id: this.generateSchemaId(name, file),
404
- name,
405
- schemaType: hasClassValidator ? "class-validator" : "typescript",
406
- source: this.createLocation(file, lineNumber, lineNumber + 20),
407
- confidence: this.createConfidence(hasClassValidator ? "high" : "medium", hasClassValidator ? "explicit-schema" : "inferred-types")
354
+ const afterMatch = content.slice(index, index + 500);
355
+ const hasTSchema = PATTERNS.tSchema.test(afterMatch);
356
+ const endpoint = {
357
+ id: this.generateEndpointId(method, path),
358
+ method,
359
+ path,
360
+ kind: this.methodToOpKind(method),
361
+ handlerName: "handler",
362
+ source: this.createLocation(file, lineNumber, lineNumber + 5),
363
+ confidence: this.createConfidence(hasTSchema ? "high" : "medium", hasTSchema ? "explicit-schema" : "decorator-hints")
408
364
  };
409
- this.addSchema(ctx, schema);
365
+ this.addEndpoint(ctx, endpoint);
410
366
  }
411
367
  }
412
- normalizePath(path) {
413
- return "/" + path.replace(/\/+/g, "/").replace(/^\/+|\/+$/g, "");
414
- }
415
368
  }
416
369
  // src/extractors/express/extractor.ts
417
370
  var PATTERNS2 = {
@@ -560,119 +513,104 @@ class HonoExtractor extends BaseExtractor {
560
513
  }
561
514
  }
562
515
  }
563
- // src/extractors/elysia/extractor.ts
516
+ // src/extractors/nestjs/extractor.ts
564
517
  var PATTERNS5 = {
565
- route: /\.(get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
566
- tSchema: /body:\s*t\.\w+/g
518
+ controller: /@Controller\s*\(\s*['"`]([^'"`]*)['"`]\s*\)/g,
519
+ route: /@(Get|Post|Put|Patch|Delete|Head|Options)\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/g,
520
+ body: /@Body\s*\(\s*\)/g,
521
+ param: /@Param\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/g,
522
+ query: /@Query\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/g,
523
+ dto: /class\s+(\w+(?:Dto|DTO|Request|Response|Input|Output))\s*\{/g,
524
+ classValidator: /@(IsString|IsNumber|IsBoolean|IsArray|IsOptional|IsNotEmpty|Min|Max|Length|Matches)/g
567
525
  };
568
526
 
569
- class ElysiaExtractor extends BaseExtractor {
570
- id = "elysia";
571
- name = "Elysia Extractor";
572
- frameworks = ["elysia"];
573
- priority = 15;
527
+ class NestJsExtractor extends BaseExtractor {
528
+ id = "nestjs";
529
+ name = "NestJS Extractor";
530
+ frameworks = ["nestjs"];
531
+ priority = 20;
574
532
  async doExtract(ctx) {
575
533
  const { project, options, fs } = ctx;
576
534
  const pattern = options.scope?.length ? options.scope.map((s) => `${s}/**/*.ts`).join(",") : "**/*.ts";
577
535
  const files = await fs.glob(pattern, { cwd: project.rootPath });
578
536
  ctx.ir.stats.filesScanned = files.length;
579
537
  for (const file of files) {
580
- if (file.includes("node_modules") || file.includes(".test."))
538
+ if (file.includes("node_modules") || file.includes(".spec.") || file.includes(".test.")) {
581
539
  continue;
540
+ }
582
541
  const fullPath = `${project.rootPath}/${file}`;
583
542
  const content = await fs.readFile(fullPath);
584
- if (!content.includes("elysia"))
585
- continue;
586
- await this.extractRoutes(ctx, file, content);
543
+ await this.extractControllers(ctx, file, content);
544
+ await this.extractDtos(ctx, file, content);
587
545
  }
588
546
  }
589
- async extractRoutes(ctx, file, content) {
590
- const matches = [...content.matchAll(PATTERNS5.route)];
591
- for (const match of matches) {
592
- const method = match[1]?.toUpperCase() ?? "GET";
593
- const path = match[2] ?? "/";
594
- const index = match.index ?? 0;
595
- const lineNumber = content.slice(0, index).split(`
547
+ async extractControllers(ctx, file, content) {
548
+ const controllerMatches = [...content.matchAll(PATTERNS5.controller)];
549
+ for (const controllerMatch of controllerMatches) {
550
+ const basePath = controllerMatch[1] || "";
551
+ const controllerIndex = controllerMatch.index ?? 0;
552
+ const afterDecorator = content.slice(controllerIndex);
553
+ const classMatch = afterDecorator.match(/class\s+(\w+)/);
554
+ const controllerName = classMatch?.[1] ?? "UnknownController";
555
+ const nextController = content.indexOf("@Controller", controllerIndex + 1);
556
+ const controllerBlock = nextController > 0 ? content.slice(controllerIndex, nextController) : content.slice(controllerIndex);
557
+ const routeMatches = [...controllerBlock.matchAll(PATTERNS5.route)];
558
+ for (const routeMatch of routeMatches) {
559
+ const method = routeMatch[1]?.toUpperCase();
560
+ const routePath = routeMatch[2] || "";
561
+ const fullPath = this.normalizePath(`/${basePath}/${routePath}`);
562
+ const afterRoute = controllerBlock.slice(routeMatch.index ?? 0);
563
+ const methodMatch = afterRoute.match(/(?:async\s+)?(\w+)\s*\([^)]*\)\s*(?::\s*\w+(?:<[^>]+>)?)?\s*\{/);
564
+ const handlerName = methodMatch?.[1] ?? "unknownHandler";
565
+ const absoluteIndex = controllerIndex + (routeMatch.index ?? 0);
566
+ const lineNumber = content.slice(0, absoluteIndex).split(`
596
567
  `).length;
597
- const afterMatch = content.slice(index, index + 500);
598
- const hasTSchema = PATTERNS5.tSchema.test(afterMatch);
599
- const endpoint = {
600
- id: this.generateEndpointId(method, path),
601
- method,
602
- path,
603
- kind: this.methodToOpKind(method),
604
- handlerName: "handler",
605
- source: this.createLocation(file, lineNumber, lineNumber + 5),
606
- confidence: this.createConfidence(hasTSchema ? "high" : "medium", hasTSchema ? "explicit-schema" : "decorator-hints")
607
- };
608
- this.addEndpoint(ctx, endpoint);
609
- }
610
- }
611
- }
612
- // src/extractors/trpc/extractor.ts
613
- var PATTERNS6 = {
614
- procedure: /\.(query|mutation)\s*\(\s*(?:\{[^}]*\}|[^)]+)\)/gi,
615
- procedureName: /(\w+)\s*:\s*(?:publicProcedure|protectedProcedure|procedure)/g,
616
- zodInput: /\.input\s*\(\s*(\w+)/g,
617
- zodOutput: /\.output\s*\(\s*(\w+)/g
618
- };
619
-
620
- class TrpcExtractor extends BaseExtractor {
621
- id = "trpc";
622
- name = "tRPC Extractor";
623
- frameworks = ["trpc"];
624
- priority = 15;
625
- async doExtract(ctx) {
626
- const { project, options, fs } = ctx;
627
- const pattern = options.scope?.length ? options.scope.map((s) => `${s}/**/*.ts`).join(",") : "**/*.ts";
628
- const files = await fs.glob(pattern, { cwd: project.rootPath });
629
- ctx.ir.stats.filesScanned = files.length;
630
- for (const file of files) {
631
- if (file.includes("node_modules") || file.includes(".test."))
632
- continue;
633
- const fullPath = `${project.rootPath}/${file}`;
634
- const content = await fs.readFile(fullPath);
635
- if (!content.includes("trpc") && !content.includes("Procedure"))
636
- continue;
637
- await this.extractProcedures(ctx, file, content);
568
+ const hasBody = PATTERNS5.body.test(afterRoute.slice(0, 200));
569
+ const hasParams = PATTERNS5.param.test(afterRoute.slice(0, 200));
570
+ const hasQuery = PATTERNS5.query.test(afterRoute.slice(0, 200));
571
+ const endpoint = {
572
+ id: this.generateEndpointId(method, fullPath, handlerName),
573
+ method,
574
+ path: fullPath,
575
+ kind: this.methodToOpKind(method),
576
+ handlerName,
577
+ controllerName,
578
+ source: this.createLocation(file, lineNumber, lineNumber + 10),
579
+ confidence: this.createConfidence("medium", "decorator-hints"),
580
+ frameworkMeta: {
581
+ hasBody,
582
+ hasParams,
583
+ hasQuery
584
+ }
585
+ };
586
+ this.addEndpoint(ctx, endpoint);
587
+ }
638
588
  }
639
589
  }
640
- async extractProcedures(ctx, file, content) {
641
- const nameMatches = [...content.matchAll(PATTERNS6.procedureName)];
642
- for (const match of nameMatches) {
643
- const procedureName = match[1] ?? "unknownProcedure";
590
+ async extractDtos(ctx, file, content) {
591
+ const dtoMatches = [...content.matchAll(PATTERNS5.dto)];
592
+ for (const match of dtoMatches) {
593
+ const name = match[1] ?? "UnknownDto";
644
594
  const index = match.index ?? 0;
645
595
  const lineNumber = content.slice(0, index).split(`
646
596
  `).length;
647
- const afterMatch = content.slice(index, index + 500);
648
- const isQuery = afterMatch.includes(".query(");
649
- const isMutation = afterMatch.includes(".mutation(");
650
- if (!isQuery && !isMutation)
651
- continue;
652
- const hasZodInput = PATTERNS6.zodInput.test(afterMatch);
653
- const hasZodOutput = PATTERNS6.zodOutput.test(afterMatch);
654
- const hasSchema = hasZodInput || hasZodOutput;
655
- const method = isMutation ? "POST" : "GET";
656
- const endpoint = {
657
- id: `trpc.${procedureName}`,
658
- method,
659
- path: `/trpc/${procedureName}`,
660
- kind: isMutation ? "command" : "query",
661
- handlerName: procedureName,
662
- source: this.createLocation(file, lineNumber, lineNumber + 10),
663
- confidence: this.createConfidence(hasSchema ? "high" : "medium", hasSchema ? "explicit-schema" : "inferred-types"),
664
- frameworkMeta: {
665
- procedureType: isMutation ? "mutation" : "query",
666
- hasInput: hasZodInput,
667
- hasOutput: hasZodOutput
668
- }
597
+ const hasClassValidator = content.includes("class-validator") || content.includes("@IsString") || content.includes("@IsNumber");
598
+ const schema = {
599
+ id: this.generateSchemaId(name, file),
600
+ name,
601
+ schemaType: hasClassValidator ? "class-validator" : "typescript",
602
+ source: this.createLocation(file, lineNumber, lineNumber + 20),
603
+ confidence: this.createConfidence(hasClassValidator ? "high" : "medium", hasClassValidator ? "explicit-schema" : "inferred-types")
669
604
  };
670
- this.addEndpoint(ctx, endpoint);
605
+ this.addSchema(ctx, schema);
671
606
  }
672
607
  }
608
+ normalizePath(path) {
609
+ return "/" + path.replace(/\/+/g, "/").replace(/^\/+|\/+$/g, "");
610
+ }
673
611
  }
674
612
  // src/extractors/next-api/extractor.ts
675
- var PATTERNS7 = {
613
+ var PATTERNS6 = {
676
614
  appRouterExport: /export\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)/gi,
677
615
  pagesHandler: /export\s+default\s+(?:async\s+)?function/g,
678
616
  zodSchema: /z\.\w+\(/g
@@ -706,13 +644,13 @@ class NextApiExtractor extends BaseExtractor {
706
644
  async extractAppRoutes(ctx, file, content) {
707
645
  const pathMatch = file.match(/app\/api\/(.+)\/route\.ts$/);
708
646
  const routePath = pathMatch ? `/api/${pathMatch[1]}` : "/api";
709
- const matches = [...content.matchAll(PATTERNS7.appRouterExport)];
647
+ const matches = [...content.matchAll(PATTERNS6.appRouterExport)];
710
648
  for (const match of matches) {
711
649
  const method = match[1]?.toUpperCase() ?? "GET";
712
650
  const index = match.index ?? 0;
713
651
  const lineNumber = content.slice(0, index).split(`
714
652
  `).length;
715
- const hasZod = PATTERNS7.zodSchema.test(content);
653
+ const hasZod = PATTERNS6.zodSchema.test(content);
716
654
  const endpoint = {
717
655
  id: this.generateEndpointId(method, routePath),
718
656
  method,
@@ -729,10 +667,10 @@ class NextApiExtractor extends BaseExtractor {
729
667
  async extractPagesRoutes(ctx, file, content) {
730
668
  const pathMatch = file.match(/pages\/api\/(.+)\.ts$/);
731
669
  const routePath = pathMatch ? `/api/${pathMatch[1]}` : "/api";
732
- if (!PATTERNS7.pagesHandler.test(content))
670
+ if (!PATTERNS6.pagesHandler.test(content))
733
671
  return;
734
672
  const lineNumber = 1;
735
- const _hasZod = PATTERNS7.zodSchema.test(content);
673
+ const _hasZod = PATTERNS6.zodSchema.test(content);
736
674
  const methods = ["GET", "POST"];
737
675
  for (const method of methods) {
738
676
  const endpoint = {
@@ -749,6 +687,68 @@ class NextApiExtractor extends BaseExtractor {
749
687
  }
750
688
  }
751
689
  }
690
+ // src/extractors/trpc/extractor.ts
691
+ var PATTERNS7 = {
692
+ procedure: /\.(query|mutation)\s*\(\s*(?:\{[^}]*\}|[^)]+)\)/gi,
693
+ procedureName: /(\w+)\s*:\s*(?:publicProcedure|protectedProcedure|procedure)/g,
694
+ zodInput: /\.input\s*\(\s*(\w+)/g,
695
+ zodOutput: /\.output\s*\(\s*(\w+)/g
696
+ };
697
+
698
+ class TrpcExtractor extends BaseExtractor {
699
+ id = "trpc";
700
+ name = "tRPC Extractor";
701
+ frameworks = ["trpc"];
702
+ priority = 15;
703
+ async doExtract(ctx) {
704
+ const { project, options, fs } = ctx;
705
+ const pattern = options.scope?.length ? options.scope.map((s) => `${s}/**/*.ts`).join(",") : "**/*.ts";
706
+ const files = await fs.glob(pattern, { cwd: project.rootPath });
707
+ ctx.ir.stats.filesScanned = files.length;
708
+ for (const file of files) {
709
+ if (file.includes("node_modules") || file.includes(".test."))
710
+ continue;
711
+ const fullPath = `${project.rootPath}/${file}`;
712
+ const content = await fs.readFile(fullPath);
713
+ if (!content.includes("trpc") && !content.includes("Procedure"))
714
+ continue;
715
+ await this.extractProcedures(ctx, file, content);
716
+ }
717
+ }
718
+ async extractProcedures(ctx, file, content) {
719
+ const nameMatches = [...content.matchAll(PATTERNS7.procedureName)];
720
+ for (const match of nameMatches) {
721
+ const procedureName = match[1] ?? "unknownProcedure";
722
+ const index = match.index ?? 0;
723
+ const lineNumber = content.slice(0, index).split(`
724
+ `).length;
725
+ const afterMatch = content.slice(index, index + 500);
726
+ const isQuery = afterMatch.includes(".query(");
727
+ const isMutation = afterMatch.includes(".mutation(");
728
+ if (!isQuery && !isMutation)
729
+ continue;
730
+ const hasZodInput = PATTERNS7.zodInput.test(afterMatch);
731
+ const hasZodOutput = PATTERNS7.zodOutput.test(afterMatch);
732
+ const hasSchema = hasZodInput || hasZodOutput;
733
+ const method = isMutation ? "POST" : "GET";
734
+ const endpoint = {
735
+ id: `trpc.${procedureName}`,
736
+ method,
737
+ path: `/trpc/${procedureName}`,
738
+ kind: isMutation ? "command" : "query",
739
+ handlerName: procedureName,
740
+ source: this.createLocation(file, lineNumber, lineNumber + 10),
741
+ confidence: this.createConfidence(hasSchema ? "high" : "medium", hasSchema ? "explicit-schema" : "inferred-types"),
742
+ frameworkMeta: {
743
+ procedureType: isMutation ? "mutation" : "query",
744
+ hasInput: hasZodInput,
745
+ hasOutput: hasZodOutput
746
+ }
747
+ };
748
+ this.addEndpoint(ctx, endpoint);
749
+ }
750
+ }
751
+ }
752
752
  // src/extractors/zod/extractor.ts
753
753
  var PATTERNS8 = {
754
754
  zodSchema: /(?:export\s+)?const\s+(\w+)\s*=\s*z\.(?:object|string|number|boolean|array|enum|union|intersection|literal|tuple|record)/g,