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.
@@ -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 === schemaName) {
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.isIdentifier(path.node.typeAnnotation.typeName) &&
248
- path.node.typeAnnotation.typeName.name === "infer" &&
249
- path.node.typeAnnotation.typeParameters &&
250
- path.node.typeAnnotation.typeParameters.params.length > 0) {
251
- const param = path.node.typeAnnotation.typeParameters.params[0];
252
- if (t.isTSTypeQuery(param) && t.isIdentifier(param.exprName)) {
253
- const referencedSchemaName = param.exprName.name;
254
- // Find the referenced schema
255
- this.processFileForZodSchema(filePath, referencedSchemaName);
256
- if (this.zodSchemas[referencedSchemaName]) {
257
- this.zodSchemas[schemaName] =
258
- this.zodSchemas[referencedSchemaName];
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-openapi-gen",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "Automatically generate OpenAPI 3.0 documentation from Next.js projects, with support for TypeScript types and Zod schemas.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",