@contractspec/lib.source-extractors 2.7.5 → 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.
@@ -104,6 +104,42 @@ function generateOperationCode(endpoint, specName, options) {
104
104
  return lines.join(`
105
105
  `);
106
106
  }
107
+ // src/codegen/registry-gen.ts
108
+ function generateRegistry(operationFiles) {
109
+ const operationImports = operationFiles.filter((f) => f.type === "operation").map((f) => {
110
+ const name = f.path.replace(".ts", "").replace(/-/g, "_");
111
+ const specName = toPascalCase(name) + "Spec";
112
+ return { path: f.path, name, specName };
113
+ });
114
+ const lines = [
115
+ `/**`,
116
+ ` * Generated operation registry.`,
117
+ ` */`,
118
+ ``,
119
+ `import { OperationSpecRegistry } from '@contractspec/lib.contracts-spec';`,
120
+ ``
121
+ ];
122
+ for (const op of operationImports) {
123
+ const importPath = `./${op.path.replace(".ts", "")}`;
124
+ lines.push(`import { ${op.specName} } from '${importPath}';`);
125
+ }
126
+ lines.push(``);
127
+ lines.push(`export const operationRegistry = new OperationSpecRegistry();`);
128
+ lines.push(``);
129
+ for (const op of operationImports) {
130
+ lines.push(`operationRegistry.register(${op.specName});`);
131
+ }
132
+ lines.push(``);
133
+ return {
134
+ path: "registry.ts",
135
+ content: lines.join(`
136
+ `),
137
+ type: "registry"
138
+ };
139
+ }
140
+ function toPascalCase(str) {
141
+ return str.split(/[-_]/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
142
+ }
107
143
  // src/codegen/schema-gen.ts
108
144
  function generateSchema(schema, _options) {
109
145
  const fileName = `${toFileName2(schema.name)}.ts`;
@@ -184,42 +220,6 @@ function mapToZodType(tsType, optional) {
184
220
  }
185
221
  return optional ? `${zodType}.optional()` : zodType;
186
222
  }
187
- // src/codegen/registry-gen.ts
188
- function generateRegistry(operationFiles) {
189
- const operationImports = operationFiles.filter((f) => f.type === "operation").map((f) => {
190
- const name = f.path.replace(".ts", "").replace(/-/g, "_");
191
- const specName = toPascalCase(name) + "Spec";
192
- return { path: f.path, name, specName };
193
- });
194
- const lines = [
195
- `/**`,
196
- ` * Generated operation registry.`,
197
- ` */`,
198
- ``,
199
- `import { OperationSpecRegistry } from '@contractspec/lib.contracts-spec';`,
200
- ``
201
- ];
202
- for (const op of operationImports) {
203
- const importPath = `./${op.path.replace(".ts", "")}`;
204
- lines.push(`import { ${op.specName} } from '${importPath}';`);
205
- }
206
- lines.push(``);
207
- lines.push(`export const operationRegistry = new OperationSpecRegistry();`);
208
- lines.push(``);
209
- for (const op of operationImports) {
210
- lines.push(`operationRegistry.register(${op.specName});`);
211
- }
212
- lines.push(``);
213
- return {
214
- path: "registry.ts",
215
- content: lines.join(`
216
- `),
217
- type: "registry"
218
- };
219
- }
220
- function toPascalCase(str) {
221
- return str.split(/[-_]/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
222
- }
223
223
  // src/extractors/index.ts
224
224
  var exports_extractors = {};
225
225
  __export(exports_extractors, {
@@ -524,101 +524,54 @@ class BaseExtractor {
524
524
  ctx.ir.schemas.push(schema);
525
525
  }
526
526
  }
527
- // src/extractors/nestjs/extractor.ts
527
+ // src/extractors/elysia/extractor.ts
528
528
  var PATTERNS = {
529
- controller: /@Controller\s*\(\s*['"`]([^'"`]*)['"`]\s*\)/g,
530
- route: /@(Get|Post|Put|Patch|Delete|Head|Options)\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/g,
531
- body: /@Body\s*\(\s*\)/g,
532
- param: /@Param\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/g,
533
- query: /@Query\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/g,
534
- dto: /class\s+(\w+(?:Dto|DTO|Request|Response|Input|Output))\s*\{/g,
535
- classValidator: /@(IsString|IsNumber|IsBoolean|IsArray|IsOptional|IsNotEmpty|Min|Max|Length|Matches)/g
529
+ route: /\.(get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
530
+ tSchema: /body:\s*t\.\w+/g
536
531
  };
537
532
 
538
- class NestJsExtractor extends BaseExtractor {
539
- id = "nestjs";
540
- name = "NestJS Extractor";
541
- frameworks = ["nestjs"];
542
- priority = 20;
533
+ class ElysiaExtractor extends BaseExtractor {
534
+ id = "elysia";
535
+ name = "Elysia Extractor";
536
+ frameworks = ["elysia"];
537
+ priority = 15;
543
538
  async doExtract(ctx) {
544
539
  const { project, options, fs } = ctx;
545
540
  const pattern = options.scope?.length ? options.scope.map((s) => `${s}/**/*.ts`).join(",") : "**/*.ts";
546
541
  const files = await fs.glob(pattern, { cwd: project.rootPath });
547
542
  ctx.ir.stats.filesScanned = files.length;
548
543
  for (const file of files) {
549
- if (file.includes("node_modules") || file.includes(".spec.") || file.includes(".test.")) {
544
+ if (file.includes("node_modules") || file.includes(".test."))
550
545
  continue;
551
- }
552
546
  const fullPath = `${project.rootPath}/${file}`;
553
547
  const content = await fs.readFile(fullPath);
554
- await this.extractControllers(ctx, file, content);
555
- await this.extractDtos(ctx, file, content);
556
- }
557
- }
558
- async extractControllers(ctx, file, content) {
559
- const controllerMatches = [...content.matchAll(PATTERNS.controller)];
560
- for (const controllerMatch of controllerMatches) {
561
- const basePath = controllerMatch[1] || "";
562
- const controllerIndex = controllerMatch.index ?? 0;
563
- const afterDecorator = content.slice(controllerIndex);
564
- const classMatch = afterDecorator.match(/class\s+(\w+)/);
565
- const controllerName = classMatch?.[1] ?? "UnknownController";
566
- const nextController = content.indexOf("@Controller", controllerIndex + 1);
567
- const controllerBlock = nextController > 0 ? content.slice(controllerIndex, nextController) : content.slice(controllerIndex);
568
- const routeMatches = [...controllerBlock.matchAll(PATTERNS.route)];
569
- for (const routeMatch of routeMatches) {
570
- const method = routeMatch[1]?.toUpperCase();
571
- const routePath = routeMatch[2] || "";
572
- const fullPath = this.normalizePath(`/${basePath}/${routePath}`);
573
- const afterRoute = controllerBlock.slice(routeMatch.index ?? 0);
574
- const methodMatch = afterRoute.match(/(?:async\s+)?(\w+)\s*\([^)]*\)\s*(?::\s*\w+(?:<[^>]+>)?)?\s*\{/);
575
- const handlerName = methodMatch?.[1] ?? "unknownHandler";
576
- const absoluteIndex = controllerIndex + (routeMatch.index ?? 0);
577
- const lineNumber = content.slice(0, absoluteIndex).split(`
578
- `).length;
579
- const hasBody = PATTERNS.body.test(afterRoute.slice(0, 200));
580
- const hasParams = PATTERNS.param.test(afterRoute.slice(0, 200));
581
- const hasQuery = PATTERNS.query.test(afterRoute.slice(0, 200));
582
- const endpoint = {
583
- id: this.generateEndpointId(method, fullPath, handlerName),
584
- method,
585
- path: fullPath,
586
- kind: this.methodToOpKind(method),
587
- handlerName,
588
- controllerName,
589
- source: this.createLocation(file, lineNumber, lineNumber + 10),
590
- confidence: this.createConfidence("medium", "decorator-hints"),
591
- frameworkMeta: {
592
- hasBody,
593
- hasParams,
594
- hasQuery
595
- }
596
- };
597
- this.addEndpoint(ctx, endpoint);
598
- }
548
+ if (!content.includes("elysia"))
549
+ continue;
550
+ await this.extractRoutes(ctx, file, content);
599
551
  }
600
552
  }
601
- async extractDtos(ctx, file, content) {
602
- const dtoMatches = [...content.matchAll(PATTERNS.dto)];
603
- for (const match of dtoMatches) {
604
- const name = match[1] ?? "UnknownDto";
553
+ async extractRoutes(ctx, file, content) {
554
+ const matches = [...content.matchAll(PATTERNS.route)];
555
+ for (const match of matches) {
556
+ const method = match[1]?.toUpperCase() ?? "GET";
557
+ const path = match[2] ?? "/";
605
558
  const index = match.index ?? 0;
606
559
  const lineNumber = content.slice(0, index).split(`
607
560
  `).length;
608
- const hasClassValidator = content.includes("class-validator") || content.includes("@IsString") || content.includes("@IsNumber");
609
- const schema = {
610
- id: this.generateSchemaId(name, file),
611
- name,
612
- schemaType: hasClassValidator ? "class-validator" : "typescript",
613
- source: this.createLocation(file, lineNumber, lineNumber + 20),
614
- confidence: this.createConfidence(hasClassValidator ? "high" : "medium", hasClassValidator ? "explicit-schema" : "inferred-types")
561
+ const afterMatch = content.slice(index, index + 500);
562
+ const hasTSchema = PATTERNS.tSchema.test(afterMatch);
563
+ const endpoint = {
564
+ id: this.generateEndpointId(method, path),
565
+ method,
566
+ path,
567
+ kind: this.methodToOpKind(method),
568
+ handlerName: "handler",
569
+ source: this.createLocation(file, lineNumber, lineNumber + 5),
570
+ confidence: this.createConfidence(hasTSchema ? "high" : "medium", hasTSchema ? "explicit-schema" : "decorator-hints")
615
571
  };
616
- this.addSchema(ctx, schema);
572
+ this.addEndpoint(ctx, endpoint);
617
573
  }
618
574
  }
619
- normalizePath(path) {
620
- return "/" + path.replace(/\/+/g, "/").replace(/^\/+|\/+$/g, "");
621
- }
622
575
  }
623
576
  // src/extractors/express/extractor.ts
624
577
  var PATTERNS2 = {
@@ -767,119 +720,104 @@ class HonoExtractor extends BaseExtractor {
767
720
  }
768
721
  }
769
722
  }
770
- // src/extractors/elysia/extractor.ts
723
+ // src/extractors/nestjs/extractor.ts
771
724
  var PATTERNS5 = {
772
- route: /\.(get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
773
- tSchema: /body:\s*t\.\w+/g
725
+ controller: /@Controller\s*\(\s*['"`]([^'"`]*)['"`]\s*\)/g,
726
+ route: /@(Get|Post|Put|Patch|Delete|Head|Options)\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/g,
727
+ body: /@Body\s*\(\s*\)/g,
728
+ param: /@Param\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/g,
729
+ query: /@Query\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/g,
730
+ dto: /class\s+(\w+(?:Dto|DTO|Request|Response|Input|Output))\s*\{/g,
731
+ classValidator: /@(IsString|IsNumber|IsBoolean|IsArray|IsOptional|IsNotEmpty|Min|Max|Length|Matches)/g
774
732
  };
775
733
 
776
- class ElysiaExtractor extends BaseExtractor {
777
- id = "elysia";
778
- name = "Elysia Extractor";
779
- frameworks = ["elysia"];
780
- priority = 15;
734
+ class NestJsExtractor extends BaseExtractor {
735
+ id = "nestjs";
736
+ name = "NestJS Extractor";
737
+ frameworks = ["nestjs"];
738
+ priority = 20;
781
739
  async doExtract(ctx) {
782
740
  const { project, options, fs } = ctx;
783
741
  const pattern = options.scope?.length ? options.scope.map((s) => `${s}/**/*.ts`).join(",") : "**/*.ts";
784
742
  const files = await fs.glob(pattern, { cwd: project.rootPath });
785
743
  ctx.ir.stats.filesScanned = files.length;
786
744
  for (const file of files) {
787
- if (file.includes("node_modules") || file.includes(".test."))
745
+ if (file.includes("node_modules") || file.includes(".spec.") || file.includes(".test.")) {
788
746
  continue;
747
+ }
789
748
  const fullPath = `${project.rootPath}/${file}`;
790
749
  const content = await fs.readFile(fullPath);
791
- if (!content.includes("elysia"))
792
- continue;
793
- await this.extractRoutes(ctx, file, content);
750
+ await this.extractControllers(ctx, file, content);
751
+ await this.extractDtos(ctx, file, content);
794
752
  }
795
753
  }
796
- async extractRoutes(ctx, file, content) {
797
- const matches = [...content.matchAll(PATTERNS5.route)];
798
- for (const match of matches) {
799
- const method = match[1]?.toUpperCase() ?? "GET";
800
- const path = match[2] ?? "/";
801
- const index = match.index ?? 0;
802
- const lineNumber = content.slice(0, index).split(`
754
+ async extractControllers(ctx, file, content) {
755
+ const controllerMatches = [...content.matchAll(PATTERNS5.controller)];
756
+ for (const controllerMatch of controllerMatches) {
757
+ const basePath = controllerMatch[1] || "";
758
+ const controllerIndex = controllerMatch.index ?? 0;
759
+ const afterDecorator = content.slice(controllerIndex);
760
+ const classMatch = afterDecorator.match(/class\s+(\w+)/);
761
+ const controllerName = classMatch?.[1] ?? "UnknownController";
762
+ const nextController = content.indexOf("@Controller", controllerIndex + 1);
763
+ const controllerBlock = nextController > 0 ? content.slice(controllerIndex, nextController) : content.slice(controllerIndex);
764
+ const routeMatches = [...controllerBlock.matchAll(PATTERNS5.route)];
765
+ for (const routeMatch of routeMatches) {
766
+ const method = routeMatch[1]?.toUpperCase();
767
+ const routePath = routeMatch[2] || "";
768
+ const fullPath = this.normalizePath(`/${basePath}/${routePath}`);
769
+ const afterRoute = controllerBlock.slice(routeMatch.index ?? 0);
770
+ const methodMatch = afterRoute.match(/(?:async\s+)?(\w+)\s*\([^)]*\)\s*(?::\s*\w+(?:<[^>]+>)?)?\s*\{/);
771
+ const handlerName = methodMatch?.[1] ?? "unknownHandler";
772
+ const absoluteIndex = controllerIndex + (routeMatch.index ?? 0);
773
+ const lineNumber = content.slice(0, absoluteIndex).split(`
803
774
  `).length;
804
- const afterMatch = content.slice(index, index + 500);
805
- const hasTSchema = PATTERNS5.tSchema.test(afterMatch);
806
- const endpoint = {
807
- id: this.generateEndpointId(method, path),
808
- method,
809
- path,
810
- kind: this.methodToOpKind(method),
811
- handlerName: "handler",
812
- source: this.createLocation(file, lineNumber, lineNumber + 5),
813
- confidence: this.createConfidence(hasTSchema ? "high" : "medium", hasTSchema ? "explicit-schema" : "decorator-hints")
814
- };
815
- this.addEndpoint(ctx, endpoint);
816
- }
817
- }
818
- }
819
- // src/extractors/trpc/extractor.ts
820
- var PATTERNS6 = {
821
- procedure: /\.(query|mutation)\s*\(\s*(?:\{[^}]*\}|[^)]+)\)/gi,
822
- procedureName: /(\w+)\s*:\s*(?:publicProcedure|protectedProcedure|procedure)/g,
823
- zodInput: /\.input\s*\(\s*(\w+)/g,
824
- zodOutput: /\.output\s*\(\s*(\w+)/g
825
- };
826
-
827
- class TrpcExtractor extends BaseExtractor {
828
- id = "trpc";
829
- name = "tRPC Extractor";
830
- frameworks = ["trpc"];
831
- priority = 15;
832
- async doExtract(ctx) {
833
- const { project, options, fs } = ctx;
834
- const pattern = options.scope?.length ? options.scope.map((s) => `${s}/**/*.ts`).join(",") : "**/*.ts";
835
- const files = await fs.glob(pattern, { cwd: project.rootPath });
836
- ctx.ir.stats.filesScanned = files.length;
837
- for (const file of files) {
838
- if (file.includes("node_modules") || file.includes(".test."))
839
- continue;
840
- const fullPath = `${project.rootPath}/${file}`;
841
- const content = await fs.readFile(fullPath);
842
- if (!content.includes("trpc") && !content.includes("Procedure"))
843
- continue;
844
- await this.extractProcedures(ctx, file, content);
775
+ const hasBody = PATTERNS5.body.test(afterRoute.slice(0, 200));
776
+ const hasParams = PATTERNS5.param.test(afterRoute.slice(0, 200));
777
+ const hasQuery = PATTERNS5.query.test(afterRoute.slice(0, 200));
778
+ const endpoint = {
779
+ id: this.generateEndpointId(method, fullPath, handlerName),
780
+ method,
781
+ path: fullPath,
782
+ kind: this.methodToOpKind(method),
783
+ handlerName,
784
+ controllerName,
785
+ source: this.createLocation(file, lineNumber, lineNumber + 10),
786
+ confidence: this.createConfidence("medium", "decorator-hints"),
787
+ frameworkMeta: {
788
+ hasBody,
789
+ hasParams,
790
+ hasQuery
791
+ }
792
+ };
793
+ this.addEndpoint(ctx, endpoint);
794
+ }
845
795
  }
846
796
  }
847
- async extractProcedures(ctx, file, content) {
848
- const nameMatches = [...content.matchAll(PATTERNS6.procedureName)];
849
- for (const match of nameMatches) {
850
- const procedureName = match[1] ?? "unknownProcedure";
797
+ async extractDtos(ctx, file, content) {
798
+ const dtoMatches = [...content.matchAll(PATTERNS5.dto)];
799
+ for (const match of dtoMatches) {
800
+ const name = match[1] ?? "UnknownDto";
851
801
  const index = match.index ?? 0;
852
802
  const lineNumber = content.slice(0, index).split(`
853
803
  `).length;
854
- const afterMatch = content.slice(index, index + 500);
855
- const isQuery = afterMatch.includes(".query(");
856
- const isMutation = afterMatch.includes(".mutation(");
857
- if (!isQuery && !isMutation)
858
- continue;
859
- const hasZodInput = PATTERNS6.zodInput.test(afterMatch);
860
- const hasZodOutput = PATTERNS6.zodOutput.test(afterMatch);
861
- const hasSchema = hasZodInput || hasZodOutput;
862
- const method = isMutation ? "POST" : "GET";
863
- const endpoint = {
864
- id: `trpc.${procedureName}`,
865
- method,
866
- path: `/trpc/${procedureName}`,
867
- kind: isMutation ? "command" : "query",
868
- handlerName: procedureName,
869
- source: this.createLocation(file, lineNumber, lineNumber + 10),
870
- confidence: this.createConfidence(hasSchema ? "high" : "medium", hasSchema ? "explicit-schema" : "inferred-types"),
871
- frameworkMeta: {
872
- procedureType: isMutation ? "mutation" : "query",
873
- hasInput: hasZodInput,
874
- hasOutput: hasZodOutput
875
- }
804
+ const hasClassValidator = content.includes("class-validator") || content.includes("@IsString") || content.includes("@IsNumber");
805
+ const schema = {
806
+ id: this.generateSchemaId(name, file),
807
+ name,
808
+ schemaType: hasClassValidator ? "class-validator" : "typescript",
809
+ source: this.createLocation(file, lineNumber, lineNumber + 20),
810
+ confidence: this.createConfidence(hasClassValidator ? "high" : "medium", hasClassValidator ? "explicit-schema" : "inferred-types")
876
811
  };
877
- this.addEndpoint(ctx, endpoint);
812
+ this.addSchema(ctx, schema);
878
813
  }
879
814
  }
815
+ normalizePath(path) {
816
+ return "/" + path.replace(/\/+/g, "/").replace(/^\/+|\/+$/g, "");
817
+ }
880
818
  }
881
819
  // src/extractors/next-api/extractor.ts
882
- var PATTERNS7 = {
820
+ var PATTERNS6 = {
883
821
  appRouterExport: /export\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)/gi,
884
822
  pagesHandler: /export\s+default\s+(?:async\s+)?function/g,
885
823
  zodSchema: /z\.\w+\(/g
@@ -913,13 +851,13 @@ class NextApiExtractor extends BaseExtractor {
913
851
  async extractAppRoutes(ctx, file, content) {
914
852
  const pathMatch = file.match(/app\/api\/(.+)\/route\.ts$/);
915
853
  const routePath = pathMatch ? `/api/${pathMatch[1]}` : "/api";
916
- const matches = [...content.matchAll(PATTERNS7.appRouterExport)];
854
+ const matches = [...content.matchAll(PATTERNS6.appRouterExport)];
917
855
  for (const match of matches) {
918
856
  const method = match[1]?.toUpperCase() ?? "GET";
919
857
  const index = match.index ?? 0;
920
858
  const lineNumber = content.slice(0, index).split(`
921
859
  `).length;
922
- const hasZod = PATTERNS7.zodSchema.test(content);
860
+ const hasZod = PATTERNS6.zodSchema.test(content);
923
861
  const endpoint = {
924
862
  id: this.generateEndpointId(method, routePath),
925
863
  method,
@@ -936,10 +874,10 @@ class NextApiExtractor extends BaseExtractor {
936
874
  async extractPagesRoutes(ctx, file, content) {
937
875
  const pathMatch = file.match(/pages\/api\/(.+)\.ts$/);
938
876
  const routePath = pathMatch ? `/api/${pathMatch[1]}` : "/api";
939
- if (!PATTERNS7.pagesHandler.test(content))
877
+ if (!PATTERNS6.pagesHandler.test(content))
940
878
  return;
941
879
  const lineNumber = 1;
942
- const _hasZod = PATTERNS7.zodSchema.test(content);
880
+ const _hasZod = PATTERNS6.zodSchema.test(content);
943
881
  const methods = ["GET", "POST"];
944
882
  for (const method of methods) {
945
883
  const endpoint = {
@@ -956,6 +894,68 @@ class NextApiExtractor extends BaseExtractor {
956
894
  }
957
895
  }
958
896
  }
897
+ // src/extractors/trpc/extractor.ts
898
+ var PATTERNS7 = {
899
+ procedure: /\.(query|mutation)\s*\(\s*(?:\{[^}]*\}|[^)]+)\)/gi,
900
+ procedureName: /(\w+)\s*:\s*(?:publicProcedure|protectedProcedure|procedure)/g,
901
+ zodInput: /\.input\s*\(\s*(\w+)/g,
902
+ zodOutput: /\.output\s*\(\s*(\w+)/g
903
+ };
904
+
905
+ class TrpcExtractor extends BaseExtractor {
906
+ id = "trpc";
907
+ name = "tRPC Extractor";
908
+ frameworks = ["trpc"];
909
+ priority = 15;
910
+ async doExtract(ctx) {
911
+ const { project, options, fs } = ctx;
912
+ const pattern = options.scope?.length ? options.scope.map((s) => `${s}/**/*.ts`).join(",") : "**/*.ts";
913
+ const files = await fs.glob(pattern, { cwd: project.rootPath });
914
+ ctx.ir.stats.filesScanned = files.length;
915
+ for (const file of files) {
916
+ if (file.includes("node_modules") || file.includes(".test."))
917
+ continue;
918
+ const fullPath = `${project.rootPath}/${file}`;
919
+ const content = await fs.readFile(fullPath);
920
+ if (!content.includes("trpc") && !content.includes("Procedure"))
921
+ continue;
922
+ await this.extractProcedures(ctx, file, content);
923
+ }
924
+ }
925
+ async extractProcedures(ctx, file, content) {
926
+ const nameMatches = [...content.matchAll(PATTERNS7.procedureName)];
927
+ for (const match of nameMatches) {
928
+ const procedureName = match[1] ?? "unknownProcedure";
929
+ const index = match.index ?? 0;
930
+ const lineNumber = content.slice(0, index).split(`
931
+ `).length;
932
+ const afterMatch = content.slice(index, index + 500);
933
+ const isQuery = afterMatch.includes(".query(");
934
+ const isMutation = afterMatch.includes(".mutation(");
935
+ if (!isQuery && !isMutation)
936
+ continue;
937
+ const hasZodInput = PATTERNS7.zodInput.test(afterMatch);
938
+ const hasZodOutput = PATTERNS7.zodOutput.test(afterMatch);
939
+ const hasSchema = hasZodInput || hasZodOutput;
940
+ const method = isMutation ? "POST" : "GET";
941
+ const endpoint = {
942
+ id: `trpc.${procedureName}`,
943
+ method,
944
+ path: `/trpc/${procedureName}`,
945
+ kind: isMutation ? "command" : "query",
946
+ handlerName: procedureName,
947
+ source: this.createLocation(file, lineNumber, lineNumber + 10),
948
+ confidence: this.createConfidence(hasSchema ? "high" : "medium", hasSchema ? "explicit-schema" : "inferred-types"),
949
+ frameworkMeta: {
950
+ procedureType: isMutation ? "mutation" : "query",
951
+ hasInput: hasZodInput,
952
+ hasOutput: hasZodOutput
953
+ }
954
+ };
955
+ this.addEndpoint(ctx, endpoint);
956
+ }
957
+ }
958
+ }
959
959
  // src/extractors/zod/extractor.ts
960
960
  var PATTERNS8 = {
961
961
  zodSchema: /(?:export\s+)?const\s+(\w+)\s*=\s*z\.(?:object|string|number|boolean|array|enum|union|intersection|literal|tuple|record)/g,
@@ -4,6 +4,6 @@
4
4
  * Generates ContractSpec definitions from the Intermediate Representation.
5
5
  */
6
6
  export { generateOperation, generateOperations } from './operation-gen';
7
- export { generateSchema, generateSchemas } from './schema-gen';
8
7
  export { generateRegistry } from './registry-gen';
8
+ export { generateSchema, generateSchemas } from './schema-gen';
9
9
  export type { GeneratedFile, GenerationOptions, GenerationResult, } from './types';
@@ -105,6 +105,42 @@ function generateOperationCode(endpoint, specName, options) {
105
105
  return lines.join(`
106
106
  `);
107
107
  }
108
+ // src/codegen/registry-gen.ts
109
+ function generateRegistry(operationFiles) {
110
+ const operationImports = operationFiles.filter((f) => f.type === "operation").map((f) => {
111
+ const name = f.path.replace(".ts", "").replace(/-/g, "_");
112
+ const specName = toPascalCase(name) + "Spec";
113
+ return { path: f.path, name, specName };
114
+ });
115
+ const lines = [
116
+ `/**`,
117
+ ` * Generated operation registry.`,
118
+ ` */`,
119
+ ``,
120
+ `import { OperationSpecRegistry } from '@contractspec/lib.contracts-spec';`,
121
+ ``
122
+ ];
123
+ for (const op of operationImports) {
124
+ const importPath = `./${op.path.replace(".ts", "")}`;
125
+ lines.push(`import { ${op.specName} } from '${importPath}';`);
126
+ }
127
+ lines.push(``);
128
+ lines.push(`export const operationRegistry = new OperationSpecRegistry();`);
129
+ lines.push(``);
130
+ for (const op of operationImports) {
131
+ lines.push(`operationRegistry.register(${op.specName});`);
132
+ }
133
+ lines.push(``);
134
+ return {
135
+ path: "registry.ts",
136
+ content: lines.join(`
137
+ `),
138
+ type: "registry"
139
+ };
140
+ }
141
+ function toPascalCase(str) {
142
+ return str.split(/[-_]/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
143
+ }
108
144
  // src/codegen/schema-gen.ts
109
145
  function generateSchema(schema, _options) {
110
146
  const fileName = `${toFileName2(schema.name)}.ts`;
@@ -185,42 +221,6 @@ function mapToZodType(tsType, optional) {
185
221
  }
186
222
  return optional ? `${zodType}.optional()` : zodType;
187
223
  }
188
- // src/codegen/registry-gen.ts
189
- function generateRegistry(operationFiles) {
190
- const operationImports = operationFiles.filter((f) => f.type === "operation").map((f) => {
191
- const name = f.path.replace(".ts", "").replace(/-/g, "_");
192
- const specName = toPascalCase(name) + "Spec";
193
- return { path: f.path, name, specName };
194
- });
195
- const lines = [
196
- `/**`,
197
- ` * Generated operation registry.`,
198
- ` */`,
199
- ``,
200
- `import { OperationSpecRegistry } from '@contractspec/lib.contracts-spec';`,
201
- ``
202
- ];
203
- for (const op of operationImports) {
204
- const importPath = `./${op.path.replace(".ts", "")}`;
205
- lines.push(`import { ${op.specName} } from '${importPath}';`);
206
- }
207
- lines.push(``);
208
- lines.push(`export const operationRegistry = new OperationSpecRegistry();`);
209
- lines.push(``);
210
- for (const op of operationImports) {
211
- lines.push(`operationRegistry.register(${op.specName});`);
212
- }
213
- lines.push(``);
214
- return {
215
- path: "registry.ts",
216
- content: lines.join(`
217
- `),
218
- type: "registry"
219
- };
220
- }
221
- function toPascalCase(str) {
222
- return str.split(/[-_]/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
223
- }
224
224
  export {
225
225
  generateSchemas,
226
226
  generateSchema,
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Generates defineSchemaModel specs from schema candidates.
5
5
  */
6
- import type { SchemaCandidate, ImportIR } from '../types';
6
+ import type { ImportIR, SchemaCandidate } from '../types';
7
7
  import type { GeneratedFile, GenerationOptions } from './types';
8
8
  /**
9
9
  * Generate a single schema file.
@@ -3,15 +3,15 @@
3
3
  *
4
4
  * Exports all available framework extractors.
5
5
  */
6
+ export type { ExtractionContext, ExtractorFsAdapter } from './base';
6
7
  export { BaseExtractor } from './base';
7
- export type { ExtractorFsAdapter, ExtractionContext } from './base';
8
- export { NestJsExtractor } from './nestjs/extractor';
8
+ export { ElysiaExtractor } from './elysia/extractor';
9
9
  export { ExpressExtractor } from './express/extractor';
10
10
  export { FastifyExtractor } from './fastify/extractor';
11
11
  export { HonoExtractor } from './hono/extractor';
12
- export { ElysiaExtractor } from './elysia/extractor';
13
- export { TrpcExtractor } from './trpc/extractor';
12
+ export { NestJsExtractor } from './nestjs/extractor';
14
13
  export { NextApiExtractor } from './next-api/extractor';
14
+ export { TrpcExtractor } from './trpc/extractor';
15
15
  export { ZodSchemaExtractor } from './zod/extractor';
16
16
  /**
17
17
  * Register all built-in extractors with the global registry.