next-openapi-gen 0.5.1 → 0.5.3
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/dist/lib/schema-processor.js +5 -0
- package/dist/lib/zod-converter.js +237 -15
- package/package.json +1 -1
|
@@ -43,6 +43,11 @@ export class SchemaProcessor {
|
|
|
43
43
|
// Check if we should use Zod schemas
|
|
44
44
|
if (this.schemaType === "zod") {
|
|
45
45
|
console.log(`Looking for Zod schema: ${schemaName}`);
|
|
46
|
+
// Check type mapping first
|
|
47
|
+
const mappedSchemaName = this.zodSchemaConverter.typeToSchemaMapping[schemaName];
|
|
48
|
+
if (mappedSchemaName) {
|
|
49
|
+
console.log(`Type '${schemaName}' is mapped to Zod schema '${mappedSchemaName}'`);
|
|
50
|
+
}
|
|
46
51
|
// Try to convert Zod schema
|
|
47
52
|
const zodSchema = this.zodSchemaConverter.convertZodSchemaToOpenApi(schemaName);
|
|
48
53
|
if (zodSchema) {
|
|
@@ -11,6 +11,7 @@ export class ZodSchemaConverter {
|
|
|
11
11
|
zodSchemas = {};
|
|
12
12
|
processingSchemas = new Set();
|
|
13
13
|
processedModules = new Set();
|
|
14
|
+
typeToSchemaMapping = {};
|
|
14
15
|
constructor(schemaDir) {
|
|
15
16
|
this.schemaDir = path.resolve(schemaDir);
|
|
16
17
|
}
|
|
@@ -18,7 +19,17 @@ export class ZodSchemaConverter {
|
|
|
18
19
|
* Find a Zod schema by name and convert it to OpenAPI spec
|
|
19
20
|
*/
|
|
20
21
|
convertZodSchemaToOpenApi(schemaName) {
|
|
22
|
+
// Run pre-scan only one time
|
|
23
|
+
if (Object.keys(this.typeToSchemaMapping).length === 0) {
|
|
24
|
+
this.preScanForTypeMappings();
|
|
25
|
+
}
|
|
21
26
|
console.log(`Looking for Zod schema: ${schemaName}`);
|
|
27
|
+
// Check mapped types
|
|
28
|
+
const mappedSchemaName = this.typeToSchemaMapping[schemaName];
|
|
29
|
+
if (mappedSchemaName) {
|
|
30
|
+
console.log(`Type '${schemaName}' is mapped to schema '${mappedSchemaName}'`);
|
|
31
|
+
schemaName = mappedSchemaName;
|
|
32
|
+
}
|
|
22
33
|
// Check for circular references
|
|
23
34
|
if (this.processingSchemas.has(schemaName)) {
|
|
24
35
|
return { $ref: `#/components/schemas/${schemaName}` };
|
|
@@ -127,6 +138,12 @@ export class ZodSchemaConverter {
|
|
|
127
138
|
if (!content.includes(schemaName)) {
|
|
128
139
|
return;
|
|
129
140
|
}
|
|
141
|
+
// Pre-process all schemas in file
|
|
142
|
+
this.preprocessAllSchemasInFile(filePath);
|
|
143
|
+
// Return it, if the schema has already been processed during pre-processing
|
|
144
|
+
if (this.zodSchemas[schemaName]) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
130
147
|
// Parse the file
|
|
131
148
|
const ast = parse(content, {
|
|
132
149
|
sourceType: "module",
|
|
@@ -240,22 +257,51 @@ export class ZodSchemaConverter {
|
|
|
240
257
|
},
|
|
241
258
|
// For type aliases that reference Zod schemas
|
|
242
259
|
TSTypeAliasDeclaration: (path) => {
|
|
243
|
-
if (t.isIdentifier(path.node.id)
|
|
244
|
-
path.node.id.name
|
|
245
|
-
// Try to find if this is a z.infer<typeof SchemaName> pattern
|
|
260
|
+
if (t.isIdentifier(path.node.id)) {
|
|
261
|
+
const typeName = path.node.id.name;
|
|
246
262
|
if (t.isTSTypeReference(path.node.typeAnnotation) &&
|
|
247
|
-
t.
|
|
248
|
-
path.node.typeAnnotation.typeName.
|
|
249
|
-
path.node.typeAnnotation.
|
|
250
|
-
path.node.typeAnnotation.
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if (
|
|
257
|
-
|
|
258
|
-
|
|
263
|
+
t.isTSQualifiedName(path.node.typeAnnotation.typeName) &&
|
|
264
|
+
t.isIdentifier(path.node.typeAnnotation.typeName.left) &&
|
|
265
|
+
path.node.typeAnnotation.typeName.left.name === "z" &&
|
|
266
|
+
t.isIdentifier(path.node.typeAnnotation.typeName.right) &&
|
|
267
|
+
path.node.typeAnnotation.typeName.right.name === "infer") {
|
|
268
|
+
// Extract schema name from z.infer<typeof SchemaName>
|
|
269
|
+
if (path.node.typeAnnotation.typeParameters &&
|
|
270
|
+
path.node.typeAnnotation.typeParameters.params.length > 0) {
|
|
271
|
+
const param = path.node.typeAnnotation.typeParameters.params[0];
|
|
272
|
+
if (t.isTSTypeQuery(param) && t.isIdentifier(param.exprName)) {
|
|
273
|
+
const referencedSchemaName = param.exprName.name;
|
|
274
|
+
// Save mapping: TypeName -> SchemaName
|
|
275
|
+
this.typeToSchemaMapping[typeName] = referencedSchemaName;
|
|
276
|
+
console.log(`Mapped type '${typeName}' to schema '${referencedSchemaName}'`);
|
|
277
|
+
// Process the referenced schema if not already processed
|
|
278
|
+
if (!this.zodSchemas[referencedSchemaName]) {
|
|
279
|
+
this.processFileForZodSchema(filePath, referencedSchemaName);
|
|
280
|
+
}
|
|
281
|
+
// Use the referenced schema for this type
|
|
282
|
+
if (this.zodSchemas[referencedSchemaName]) {
|
|
283
|
+
this.zodSchemas[typeName] =
|
|
284
|
+
this.zodSchemas[referencedSchemaName];
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (path.node.id.name === schemaName) {
|
|
290
|
+
// Try to find if this is a z.infer<typeof SchemaName> pattern
|
|
291
|
+
if (t.isTSTypeReference(path.node.typeAnnotation) &&
|
|
292
|
+
t.isIdentifier(path.node.typeAnnotation.typeName) &&
|
|
293
|
+
path.node.typeAnnotation.typeName.name === "infer" &&
|
|
294
|
+
path.node.typeAnnotation.typeParameters &&
|
|
295
|
+
path.node.typeAnnotation.typeParameters.params.length > 0) {
|
|
296
|
+
const param = path.node.typeAnnotation.typeParameters.params[0];
|
|
297
|
+
if (t.isTSTypeQuery(param) && t.isIdentifier(param.exprName)) {
|
|
298
|
+
const referencedSchemaName = param.exprName.name;
|
|
299
|
+
// Find the referenced schema
|
|
300
|
+
this.processFileForZodSchema(filePath, referencedSchemaName);
|
|
301
|
+
if (this.zodSchemas[referencedSchemaName]) {
|
|
302
|
+
this.zodSchemas[schemaName] =
|
|
303
|
+
this.zodSchemas[referencedSchemaName];
|
|
304
|
+
}
|
|
259
305
|
}
|
|
260
306
|
}
|
|
261
307
|
}
|
|
@@ -553,6 +599,44 @@ export class ZodSchemaConverter {
|
|
|
553
599
|
console.log(`Skipping property ${index} - unsupported key type`);
|
|
554
600
|
return; // Skip if key is not identifier or string literal
|
|
555
601
|
}
|
|
602
|
+
if (t.isCallExpression(prop.value) &&
|
|
603
|
+
t.isMemberExpression(prop.value.callee) &&
|
|
604
|
+
t.isIdentifier(prop.value.callee.object)) {
|
|
605
|
+
const schemaName = prop.value.callee.object.name;
|
|
606
|
+
// @ts-ignore
|
|
607
|
+
const methodName = prop.value.callee.property.name;
|
|
608
|
+
// Process base schema first
|
|
609
|
+
if (!this.zodSchemas[schemaName]) {
|
|
610
|
+
this.convertZodSchemaToOpenApi(schemaName);
|
|
611
|
+
}
|
|
612
|
+
// For describe method, use reference with description
|
|
613
|
+
if (methodName === "describe" && this.zodSchemas[schemaName]) {
|
|
614
|
+
if (prop.value.arguments.length > 0 &&
|
|
615
|
+
t.isStringLiteral(prop.value.arguments[0])) {
|
|
616
|
+
properties[propName] = {
|
|
617
|
+
allOf: [{ $ref: `#/components/schemas/${schemaName}` }],
|
|
618
|
+
description: prop.value.arguments[0].value,
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
else {
|
|
622
|
+
properties[propName] = {
|
|
623
|
+
$ref: `#/components/schemas/${schemaName}`,
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
required.push(propName);
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
// For other methods, process normally
|
|
630
|
+
const processedSchema = this.processZodNode(prop.value);
|
|
631
|
+
if (processedSchema) {
|
|
632
|
+
properties[propName] = processedSchema;
|
|
633
|
+
const isOptional = this.isOptional(prop.value) || this.hasOptionalMethod(prop.value);
|
|
634
|
+
if (!isOptional) {
|
|
635
|
+
required.push(propName);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
556
640
|
// Check if the property value is an identifier (reference to another schema)
|
|
557
641
|
if (t.isIdentifier(prop.value)) {
|
|
558
642
|
const referencedSchemaName = prop.value.name;
|
|
@@ -1058,4 +1142,142 @@ export class ZodSchemaConverter {
|
|
|
1058
1142
|
getProcessedSchemas() {
|
|
1059
1143
|
return this.zodSchemas;
|
|
1060
1144
|
}
|
|
1145
|
+
/**
|
|
1146
|
+
* Pre-scan all files to build type mappings
|
|
1147
|
+
*/
|
|
1148
|
+
preScanForTypeMappings() {
|
|
1149
|
+
console.log("Pre-scanning for type mappings...");
|
|
1150
|
+
// Scan route files
|
|
1151
|
+
const routeFiles = this.findRouteFiles();
|
|
1152
|
+
for (const routeFile of routeFiles) {
|
|
1153
|
+
this.scanFileForTypeMappings(routeFile);
|
|
1154
|
+
}
|
|
1155
|
+
// Scan schema directory
|
|
1156
|
+
this.scanDirectoryForTypeMappings(this.schemaDir);
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Scan a single file for type mappings
|
|
1160
|
+
*/
|
|
1161
|
+
scanFileForTypeMappings(filePath) {
|
|
1162
|
+
try {
|
|
1163
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
1164
|
+
const ast = parse(content, {
|
|
1165
|
+
sourceType: "module",
|
|
1166
|
+
plugins: ["typescript", "decorators-legacy"],
|
|
1167
|
+
});
|
|
1168
|
+
traverse.default(ast, {
|
|
1169
|
+
TSTypeAliasDeclaration: (path) => {
|
|
1170
|
+
if (t.isIdentifier(path.node.id)) {
|
|
1171
|
+
const typeName = path.node.id.name;
|
|
1172
|
+
// Check for z.infer<typeof SchemaName> pattern
|
|
1173
|
+
if (t.isTSTypeReference(path.node.typeAnnotation)) {
|
|
1174
|
+
const typeRef = path.node.typeAnnotation;
|
|
1175
|
+
// Handle both z.infer and just infer (when z is imported)
|
|
1176
|
+
let isInferType = false;
|
|
1177
|
+
if (t.isTSQualifiedName(typeRef.typeName) &&
|
|
1178
|
+
t.isIdentifier(typeRef.typeName.left) &&
|
|
1179
|
+
typeRef.typeName.left.name === "z" &&
|
|
1180
|
+
t.isIdentifier(typeRef.typeName.right) &&
|
|
1181
|
+
typeRef.typeName.right.name === "infer") {
|
|
1182
|
+
isInferType = true;
|
|
1183
|
+
}
|
|
1184
|
+
else if (t.isIdentifier(typeRef.typeName) &&
|
|
1185
|
+
typeRef.typeName.name === "infer") {
|
|
1186
|
+
isInferType = true;
|
|
1187
|
+
}
|
|
1188
|
+
if (isInferType &&
|
|
1189
|
+
typeRef.typeParameters &&
|
|
1190
|
+
typeRef.typeParameters.params.length > 0) {
|
|
1191
|
+
const param = typeRef.typeParameters.params[0];
|
|
1192
|
+
if (t.isTSTypeQuery(param) && t.isIdentifier(param.exprName)) {
|
|
1193
|
+
const referencedSchemaName = param.exprName.name;
|
|
1194
|
+
this.typeToSchemaMapping[typeName] = referencedSchemaName;
|
|
1195
|
+
console.log(`Pre-scan: Mapped type '${typeName}' to schema '${referencedSchemaName}'`);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
},
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
catch (error) {
|
|
1204
|
+
console.error(`Error scanning file ${filePath} for type mappings:`, error);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Recursively scan directory for type mappings
|
|
1209
|
+
*/
|
|
1210
|
+
scanDirectoryForTypeMappings(dir) {
|
|
1211
|
+
try {
|
|
1212
|
+
const files = fs.readdirSync(dir);
|
|
1213
|
+
for (const file of files) {
|
|
1214
|
+
const filePath = path.join(dir, file);
|
|
1215
|
+
const stats = fs.statSync(filePath);
|
|
1216
|
+
if (stats.isDirectory()) {
|
|
1217
|
+
this.scanDirectoryForTypeMappings(filePath);
|
|
1218
|
+
}
|
|
1219
|
+
else if (file.endsWith(".ts") || file.endsWith(".tsx")) {
|
|
1220
|
+
this.scanFileForTypeMappings(filePath);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
catch (error) {
|
|
1225
|
+
console.error(`Error scanning directory ${dir} for type mappings:`, error);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
1229
|
+
* Pre-process all Zod schemas in a file
|
|
1230
|
+
*/
|
|
1231
|
+
preprocessAllSchemasInFile(filePath) {
|
|
1232
|
+
try {
|
|
1233
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
1234
|
+
const ast = parse(content, {
|
|
1235
|
+
sourceType: "module",
|
|
1236
|
+
plugins: ["typescript", "decorators-legacy"],
|
|
1237
|
+
});
|
|
1238
|
+
// Collect all exported Zod schemas
|
|
1239
|
+
traverse.default(ast, {
|
|
1240
|
+
ExportNamedDeclaration: (path) => {
|
|
1241
|
+
if (t.isVariableDeclaration(path.node.declaration)) {
|
|
1242
|
+
path.node.declaration.declarations.forEach((declaration) => {
|
|
1243
|
+
if (t.isIdentifier(declaration.id) && declaration.init) {
|
|
1244
|
+
const schemaName = declaration.id.name;
|
|
1245
|
+
// Check if is Zos schema
|
|
1246
|
+
if (this.isZodSchema(declaration.init) &&
|
|
1247
|
+
!this.zodSchemas[schemaName]) {
|
|
1248
|
+
console.log(`Pre-processing Zod schema: ${schemaName}`);
|
|
1249
|
+
this.processingSchemas.add(schemaName);
|
|
1250
|
+
const schema = this.processZodNode(declaration.init);
|
|
1251
|
+
if (schema) {
|
|
1252
|
+
this.zodSchemas[schemaName] = schema;
|
|
1253
|
+
}
|
|
1254
|
+
this.processingSchemas.delete(schemaName);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
},
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
catch (error) {
|
|
1263
|
+
console.error(`Error pre-processing file ${filePath}:`, error);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Check if node is Zod schema
|
|
1268
|
+
*/
|
|
1269
|
+
isZodSchema(node) {
|
|
1270
|
+
if (t.isCallExpression(node)) {
|
|
1271
|
+
if (t.isMemberExpression(node.callee) &&
|
|
1272
|
+
t.isIdentifier(node.callee.object) &&
|
|
1273
|
+
node.callee.object.name === "z") {
|
|
1274
|
+
return true;
|
|
1275
|
+
}
|
|
1276
|
+
if (t.isMemberExpression(node.callee) &&
|
|
1277
|
+
t.isCallExpression(node.callee.object)) {
|
|
1278
|
+
return this.isZodSchema(node.callee.object);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
return false;
|
|
1282
|
+
}
|
|
1061
1283
|
}
|
package/package.json
CHANGED