next-openapi-gen 0.3.1 → 0.3.2

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.
@@ -11,6 +11,7 @@ export class SchemaProcessor {
11
11
  directoryCache = {};
12
12
  statCache = {};
13
13
  processSchemaTracker = {};
14
+ processingTypes = new Set();
14
15
  constructor(schemaDir) {
15
16
  this.schemaDir = path.resolve(schemaDir);
16
17
  }
@@ -71,37 +72,49 @@ export class SchemaProcessor {
71
72
  });
72
73
  }
73
74
  resolveType(typeName) {
74
- const typeNode = this.typeDefinitions[typeName.toString()];
75
- if (!typeNode)
76
- return {};
77
- if (t.isTSEnumDeclaration(typeNode)) {
78
- const enumValues = this.processEnum(typeNode);
79
- return enumValues;
75
+ if (this.processingTypes.has(typeName)) {
76
+ // Return reference to type to avoid infinite recursion
77
+ return { $ref: `#/components/schemas/${typeName}` };
80
78
  }
81
- if (t.isTSTypeLiteral(typeNode) || t.isTSInterfaceBody(typeNode)) {
82
- const properties = {};
83
- if ("members" in typeNode) {
84
- (typeNode.members || []).forEach((member) => {
85
- if (t.isTSPropertySignature(member) && t.isIdentifier(member.key)) {
86
- const propName = member.key.name;
87
- const options = this.getPropertyOptions(member);
88
- const property = {
89
- ...this.resolveTSNodeType(member.typeAnnotation?.typeAnnotation),
90
- ...options,
91
- };
92
- properties[propName] = property;
93
- }
94
- });
79
+ // Add type to precessing types
80
+ this.processingTypes.add(typeName);
81
+ try {
82
+ const typeNode = this.typeDefinitions[typeName.toString()];
83
+ if (!typeNode)
84
+ return {};
85
+ if (t.isTSEnumDeclaration(typeNode)) {
86
+ const enumValues = this.processEnum(typeNode);
87
+ return enumValues;
95
88
  }
96
- return { type: "object", properties };
89
+ if (t.isTSTypeLiteral(typeNode) || t.isTSInterfaceBody(typeNode)) {
90
+ const properties = {};
91
+ if ("members" in typeNode) {
92
+ (typeNode.members || []).forEach((member) => {
93
+ if (t.isTSPropertySignature(member) && t.isIdentifier(member.key)) {
94
+ const propName = member.key.name;
95
+ const options = this.getPropertyOptions(member);
96
+ const property = {
97
+ ...this.resolveTSNodeType(member.typeAnnotation?.typeAnnotation),
98
+ ...options,
99
+ };
100
+ properties[propName] = property;
101
+ }
102
+ });
103
+ }
104
+ return { type: "object", properties };
105
+ }
106
+ if (t.isTSArrayType(typeNode)) {
107
+ return {
108
+ type: "array",
109
+ items: this.resolveTSNodeType(typeNode.elementType),
110
+ };
111
+ }
112
+ return {};
97
113
  }
98
- if (t.isTSArrayType(typeNode)) {
99
- return {
100
- type: "array",
101
- items: this.resolveTSNodeType(typeNode.elementType),
102
- };
114
+ finally {
115
+ // Remove type from processed set after we finish
116
+ this.processingTypes.delete(typeName);
103
117
  }
104
- return {};
105
118
  }
106
119
  isDateString(node) {
107
120
  if (t.isStringLiteral(node)) {
@@ -117,16 +130,85 @@ export class SchemaProcessor {
117
130
  return this.isDateString(node) || this.isDateObject(node);
118
131
  }
119
132
  resolveTSNodeType(node) {
133
+ if (!node)
134
+ return { type: "object" }; // Default type for undefined/null
120
135
  if (t.isTSStringKeyword(node))
121
136
  return { type: "string" };
122
137
  if (t.isTSNumberKeyword(node))
123
138
  return { type: "number" };
124
139
  if (t.isTSBooleanKeyword(node))
125
140
  return { type: "boolean" };
141
+ if (t.isTSAnyKeyword(node) || t.isTSUnknownKeyword(node))
142
+ return { type: "object" };
143
+ if (t.isTSVoidKeyword(node) ||
144
+ t.isTSNullKeyword(node) ||
145
+ t.isTSUndefinedKeyword(node))
146
+ return { type: "null" };
126
147
  if (this.isDateNode(node))
127
- return { type: "Date" };
148
+ return { type: "string", format: "date-time" };
149
+ // Handle literal types like "admin" | "member" | "guest"
150
+ if (t.isTSLiteralType(node)) {
151
+ if (t.isStringLiteral(node.literal)) {
152
+ return {
153
+ type: "string",
154
+ enum: [node.literal.value],
155
+ };
156
+ }
157
+ else if (t.isNumericLiteral(node.literal)) {
158
+ return {
159
+ type: "number",
160
+ enum: [node.literal.value],
161
+ };
162
+ }
163
+ else if (t.isBooleanLiteral(node.literal)) {
164
+ return {
165
+ type: "boolean",
166
+ enum: [node.literal.value],
167
+ };
168
+ }
169
+ }
128
170
  if (t.isTSTypeReference(node) && t.isIdentifier(node.typeName)) {
129
171
  const typeName = node.typeName.name;
172
+ // Special handling for built-in types
173
+ if (typeName === "Date") {
174
+ return { type: "string", format: "date-time" };
175
+ }
176
+ if (typeName === "Array" || typeName === "ReadonlyArray") {
177
+ if (node.typeParameters && node.typeParameters.params.length > 0) {
178
+ return {
179
+ type: "array",
180
+ items: this.resolveTSNodeType(node.typeParameters.params[0]),
181
+ };
182
+ }
183
+ return { type: "array", items: { type: "object" } };
184
+ }
185
+ if (typeName === "Record") {
186
+ if (node.typeParameters && node.typeParameters.params.length > 1) {
187
+ const keyType = this.resolveTSNodeType(node.typeParameters.params[0]);
188
+ const valueType = this.resolveTSNodeType(node.typeParameters.params[1]);
189
+ return {
190
+ type: "object",
191
+ additionalProperties: valueType,
192
+ };
193
+ }
194
+ return { type: "object", additionalProperties: true };
195
+ }
196
+ if (typeName === "Partial" ||
197
+ typeName === "Required" ||
198
+ typeName === "Readonly") {
199
+ if (node.typeParameters && node.typeParameters.params.length > 0) {
200
+ return this.resolveTSNodeType(node.typeParameters.params[0]);
201
+ }
202
+ }
203
+ if (typeName === "Pick" || typeName === "Omit") {
204
+ if (node.typeParameters && node.typeParameters.params.length > 0) {
205
+ return this.resolveTSNodeType(node.typeParameters.params[0]);
206
+ }
207
+ }
208
+ // Check if it is a type that we are already processing
209
+ if (this.processingTypes.has(typeName)) {
210
+ return { $ref: `#/components/schemas/${typeName}` };
211
+ }
130
212
  // Find type definition
131
213
  this.findSchemaDefinition(typeName, this.contentType);
132
214
  return this.resolveType(node.typeName.name);
@@ -148,32 +230,115 @@ export class SchemaProcessor {
148
230
  return { type: "object", properties };
149
231
  }
150
232
  if (t.isTSUnionType(node)) {
233
+ // Handle union types with literal types, like "admin" | "member" | "guest"
234
+ const literals = node.types.filter((type) => t.isTSLiteralType(type));
235
+ // Check if all union elements are literals
236
+ if (literals.length === node.types.length) {
237
+ // All union members are literals, convert to enum
238
+ const enumValues = literals
239
+ .map((type) => {
240
+ if (t.isTSLiteralType(type) && t.isStringLiteral(type.literal)) {
241
+ return type.literal.value;
242
+ }
243
+ else if (t.isTSLiteralType(type) &&
244
+ t.isNumericLiteral(type.literal)) {
245
+ return type.literal.value;
246
+ }
247
+ else if (t.isTSLiteralType(type) &&
248
+ t.isBooleanLiteral(type.literal)) {
249
+ return type.literal.value;
250
+ }
251
+ return null;
252
+ })
253
+ .filter((value) => value !== null);
254
+ if (enumValues.length > 0) {
255
+ // Check if all enum values are of the same type
256
+ const firstType = typeof enumValues[0];
257
+ const sameType = enumValues.every((val) => typeof val === firstType);
258
+ if (sameType) {
259
+ return {
260
+ type: firstType,
261
+ enum: enumValues,
262
+ };
263
+ }
264
+ }
265
+ }
266
+ // Handling null | undefined in type union
267
+ const nullableTypes = node.types.filter((type) => t.isTSNullKeyword(type) ||
268
+ t.isTSUndefinedKeyword(type) ||
269
+ t.isTSVoidKeyword(type));
270
+ const nonNullableTypes = node.types.filter((type) => !t.isTSNullKeyword(type) &&
271
+ !t.isTSUndefinedKeyword(type) &&
272
+ !t.isTSVoidKeyword(type));
273
+ // If a type can be null/undefined, we mark it as nullable
274
+ if (nullableTypes.length > 0 && nonNullableTypes.length === 1) {
275
+ const mainType = this.resolveTSNodeType(nonNullableTypes[0]);
276
+ return {
277
+ ...mainType,
278
+ nullable: true,
279
+ };
280
+ }
281
+ // Standard union type support via oneOf
282
+ return {
283
+ oneOf: node.types
284
+ .filter((type) => !t.isTSNullKeyword(type) &&
285
+ !t.isTSUndefinedKeyword(type) &&
286
+ !t.isTSVoidKeyword(type))
287
+ .map((subNode) => this.resolveTSNodeType(subNode)),
288
+ };
289
+ }
290
+ if (t.isTSIntersectionType(node)) {
291
+ // For intersection types, we combine properties
292
+ const allProperties = {};
293
+ const requiredProperties = [];
294
+ node.types.forEach((typeNode) => {
295
+ const resolvedType = this.resolveTSNodeType(typeNode);
296
+ if (resolvedType.type === "object" && resolvedType.properties) {
297
+ Object.entries(resolvedType.properties).forEach(([key, value]) => {
298
+ allProperties[key] = value;
299
+ // @ts-ignore
300
+ if (value.required) {
301
+ requiredProperties.push(key);
302
+ }
303
+ });
304
+ }
305
+ });
151
306
  return {
152
- anyOf: node.types.map((subNode) => this.resolveTSNodeType(subNode)),
307
+ type: "object",
308
+ properties: allProperties,
309
+ required: requiredProperties.length > 0 ? requiredProperties : undefined,
153
310
  };
154
311
  }
155
- // case where a type is a reference to another defined type
312
+ // Case where a type is a reference to another defined type
156
313
  if (t.isTSTypeReference(node) && t.isIdentifier(node.typeName)) {
157
314
  return { $ref: `#/components/schemas/${node.typeName.name}` };
158
315
  }
159
316
  console.warn("Unrecognized TypeScript type node:", node);
160
- return {};
317
+ return { type: "object" }; // By default we return an object
161
318
  }
162
319
  processSchemaFile(filePath, schemaName) {
163
320
  // Check if the file has already been processed
164
321
  if (this.processSchemaTracker[`${filePath}-${schemaName}`])
165
322
  return;
166
- // Recognizes different elements of TS like variable, type, interface, enum
167
- const content = fs.readFileSync(filePath, "utf-8");
168
- const ast = parse(content, {
169
- sourceType: "module",
170
- plugins: ["typescript", "decorators-legacy"],
171
- });
172
- this.collectTypeDefinitions(ast, schemaName);
173
- const definition = this.resolveType(schemaName);
174
- this.openapiDefinitions[schemaName] = definition;
175
- this.processSchemaTracker[`${filePath}-${schemaName}`] = true;
176
- return definition;
323
+ try {
324
+ // Recognizes different elements of TS like variable, type, interface, enum
325
+ const content = fs.readFileSync(filePath, "utf-8");
326
+ const ast = parse(content, {
327
+ sourceType: "module",
328
+ plugins: ["typescript", "decorators-legacy"],
329
+ });
330
+ this.collectTypeDefinitions(ast, schemaName);
331
+ // Reset the set of processed types before each schema processing
332
+ this.processingTypes.clear();
333
+ const definition = this.resolveType(schemaName);
334
+ this.openapiDefinitions[schemaName] = definition;
335
+ this.processSchemaTracker[`${filePath}-${schemaName}`] = true;
336
+ return definition;
337
+ }
338
+ catch (error) {
339
+ console.error(`Error processing schema file ${filePath} for schema ${schemaName}:`, error);
340
+ return { type: "object" }; // By default we return an empty object on error
341
+ }
177
342
  }
178
343
  processEnum(enumNode) {
179
344
  // Initialization OpenAPI enum object
package/dist/lib/utils.js CHANGED
@@ -29,8 +29,7 @@ export function extractJSDocComments(path) {
29
29
  const commentValue = cleanComment(comment.value);
30
30
  isOpenApi = commentValue.includes("@openapi");
31
31
  if (!summary) {
32
- const summaryIndex = isOpenApi ? 1 : 0;
33
- summary = commentValue.split("\n")[summaryIndex];
32
+ summary = commentValue.split("\n")[0];
34
33
  }
35
34
  if (commentValue.includes("@auth")) {
36
35
  const regex = /@auth:\s*(.*)/;
@@ -77,7 +76,7 @@ export function extractJSDocComments(path) {
77
76
  };
78
77
  }
79
78
  export function extractTypeFromComment(commentValue, tag) {
80
- return commentValue.match(new RegExp(`${tag}\\s*:\\s*(\\w+)`))?.[1] || "";
79
+ return commentValue.match(new RegExp(`${tag}\\s*\\s*(\\w+)`))?.[1] || "";
81
80
  }
82
81
  export function cleanComment(commentValue) {
83
82
  return commentValue.replace(/\*\s*/g, "").trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-openapi-gen",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Super fast and easy way to generate OpenAPI documentation automatically from API routes in NextJS.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",