@contractspec/lib.source-extractors 2.7.6 → 2.7.10

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