@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.
- 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 +7 -7
package/dist/node/index.js
CHANGED
|
@@ -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/
|
|
527
|
+
// src/extractors/elysia/extractor.ts
|
|
528
528
|
var PATTERNS = {
|
|
529
|
-
|
|
530
|
-
|
|
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
|
|
539
|
-
id = "
|
|
540
|
-
name = "
|
|
541
|
-
frameworks = ["
|
|
542
|
-
priority =
|
|
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(".
|
|
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
|
-
|
|
555
|
-
|
|
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
|
|
602
|
-
const
|
|
603
|
-
for (const match of
|
|
604
|
-
const
|
|
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
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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.
|
|
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/
|
|
723
|
+
// src/extractors/nestjs/extractor.ts
|
|
771
724
|
var PATTERNS5 = {
|
|
772
|
-
|
|
773
|
-
|
|
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
|
|
777
|
-
id = "
|
|
778
|
-
name = "
|
|
779
|
-
frameworks = ["
|
|
780
|
-
priority =
|
|
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
|
-
|
|
792
|
-
|
|
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
|
|
797
|
-
const
|
|
798
|
-
for (const
|
|
799
|
-
const
|
|
800
|
-
const
|
|
801
|
-
const
|
|
802
|
-
const
|
|
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
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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
|
|
848
|
-
const
|
|
849
|
-
for (const match of
|
|
850
|
-
const
|
|
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
|
|
855
|
-
const
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
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.
|
|
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
|
|
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(
|
|
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 =
|
|
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 (!
|
|
877
|
+
if (!PATTERNS6.pagesHandler.test(content))
|
|
940
878
|
return;
|
|
941
879
|
const lineNumber = 1;
|
|
942
|
-
const _hasZod =
|
|
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,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contractspec/lib.source-extractors",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.10",
|
|
4
4
|
"description": "Extract contract candidates from TypeScript source code across multiple frameworks (NestJS, Express, Fastify, Hono, Elysia, tRPC, Next.js)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"contractspec",
|
|
@@ -23,21 +23,21 @@
|
|
|
23
23
|
"dev": "contractspec-bun-build dev",
|
|
24
24
|
"clean": "rimraf dist .turbo",
|
|
25
25
|
"lint": "bun lint:fix",
|
|
26
|
-
"lint:fix": "
|
|
27
|
-
"lint:check": "
|
|
26
|
+
"lint:fix": "biome check --write --unsafe --only=nursery/useSortedClasses . && biome check --write .",
|
|
27
|
+
"lint:check": "biome check .",
|
|
28
28
|
"test": "bun test",
|
|
29
29
|
"prebuild": "contractspec-bun-build prebuild",
|
|
30
30
|
"typecheck": "tsc --noEmit"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@contractspec/lib.contracts-spec": "
|
|
34
|
-
"@contractspec/lib.schema": "3.7.
|
|
33
|
+
"@contractspec/lib.contracts-spec": "4.1.2",
|
|
34
|
+
"@contractspec/lib.schema": "3.7.8",
|
|
35
35
|
"typescript": "^5.9.3",
|
|
36
36
|
"zod": "^4.3.5"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@contractspec/tool.typescript": "3.7.
|
|
40
|
-
"@contractspec/tool.bun": "3.7.
|
|
39
|
+
"@contractspec/tool.typescript": "3.7.8",
|
|
40
|
+
"@contractspec/tool.bun": "3.7.8"
|
|
41
41
|
},
|
|
42
42
|
"types": "./dist/index.d.ts",
|
|
43
43
|
"files": [
|