@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.
- package/README.md +58 -76
- package/dist/browser/codegen/index.js +36 -36
- package/dist/browser/extractors/index.js +167 -167
- package/dist/browser/index.js +203 -203
- package/dist/codegen/index.d.ts +1 -1
- package/dist/codegen/index.js +36 -36
- package/dist/codegen/schema-gen.d.ts +1 -1
- package/dist/extractors/index.d.ts +4 -4
- package/dist/extractors/index.js +167 -167
- package/dist/index.d.ts +3 -3
- package/dist/index.js +203 -203
- package/dist/node/codegen/index.js +36 -36
- package/dist/node/extractors/index.js +167 -167
- package/dist/node/index.js +203 -203
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -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
|
// src/extractors/index.ts
|
|
225
225
|
var exports_extractors = {};
|
|
226
226
|
__export(exports_extractors, {
|
|
@@ -525,101 +525,54 @@ class BaseExtractor {
|
|
|
525
525
|
ctx.ir.schemas.push(schema);
|
|
526
526
|
}
|
|
527
527
|
}
|
|
528
|
-
// src/extractors/
|
|
528
|
+
// src/extractors/elysia/extractor.ts
|
|
529
529
|
var PATTERNS = {
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
body: /@Body\s*\(\s*\)/g,
|
|
533
|
-
param: /@Param\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/g,
|
|
534
|
-
query: /@Query\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/g,
|
|
535
|
-
dto: /class\s+(\w+(?:Dto|DTO|Request|Response|Input|Output))\s*\{/g,
|
|
536
|
-
classValidator: /@(IsString|IsNumber|IsBoolean|IsArray|IsOptional|IsNotEmpty|Min|Max|Length|Matches)/g
|
|
530
|
+
route: /\.(get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
|
|
531
|
+
tSchema: /body:\s*t\.\w+/g
|
|
537
532
|
};
|
|
538
533
|
|
|
539
|
-
class
|
|
540
|
-
id = "
|
|
541
|
-
name = "
|
|
542
|
-
frameworks = ["
|
|
543
|
-
priority =
|
|
534
|
+
class ElysiaExtractor extends BaseExtractor {
|
|
535
|
+
id = "elysia";
|
|
536
|
+
name = "Elysia Extractor";
|
|
537
|
+
frameworks = ["elysia"];
|
|
538
|
+
priority = 15;
|
|
544
539
|
async doExtract(ctx) {
|
|
545
540
|
const { project, options, fs } = ctx;
|
|
546
541
|
const pattern = options.scope?.length ? options.scope.map((s) => `${s}/**/*.ts`).join(",") : "**/*.ts";
|
|
547
542
|
const files = await fs.glob(pattern, { cwd: project.rootPath });
|
|
548
543
|
ctx.ir.stats.filesScanned = files.length;
|
|
549
544
|
for (const file of files) {
|
|
550
|
-
if (file.includes("node_modules") || file.includes(".
|
|
545
|
+
if (file.includes("node_modules") || file.includes(".test."))
|
|
551
546
|
continue;
|
|
552
|
-
}
|
|
553
547
|
const fullPath = `${project.rootPath}/${file}`;
|
|
554
548
|
const content = await fs.readFile(fullPath);
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
}
|
|
559
|
-
async extractControllers(ctx, file, content) {
|
|
560
|
-
const controllerMatches = [...content.matchAll(PATTERNS.controller)];
|
|
561
|
-
for (const controllerMatch of controllerMatches) {
|
|
562
|
-
const basePath = controllerMatch[1] || "";
|
|
563
|
-
const controllerIndex = controllerMatch.index ?? 0;
|
|
564
|
-
const afterDecorator = content.slice(controllerIndex);
|
|
565
|
-
const classMatch = afterDecorator.match(/class\s+(\w+)/);
|
|
566
|
-
const controllerName = classMatch?.[1] ?? "UnknownController";
|
|
567
|
-
const nextController = content.indexOf("@Controller", controllerIndex + 1);
|
|
568
|
-
const controllerBlock = nextController > 0 ? content.slice(controllerIndex, nextController) : content.slice(controllerIndex);
|
|
569
|
-
const routeMatches = [...controllerBlock.matchAll(PATTERNS.route)];
|
|
570
|
-
for (const routeMatch of routeMatches) {
|
|
571
|
-
const method = routeMatch[1]?.toUpperCase();
|
|
572
|
-
const routePath = routeMatch[2] || "";
|
|
573
|
-
const fullPath = this.normalizePath(`/${basePath}/${routePath}`);
|
|
574
|
-
const afterRoute = controllerBlock.slice(routeMatch.index ?? 0);
|
|
575
|
-
const methodMatch = afterRoute.match(/(?:async\s+)?(\w+)\s*\([^)]*\)\s*(?::\s*\w+(?:<[^>]+>)?)?\s*\{/);
|
|
576
|
-
const handlerName = methodMatch?.[1] ?? "unknownHandler";
|
|
577
|
-
const absoluteIndex = controllerIndex + (routeMatch.index ?? 0);
|
|
578
|
-
const lineNumber = content.slice(0, absoluteIndex).split(`
|
|
579
|
-
`).length;
|
|
580
|
-
const hasBody = PATTERNS.body.test(afterRoute.slice(0, 200));
|
|
581
|
-
const hasParams = PATTERNS.param.test(afterRoute.slice(0, 200));
|
|
582
|
-
const hasQuery = PATTERNS.query.test(afterRoute.slice(0, 200));
|
|
583
|
-
const endpoint = {
|
|
584
|
-
id: this.generateEndpointId(method, fullPath, handlerName),
|
|
585
|
-
method,
|
|
586
|
-
path: fullPath,
|
|
587
|
-
kind: this.methodToOpKind(method),
|
|
588
|
-
handlerName,
|
|
589
|
-
controllerName,
|
|
590
|
-
source: this.createLocation(file, lineNumber, lineNumber + 10),
|
|
591
|
-
confidence: this.createConfidence("medium", "decorator-hints"),
|
|
592
|
-
frameworkMeta: {
|
|
593
|
-
hasBody,
|
|
594
|
-
hasParams,
|
|
595
|
-
hasQuery
|
|
596
|
-
}
|
|
597
|
-
};
|
|
598
|
-
this.addEndpoint(ctx, endpoint);
|
|
599
|
-
}
|
|
549
|
+
if (!content.includes("elysia"))
|
|
550
|
+
continue;
|
|
551
|
+
await this.extractRoutes(ctx, file, content);
|
|
600
552
|
}
|
|
601
553
|
}
|
|
602
|
-
async
|
|
603
|
-
const
|
|
604
|
-
for (const match of
|
|
605
|
-
const
|
|
554
|
+
async extractRoutes(ctx, file, content) {
|
|
555
|
+
const matches = [...content.matchAll(PATTERNS.route)];
|
|
556
|
+
for (const match of matches) {
|
|
557
|
+
const method = match[1]?.toUpperCase() ?? "GET";
|
|
558
|
+
const path = match[2] ?? "/";
|
|
606
559
|
const index = match.index ?? 0;
|
|
607
560
|
const lineNumber = content.slice(0, index).split(`
|
|
608
561
|
`).length;
|
|
609
|
-
const
|
|
610
|
-
const
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
562
|
+
const afterMatch = content.slice(index, index + 500);
|
|
563
|
+
const hasTSchema = PATTERNS.tSchema.test(afterMatch);
|
|
564
|
+
const endpoint = {
|
|
565
|
+
id: this.generateEndpointId(method, path),
|
|
566
|
+
method,
|
|
567
|
+
path,
|
|
568
|
+
kind: this.methodToOpKind(method),
|
|
569
|
+
handlerName: "handler",
|
|
570
|
+
source: this.createLocation(file, lineNumber, lineNumber + 5),
|
|
571
|
+
confidence: this.createConfidence(hasTSchema ? "high" : "medium", hasTSchema ? "explicit-schema" : "decorator-hints")
|
|
616
572
|
};
|
|
617
|
-
this.
|
|
573
|
+
this.addEndpoint(ctx, endpoint);
|
|
618
574
|
}
|
|
619
575
|
}
|
|
620
|
-
normalizePath(path) {
|
|
621
|
-
return "/" + path.replace(/\/+/g, "/").replace(/^\/+|\/+$/g, "");
|
|
622
|
-
}
|
|
623
576
|
}
|
|
624
577
|
// src/extractors/express/extractor.ts
|
|
625
578
|
var PATTERNS2 = {
|
|
@@ -768,119 +721,104 @@ class HonoExtractor extends BaseExtractor {
|
|
|
768
721
|
}
|
|
769
722
|
}
|
|
770
723
|
}
|
|
771
|
-
// src/extractors/
|
|
724
|
+
// src/extractors/nestjs/extractor.ts
|
|
772
725
|
var PATTERNS5 = {
|
|
773
|
-
|
|
774
|
-
|
|
726
|
+
controller: /@Controller\s*\(\s*['"`]([^'"`]*)['"`]\s*\)/g,
|
|
727
|
+
route: /@(Get|Post|Put|Patch|Delete|Head|Options)\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/g,
|
|
728
|
+
body: /@Body\s*\(\s*\)/g,
|
|
729
|
+
param: /@Param\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/g,
|
|
730
|
+
query: /@Query\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/g,
|
|
731
|
+
dto: /class\s+(\w+(?:Dto|DTO|Request|Response|Input|Output))\s*\{/g,
|
|
732
|
+
classValidator: /@(IsString|IsNumber|IsBoolean|IsArray|IsOptional|IsNotEmpty|Min|Max|Length|Matches)/g
|
|
775
733
|
};
|
|
776
734
|
|
|
777
|
-
class
|
|
778
|
-
id = "
|
|
779
|
-
name = "
|
|
780
|
-
frameworks = ["
|
|
781
|
-
priority =
|
|
735
|
+
class NestJsExtractor extends BaseExtractor {
|
|
736
|
+
id = "nestjs";
|
|
737
|
+
name = "NestJS Extractor";
|
|
738
|
+
frameworks = ["nestjs"];
|
|
739
|
+
priority = 20;
|
|
782
740
|
async doExtract(ctx) {
|
|
783
741
|
const { project, options, fs } = ctx;
|
|
784
742
|
const pattern = options.scope?.length ? options.scope.map((s) => `${s}/**/*.ts`).join(",") : "**/*.ts";
|
|
785
743
|
const files = await fs.glob(pattern, { cwd: project.rootPath });
|
|
786
744
|
ctx.ir.stats.filesScanned = files.length;
|
|
787
745
|
for (const file of files) {
|
|
788
|
-
if (file.includes("node_modules") || file.includes(".test."))
|
|
746
|
+
if (file.includes("node_modules") || file.includes(".spec.") || file.includes(".test.")) {
|
|
789
747
|
continue;
|
|
748
|
+
}
|
|
790
749
|
const fullPath = `${project.rootPath}/${file}`;
|
|
791
750
|
const content = await fs.readFile(fullPath);
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
await this.extractRoutes(ctx, file, content);
|
|
751
|
+
await this.extractControllers(ctx, file, content);
|
|
752
|
+
await this.extractDtos(ctx, file, content);
|
|
795
753
|
}
|
|
796
754
|
}
|
|
797
|
-
async
|
|
798
|
-
const
|
|
799
|
-
for (const
|
|
800
|
-
const
|
|
801
|
-
const
|
|
802
|
-
const
|
|
803
|
-
const
|
|
755
|
+
async extractControllers(ctx, file, content) {
|
|
756
|
+
const controllerMatches = [...content.matchAll(PATTERNS5.controller)];
|
|
757
|
+
for (const controllerMatch of controllerMatches) {
|
|
758
|
+
const basePath = controllerMatch[1] || "";
|
|
759
|
+
const controllerIndex = controllerMatch.index ?? 0;
|
|
760
|
+
const afterDecorator = content.slice(controllerIndex);
|
|
761
|
+
const classMatch = afterDecorator.match(/class\s+(\w+)/);
|
|
762
|
+
const controllerName = classMatch?.[1] ?? "UnknownController";
|
|
763
|
+
const nextController = content.indexOf("@Controller", controllerIndex + 1);
|
|
764
|
+
const controllerBlock = nextController > 0 ? content.slice(controllerIndex, nextController) : content.slice(controllerIndex);
|
|
765
|
+
const routeMatches = [...controllerBlock.matchAll(PATTERNS5.route)];
|
|
766
|
+
for (const routeMatch of routeMatches) {
|
|
767
|
+
const method = routeMatch[1]?.toUpperCase();
|
|
768
|
+
const routePath = routeMatch[2] || "";
|
|
769
|
+
const fullPath = this.normalizePath(`/${basePath}/${routePath}`);
|
|
770
|
+
const afterRoute = controllerBlock.slice(routeMatch.index ?? 0);
|
|
771
|
+
const methodMatch = afterRoute.match(/(?:async\s+)?(\w+)\s*\([^)]*\)\s*(?::\s*\w+(?:<[^>]+>)?)?\s*\{/);
|
|
772
|
+
const handlerName = methodMatch?.[1] ?? "unknownHandler";
|
|
773
|
+
const absoluteIndex = controllerIndex + (routeMatch.index ?? 0);
|
|
774
|
+
const lineNumber = content.slice(0, absoluteIndex).split(`
|
|
804
775
|
`).length;
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
zodOutput: /\.output\s*\(\s*(\w+)/g
|
|
826
|
-
};
|
|
827
|
-
|
|
828
|
-
class TrpcExtractor extends BaseExtractor {
|
|
829
|
-
id = "trpc";
|
|
830
|
-
name = "tRPC Extractor";
|
|
831
|
-
frameworks = ["trpc"];
|
|
832
|
-
priority = 15;
|
|
833
|
-
async doExtract(ctx) {
|
|
834
|
-
const { project, options, fs } = ctx;
|
|
835
|
-
const pattern = options.scope?.length ? options.scope.map((s) => `${s}/**/*.ts`).join(",") : "**/*.ts";
|
|
836
|
-
const files = await fs.glob(pattern, { cwd: project.rootPath });
|
|
837
|
-
ctx.ir.stats.filesScanned = files.length;
|
|
838
|
-
for (const file of files) {
|
|
839
|
-
if (file.includes("node_modules") || file.includes(".test."))
|
|
840
|
-
continue;
|
|
841
|
-
const fullPath = `${project.rootPath}/${file}`;
|
|
842
|
-
const content = await fs.readFile(fullPath);
|
|
843
|
-
if (!content.includes("trpc") && !content.includes("Procedure"))
|
|
844
|
-
continue;
|
|
845
|
-
await this.extractProcedures(ctx, file, content);
|
|
776
|
+
const hasBody = PATTERNS5.body.test(afterRoute.slice(0, 200));
|
|
777
|
+
const hasParams = PATTERNS5.param.test(afterRoute.slice(0, 200));
|
|
778
|
+
const hasQuery = PATTERNS5.query.test(afterRoute.slice(0, 200));
|
|
779
|
+
const endpoint = {
|
|
780
|
+
id: this.generateEndpointId(method, fullPath, handlerName),
|
|
781
|
+
method,
|
|
782
|
+
path: fullPath,
|
|
783
|
+
kind: this.methodToOpKind(method),
|
|
784
|
+
handlerName,
|
|
785
|
+
controllerName,
|
|
786
|
+
source: this.createLocation(file, lineNumber, lineNumber + 10),
|
|
787
|
+
confidence: this.createConfidence("medium", "decorator-hints"),
|
|
788
|
+
frameworkMeta: {
|
|
789
|
+
hasBody,
|
|
790
|
+
hasParams,
|
|
791
|
+
hasQuery
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
this.addEndpoint(ctx, endpoint);
|
|
795
|
+
}
|
|
846
796
|
}
|
|
847
797
|
}
|
|
848
|
-
async
|
|
849
|
-
const
|
|
850
|
-
for (const match of
|
|
851
|
-
const
|
|
798
|
+
async extractDtos(ctx, file, content) {
|
|
799
|
+
const dtoMatches = [...content.matchAll(PATTERNS5.dto)];
|
|
800
|
+
for (const match of dtoMatches) {
|
|
801
|
+
const name = match[1] ?? "UnknownDto";
|
|
852
802
|
const index = match.index ?? 0;
|
|
853
803
|
const lineNumber = content.slice(0, index).split(`
|
|
854
804
|
`).length;
|
|
855
|
-
const
|
|
856
|
-
const
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
const hasSchema = hasZodInput || hasZodOutput;
|
|
863
|
-
const method = isMutation ? "POST" : "GET";
|
|
864
|
-
const endpoint = {
|
|
865
|
-
id: `trpc.${procedureName}`,
|
|
866
|
-
method,
|
|
867
|
-
path: `/trpc/${procedureName}`,
|
|
868
|
-
kind: isMutation ? "command" : "query",
|
|
869
|
-
handlerName: procedureName,
|
|
870
|
-
source: this.createLocation(file, lineNumber, lineNumber + 10),
|
|
871
|
-
confidence: this.createConfidence(hasSchema ? "high" : "medium", hasSchema ? "explicit-schema" : "inferred-types"),
|
|
872
|
-
frameworkMeta: {
|
|
873
|
-
procedureType: isMutation ? "mutation" : "query",
|
|
874
|
-
hasInput: hasZodInput,
|
|
875
|
-
hasOutput: hasZodOutput
|
|
876
|
-
}
|
|
805
|
+
const hasClassValidator = content.includes("class-validator") || content.includes("@IsString") || content.includes("@IsNumber");
|
|
806
|
+
const schema = {
|
|
807
|
+
id: this.generateSchemaId(name, file),
|
|
808
|
+
name,
|
|
809
|
+
schemaType: hasClassValidator ? "class-validator" : "typescript",
|
|
810
|
+
source: this.createLocation(file, lineNumber, lineNumber + 20),
|
|
811
|
+
confidence: this.createConfidence(hasClassValidator ? "high" : "medium", hasClassValidator ? "explicit-schema" : "inferred-types")
|
|
877
812
|
};
|
|
878
|
-
this.
|
|
813
|
+
this.addSchema(ctx, schema);
|
|
879
814
|
}
|
|
880
815
|
}
|
|
816
|
+
normalizePath(path) {
|
|
817
|
+
return "/" + path.replace(/\/+/g, "/").replace(/^\/+|\/+$/g, "");
|
|
818
|
+
}
|
|
881
819
|
}
|
|
882
820
|
// src/extractors/next-api/extractor.ts
|
|
883
|
-
var
|
|
821
|
+
var PATTERNS6 = {
|
|
884
822
|
appRouterExport: /export\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)/gi,
|
|
885
823
|
pagesHandler: /export\s+default\s+(?:async\s+)?function/g,
|
|
886
824
|
zodSchema: /z\.\w+\(/g
|
|
@@ -914,13 +852,13 @@ class NextApiExtractor extends BaseExtractor {
|
|
|
914
852
|
async extractAppRoutes(ctx, file, content) {
|
|
915
853
|
const pathMatch = file.match(/app\/api\/(.+)\/route\.ts$/);
|
|
916
854
|
const routePath = pathMatch ? `/api/${pathMatch[1]}` : "/api";
|
|
917
|
-
const matches = [...content.matchAll(
|
|
855
|
+
const matches = [...content.matchAll(PATTERNS6.appRouterExport)];
|
|
918
856
|
for (const match of matches) {
|
|
919
857
|
const method = match[1]?.toUpperCase() ?? "GET";
|
|
920
858
|
const index = match.index ?? 0;
|
|
921
859
|
const lineNumber = content.slice(0, index).split(`
|
|
922
860
|
`).length;
|
|
923
|
-
const hasZod =
|
|
861
|
+
const hasZod = PATTERNS6.zodSchema.test(content);
|
|
924
862
|
const endpoint = {
|
|
925
863
|
id: this.generateEndpointId(method, routePath),
|
|
926
864
|
method,
|
|
@@ -937,10 +875,10 @@ class NextApiExtractor extends BaseExtractor {
|
|
|
937
875
|
async extractPagesRoutes(ctx, file, content) {
|
|
938
876
|
const pathMatch = file.match(/pages\/api\/(.+)\.ts$/);
|
|
939
877
|
const routePath = pathMatch ? `/api/${pathMatch[1]}` : "/api";
|
|
940
|
-
if (!
|
|
878
|
+
if (!PATTERNS6.pagesHandler.test(content))
|
|
941
879
|
return;
|
|
942
880
|
const lineNumber = 1;
|
|
943
|
-
const _hasZod =
|
|
881
|
+
const _hasZod = PATTERNS6.zodSchema.test(content);
|
|
944
882
|
const methods = ["GET", "POST"];
|
|
945
883
|
for (const method of methods) {
|
|
946
884
|
const endpoint = {
|
|
@@ -957,6 +895,68 @@ class NextApiExtractor extends BaseExtractor {
|
|
|
957
895
|
}
|
|
958
896
|
}
|
|
959
897
|
}
|
|
898
|
+
// src/extractors/trpc/extractor.ts
|
|
899
|
+
var PATTERNS7 = {
|
|
900
|
+
procedure: /\.(query|mutation)\s*\(\s*(?:\{[^}]*\}|[^)]+)\)/gi,
|
|
901
|
+
procedureName: /(\w+)\s*:\s*(?:publicProcedure|protectedProcedure|procedure)/g,
|
|
902
|
+
zodInput: /\.input\s*\(\s*(\w+)/g,
|
|
903
|
+
zodOutput: /\.output\s*\(\s*(\w+)/g
|
|
904
|
+
};
|
|
905
|
+
|
|
906
|
+
class TrpcExtractor extends BaseExtractor {
|
|
907
|
+
id = "trpc";
|
|
908
|
+
name = "tRPC Extractor";
|
|
909
|
+
frameworks = ["trpc"];
|
|
910
|
+
priority = 15;
|
|
911
|
+
async doExtract(ctx) {
|
|
912
|
+
const { project, options, fs } = ctx;
|
|
913
|
+
const pattern = options.scope?.length ? options.scope.map((s) => `${s}/**/*.ts`).join(",") : "**/*.ts";
|
|
914
|
+
const files = await fs.glob(pattern, { cwd: project.rootPath });
|
|
915
|
+
ctx.ir.stats.filesScanned = files.length;
|
|
916
|
+
for (const file of files) {
|
|
917
|
+
if (file.includes("node_modules") || file.includes(".test."))
|
|
918
|
+
continue;
|
|
919
|
+
const fullPath = `${project.rootPath}/${file}`;
|
|
920
|
+
const content = await fs.readFile(fullPath);
|
|
921
|
+
if (!content.includes("trpc") && !content.includes("Procedure"))
|
|
922
|
+
continue;
|
|
923
|
+
await this.extractProcedures(ctx, file, content);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
async extractProcedures(ctx, file, content) {
|
|
927
|
+
const nameMatches = [...content.matchAll(PATTERNS7.procedureName)];
|
|
928
|
+
for (const match of nameMatches) {
|
|
929
|
+
const procedureName = match[1] ?? "unknownProcedure";
|
|
930
|
+
const index = match.index ?? 0;
|
|
931
|
+
const lineNumber = content.slice(0, index).split(`
|
|
932
|
+
`).length;
|
|
933
|
+
const afterMatch = content.slice(index, index + 500);
|
|
934
|
+
const isQuery = afterMatch.includes(".query(");
|
|
935
|
+
const isMutation = afterMatch.includes(".mutation(");
|
|
936
|
+
if (!isQuery && !isMutation)
|
|
937
|
+
continue;
|
|
938
|
+
const hasZodInput = PATTERNS7.zodInput.test(afterMatch);
|
|
939
|
+
const hasZodOutput = PATTERNS7.zodOutput.test(afterMatch);
|
|
940
|
+
const hasSchema = hasZodInput || hasZodOutput;
|
|
941
|
+
const method = isMutation ? "POST" : "GET";
|
|
942
|
+
const endpoint = {
|
|
943
|
+
id: `trpc.${procedureName}`,
|
|
944
|
+
method,
|
|
945
|
+
path: `/trpc/${procedureName}`,
|
|
946
|
+
kind: isMutation ? "command" : "query",
|
|
947
|
+
handlerName: procedureName,
|
|
948
|
+
source: this.createLocation(file, lineNumber, lineNumber + 10),
|
|
949
|
+
confidence: this.createConfidence(hasSchema ? "high" : "medium", hasSchema ? "explicit-schema" : "inferred-types"),
|
|
950
|
+
frameworkMeta: {
|
|
951
|
+
procedureType: isMutation ? "mutation" : "query",
|
|
952
|
+
hasInput: hasZodInput,
|
|
953
|
+
hasOutput: hasZodOutput
|
|
954
|
+
}
|
|
955
|
+
};
|
|
956
|
+
this.addEndpoint(ctx, endpoint);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
960
|
// src/extractors/zod/extractor.ts
|
|
961
961
|
var PATTERNS8 = {
|
|
962
962
|
zodSchema: /(?:export\s+)?const\s+(\w+)\s*=\s*z\.(?:object|string|number|boolean|array|enum|union|intersection|literal|tuple|record)/g,
|
|
@@ -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
|
export {
|
|
224
224
|
generateSchemas,
|
|
225
225
|
generateSchema,
|