next-openapi-gen 0.4.5 → 0.5.0

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.
@@ -50,7 +50,7 @@ export class RouteProcessor {
50
50
  const routePath = this.getRoutePath(filePath);
51
51
  const pathParams = extractPathParameters(routePath);
52
52
  // If we have path parameters but no pathParamsType defined, we should log a warning
53
- if (pathParams.length > 0 && !dataTypes.pathParams) {
53
+ if (pathParams.length > 0 && !dataTypes.pathParamsType) {
54
54
  console.warn(`Route ${routePath} contains path parameters ${pathParams.join(", ")} but no @pathParams type is defined.`);
55
55
  }
56
56
  this.addRouteToPaths(declaration.id.name, filePath, dataTypes);
@@ -67,7 +67,7 @@ export class RouteProcessor {
67
67
  (this.config.includeOpenApiRoutes && dataTypes.isOpenApi)) {
68
68
  const routePath = this.getRoutePath(filePath);
69
69
  const pathParams = extractPathParameters(routePath);
70
- if (pathParams.length > 0 && !dataTypes.pathParams) {
70
+ if (pathParams.length > 0 && !dataTypes.pathParamsType) {
71
71
  console.warn(`Route ${routePath} contains path parameters ${pathParams.join(", ")} but no @pathParams type is defined.`);
72
72
  }
73
73
  this.addRouteToPaths(decl.id.name, filePath, dataTypes);
@@ -26,6 +26,14 @@ export class SchemaProcessor {
26
26
  * Get all defined schemas (for components.schemas section)
27
27
  */
28
28
  getDefinedSchemas() {
29
+ // If using Zod, also include all processed Zod schemas
30
+ if (this.schemaType === "zod" && this.zodSchemaConverter) {
31
+ const zodSchemas = this.zodSchemaConverter.getProcessedSchemas();
32
+ return {
33
+ ...this.openapiDefinitions,
34
+ ...zodSchemas,
35
+ };
36
+ }
29
37
  return this.openapiDefinitions;
30
38
  }
31
39
  findSchemaDefinition(schemaName, contentType) {
@@ -441,10 +449,7 @@ export class SchemaProcessor {
441
449
  if (description) {
442
450
  options.description = description;
443
451
  }
444
- if (this.contentType === "params") {
445
- options.required = !isOptional;
446
- }
447
- else if (this.contentType === "body") {
452
+ if (this.contentType === "body") {
448
453
  options.nullable = isOptional;
449
454
  }
450
455
  return options;
@@ -526,7 +531,7 @@ export class SchemaProcessor {
526
531
  schema: {
527
532
  type: value.type,
528
533
  },
529
- required: isPathParam ? true : value.required, // Path parameters are always required
534
+ required: isPathParam ? true : !!value.required, // Path parameters are always required
530
535
  };
531
536
  if (value.enum) {
532
537
  param.schema.enum = value.enum;
@@ -589,6 +594,19 @@ export class SchemaProcessor {
589
594
  this.findSchemaDefinition(responseType, "response");
590
595
  responses = this.openapiDefinitions[responseType] || {};
591
596
  }
597
+ if (this.schemaType === "zod") {
598
+ const schemasToProcess = [
599
+ paramsType,
600
+ pathParamsType,
601
+ bodyType,
602
+ responseType,
603
+ ].filter(Boolean);
604
+ schemasToProcess.forEach((schemaName) => {
605
+ if (!this.openapiDefinitions[schemaName]) {
606
+ this.findSchemaDefinition(schemaName, "");
607
+ }
608
+ });
609
+ }
592
610
  return {
593
611
  params,
594
612
  pathParams,
package/dist/lib/utils.js CHANGED
@@ -18,10 +18,10 @@ export function extractJSDocComments(path) {
18
18
  const comments = path.node.leadingComments;
19
19
  let summary = "";
20
20
  let description = "";
21
- let params = "";
22
- let pathParams = "";
23
- let body = "";
24
- let response = "";
21
+ let paramsType = "";
22
+ let pathParamsType = "";
23
+ let bodyType = "";
24
+ let responseType = "";
25
25
  let auth = "";
26
26
  let isOpenApi = false;
27
27
  if (comments) {
@@ -51,16 +51,16 @@ export function extractJSDocComments(path) {
51
51
  description = commentValue.match(regex)[1].trim();
52
52
  }
53
53
  if (commentValue.includes("@params")) {
54
- params = extractTypeFromComment(commentValue, "@params");
54
+ paramsType = extractTypeFromComment(commentValue, "@params");
55
55
  }
56
56
  if (commentValue.includes("@pathParams")) {
57
- pathParams = extractTypeFromComment(commentValue, "@pathParams");
57
+ pathParamsType = extractTypeFromComment(commentValue, "@pathParams");
58
58
  }
59
59
  if (commentValue.includes("@body")) {
60
- body = extractTypeFromComment(commentValue, "@body");
60
+ bodyType = extractTypeFromComment(commentValue, "@body");
61
61
  }
62
62
  if (commentValue.includes("@response")) {
63
- response = extractTypeFromComment(commentValue, "@response");
63
+ responseType = extractTypeFromComment(commentValue, "@response");
64
64
  }
65
65
  });
66
66
  }
@@ -68,10 +68,10 @@ export function extractJSDocComments(path) {
68
68
  auth,
69
69
  summary,
70
70
  description,
71
- params,
72
- pathParams,
73
- body,
74
- response,
71
+ paramsType,
72
+ pathParamsType,
73
+ bodyType,
74
+ responseType,
75
75
  isOpenApi,
76
76
  };
77
77
  }
@@ -9,7 +9,6 @@ import * as t from "@babel/types";
9
9
  export class ZodSchemaConverter {
10
10
  schemaDir;
11
11
  zodSchemas = {};
12
- processedFiles = {};
13
12
  processingSchemas = new Set();
14
13
  processedModules = new Set();
15
14
  constructor(schemaDir) {
@@ -111,10 +110,6 @@ export class ZodSchemaConverter {
111
110
  }
112
111
  else if (file.endsWith(".ts") || file.endsWith(".tsx")) {
113
112
  this.processFileForZodSchema(filePath, schemaName);
114
- // Stop searching if we found the schema
115
- if (this.zodSchemas[schemaName]) {
116
- break;
117
- }
118
113
  }
119
114
  }
120
115
  }
@@ -126,12 +121,12 @@ export class ZodSchemaConverter {
126
121
  * Process a file to find Zod schema definitions
127
122
  */
128
123
  processFileForZodSchema(filePath, schemaName) {
129
- // Skip if already processed
130
- if (this.processedFiles[filePath])
131
- return;
132
- this.processedFiles[filePath] = true;
133
124
  try {
134
125
  const content = fs.readFileSync(filePath, "utf-8");
126
+ // Check if file contains schema we are looking for
127
+ if (!content.includes(schemaName)) {
128
+ return;
129
+ }
135
130
  // Parse the file
136
131
  const ast = parse(content, {
137
132
  sourceType: "module",
@@ -161,9 +156,25 @@ export class ZodSchemaConverter {
161
156
  if (t.isIdentifier(declaration.id) &&
162
157
  declaration.id.name === schemaName &&
163
158
  declaration.init) {
164
- const schema = this.processZodNode(declaration.init);
165
- if (schema) {
166
- this.zodSchemas[schemaName] = schema;
159
+ // Check if this is a call expression with .extend()
160
+ if (t.isCallExpression(declaration.init) &&
161
+ t.isMemberExpression(declaration.init.callee) &&
162
+ t.isIdentifier(declaration.init.callee.property) &&
163
+ declaration.init.callee.property.name === "extend") {
164
+ const schema = this.processZodNode(declaration.init);
165
+ if (schema) {
166
+ this.zodSchemas[schemaName] = schema;
167
+ }
168
+ }
169
+ // Existing code for z.object({...})
170
+ else if (t.isCallExpression(declaration.init) &&
171
+ t.isMemberExpression(declaration.init.callee) &&
172
+ t.isIdentifier(declaration.init.callee.object) &&
173
+ declaration.init.callee.object.name === "z") {
174
+ const schema = this.processZodNode(declaration.init);
175
+ if (schema) {
176
+ this.zodSchemas[schemaName] = schema;
177
+ }
167
178
  }
168
179
  }
169
180
  });
@@ -208,9 +219,22 @@ export class ZodSchemaConverter {
208
219
  if (t.isIdentifier(path.node.id) &&
209
220
  path.node.id.name === schemaName &&
210
221
  path.node.init) {
211
- const schema = this.processZodNode(path.node.init);
212
- if (schema) {
213
- this.zodSchemas[schemaName] = schema;
222
+ // Check if it is .extend()
223
+ if (t.isCallExpression(path.node.init) &&
224
+ t.isMemberExpression(path.node.init.callee) &&
225
+ t.isIdentifier(path.node.init.callee.property) &&
226
+ path.node.init.callee.property.name === "extend") {
227
+ const schema = this.processZodNode(path.node.init);
228
+ if (schema) {
229
+ this.zodSchemas[schemaName] = schema;
230
+ }
231
+ }
232
+ // Existing code
233
+ else {
234
+ const schema = this.processZodNode(path.node.init);
235
+ if (schema) {
236
+ this.zodSchemas[schemaName] = schema;
237
+ }
214
238
  }
215
239
  }
216
240
  },
@@ -243,10 +267,64 @@ export class ZodSchemaConverter {
243
267
  console.error(`Error processing file ${filePath} for schema ${schemaName}: ${error}`);
244
268
  }
245
269
  }
270
+ /**
271
+ * Process all exported schemas in a file, not just the one we're looking for
272
+ */
273
+ processAllSchemasInFile(filePath) {
274
+ try {
275
+ const content = fs.readFileSync(filePath, "utf-8");
276
+ const ast = parse(content, {
277
+ sourceType: "module",
278
+ plugins: ["typescript", "decorators-legacy"],
279
+ });
280
+ traverse.default(ast, {
281
+ ExportNamedDeclaration: (path) => {
282
+ if (t.isVariableDeclaration(path.node.declaration)) {
283
+ path.node.declaration.declarations.forEach((declaration) => {
284
+ if (t.isIdentifier(declaration.id) &&
285
+ declaration.init &&
286
+ t.isCallExpression(declaration.init) &&
287
+ t.isMemberExpression(declaration.init.callee) &&
288
+ t.isIdentifier(declaration.init.callee.object) &&
289
+ declaration.init.callee.object.name === "z") {
290
+ const schemaName = declaration.id.name;
291
+ if (!this.zodSchemas[schemaName] &&
292
+ !this.processingSchemas.has(schemaName)) {
293
+ this.processingSchemas.add(schemaName);
294
+ const schema = this.processZodNode(declaration.init);
295
+ if (schema) {
296
+ this.zodSchemas[schemaName] = schema;
297
+ }
298
+ this.processingSchemas.delete(schemaName);
299
+ }
300
+ }
301
+ });
302
+ }
303
+ },
304
+ });
305
+ }
306
+ catch (error) {
307
+ console.error(`Error processing all schemas in file ${filePath}: ${error}`);
308
+ }
309
+ }
246
310
  /**
247
311
  * Process a Zod node and convert it to OpenAPI schema
248
312
  */
249
313
  processZodNode(node) {
314
+ // Handle reference to another schema (e.g. UserBaseSchema.extend)
315
+ if (t.isCallExpression(node) &&
316
+ t.isMemberExpression(node.callee) &&
317
+ t.isIdentifier(node.callee.object) &&
318
+ t.isIdentifier(node.callee.property) &&
319
+ node.callee.property.name === "extend") {
320
+ const baseSchemaName = node.callee.object.name;
321
+ // Check if the base schema already exists
322
+ if (!this.zodSchemas[baseSchemaName]) {
323
+ // Try to find the basic pattern
324
+ this.convertZodSchemaToOpenApi(baseSchemaName);
325
+ }
326
+ return this.processZodChain(node);
327
+ }
250
328
  // Handle z.object({...})
251
329
  if (t.isCallExpression(node) &&
252
330
  t.isMemberExpression(node.callee) &&
@@ -461,7 +539,7 @@ export class ZodSchemaConverter {
461
539
  const objectExpression = node.arguments[0];
462
540
  const properties = {};
463
541
  const required = [];
464
- objectExpression.properties.forEach((prop) => {
542
+ objectExpression.properties.forEach((prop, index) => {
465
543
  if (t.isObjectProperty(prop)) {
466
544
  let propName;
467
545
  // Handle both identifier and string literal keys
@@ -472,15 +550,56 @@ export class ZodSchemaConverter {
472
550
  propName = prop.key.value;
473
551
  }
474
552
  else {
553
+ console.log(`Skipping property ${index} - unsupported key type`);
475
554
  return; // Skip if key is not identifier or string literal
476
555
  }
556
+ // Check if the property value is an identifier (reference to another schema)
557
+ if (t.isIdentifier(prop.value)) {
558
+ const referencedSchemaName = prop.value.name;
559
+ // Try to find and convert the referenced schema
560
+ if (!this.zodSchemas[referencedSchemaName]) {
561
+ this.convertZodSchemaToOpenApi(referencedSchemaName);
562
+ }
563
+ // Create a reference
564
+ properties[propName] = {
565
+ $ref: `#/components/schemas/${referencedSchemaName}`,
566
+ };
567
+ required.push(propName); // Assuming it's required unless marked optional
568
+ }
569
+ // For array of schemas (like z.array(PaymentMethodSchema))
570
+ if (t.isCallExpression(prop.value) &&
571
+ t.isMemberExpression(prop.value.callee) &&
572
+ t.isIdentifier(prop.value.callee.object) &&
573
+ prop.value.callee.object.name === "z" &&
574
+ t.isIdentifier(prop.value.callee.property) &&
575
+ prop.value.callee.property.name === "array" &&
576
+ prop.value.arguments.length > 0 &&
577
+ t.isIdentifier(prop.value.arguments[0])) {
578
+ const itemSchemaName = prop.value.arguments[0].name;
579
+ // Try to find and convert the referenced schema
580
+ if (!this.zodSchemas[itemSchemaName]) {
581
+ this.convertZodSchemaToOpenApi(itemSchemaName);
582
+ }
583
+ // Process as array with reference
584
+ const arraySchema = this.processZodNode(prop.value);
585
+ arraySchema.items = {
586
+ $ref: `#/components/schemas/${itemSchemaName}`,
587
+ };
588
+ properties[propName] = arraySchema;
589
+ const isOptional = this.isOptional(prop.value) || this.hasOptionalMethod(prop.value);
590
+ if (!isOptional) {
591
+ required.push(propName);
592
+ }
593
+ }
477
594
  // Process property value (a Zod schema)
478
595
  const propSchema = this.processZodNode(prop.value);
479
596
  if (propSchema) {
480
597
  properties[propName] = propSchema;
481
598
  // If the property is not marked as optional, add it to required list
599
+ const isOptional =
482
600
  // @ts-ignore
483
- if (!this.isOptional(prop.value)) {
601
+ this.isOptional(prop.value) || this.hasOptionalMethod(prop.value);
602
+ if (!isOptional) {
484
603
  required.push(propName);
485
604
  }
486
605
  }
@@ -534,7 +653,20 @@ export class ZodSchemaConverter {
534
653
  case "array":
535
654
  let itemsType = { type: "string" };
536
655
  if (node.arguments.length > 0) {
537
- itemsType = this.processZodNode(node.arguments[0]);
656
+ // Check if argument is an identifier (schema reference)
657
+ if (t.isIdentifier(node.arguments[0])) {
658
+ const schemaName = node.arguments[0].name;
659
+ // Try to find and convert the referenced schema
660
+ if (!this.zodSchemas[schemaName]) {
661
+ this.convertZodSchemaToOpenApi(schemaName);
662
+ }
663
+ // @ts-ignore
664
+ itemsType = { $ref: `#/components/schemas/${schemaName}` };
665
+ }
666
+ else {
667
+ // @ts-ignore
668
+ itemsType = this.processZodNode(node.arguments[0]);
669
+ }
538
670
  }
539
671
  schema = { type: "array", items: itemsType };
540
672
  break;
@@ -806,7 +938,10 @@ export class ZodSchemaConverter {
806
938
  case "extend":
807
939
  if (node.arguments.length > 0 &&
808
940
  t.isObjectExpression(node.arguments[0])) {
809
- const extendedProps = this.processZodObject({
941
+ // Get the base schema by processing the object that extend is called on
942
+ const baseSchema = this.processZodNode(node.callee.object);
943
+ // Process the extension object
944
+ const extendNode = {
810
945
  type: "CallExpression",
811
946
  callee: {
812
947
  type: "MemberExpression",
@@ -816,18 +951,28 @@ export class ZodSchemaConverter {
816
951
  optional: false,
817
952
  },
818
953
  arguments: [node.arguments[0]],
819
- });
820
- if (extendedProps && extendedProps.properties) {
821
- schema.properties = {
822
- ...schema.properties,
823
- ...extendedProps.properties,
954
+ };
955
+ const extendedProps = this.processZodObject(extendNode);
956
+ // Merge base schema and extensions
957
+ if (baseSchema && baseSchema.properties) {
958
+ schema = {
959
+ type: "object",
960
+ properties: {
961
+ ...baseSchema.properties,
962
+ ...(extendedProps?.properties || {}),
963
+ },
964
+ required: [
965
+ ...(baseSchema.required || []),
966
+ ...(extendedProps?.required || []),
967
+ ].filter((item, index, arr) => arr.indexOf(item) === index), // Remove duplicates
824
968
  };
825
- if (extendedProps.required) {
826
- schema.required = [
827
- ...(schema.required || []),
828
- ...extendedProps.required,
829
- ];
830
- }
969
+ // Copy other properties from base schema
970
+ if (baseSchema.description)
971
+ schema.description = baseSchema.description;
972
+ }
973
+ else {
974
+ console.log(`Warning: Could not resolve base schema for extend`);
975
+ schema = extendedProps || { type: "object" };
831
976
  }
832
977
  }
833
978
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-openapi-gen",
3
- "version": "0.4.5",
3
+ "version": "0.5.0",
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",