next-openapi-gen 0.2.2 → 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.
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  ## Interfaces
11
11
 
12
- - Scalar
12
+ - Scalar 🆕
13
13
  - Swagger
14
14
  - Redoc
15
15
  - Stoplight Elements
@@ -89,6 +89,7 @@ Annotate your API routes using JSDoc comments. Here's an example:
89
89
  </td>
90
90
  <td>
91
91
  <img width="340" alt="api-login-scalar" src="https://raw.githubusercontent.com/tazo90/next-openapi-gen/refs/heads/main/assets/api-login-scalar.png" alt-text="api-login"/>
92
+ <br/><br/><br/>
92
93
  <img width="340" alt="api-login-swagger" src="https://raw.githubusercontent.com/tazo90/next-openapi-gen/refs/heads/main/assets/api-login-swagger.png" alt-text="api-login-swagger"/>
93
94
  </td>
94
95
  </tr>
@@ -148,6 +149,7 @@ Annotate your API routes using JSDoc comments. Here's an example:
148
149
  </td>
149
150
  <td>
150
151
  <img width="340" alt="api-users-scalar" src="https://raw.githubusercontent.com/tazo90/next-openapi-gen/refs/heads/main/assets/api-users-scalar.png" alt-text="api-users-scalar"/>
152
+ <br/><br/><br/>
151
153
  <img width="340" alt="api-users-swagger" src="https://raw.githubusercontent.com/tazo90/next-openapi-gen/refs/heads/main/assets/api-users-swagger.png" alt-text="api-users-swagger"/>
152
154
  </td>
153
155
  </tr>
@@ -201,6 +203,9 @@ The `next.openapi.json` file allows you to configure the behavior of the OpenAPI
201
203
  </thead>
202
204
  <tbody>
203
205
  <tr>
206
+ <td>
207
+ <img width="320" alt="scalar" src="https://raw.githubusercontent.com/tazo90/next-openapi-gen/refs/heads/main/assets/scalar.png" alt-text="scalar">
208
+ </td>
204
209
  <td>
205
210
  <img width="320" alt="swagger" src="https://raw.githubusercontent.com/tazo90/next-openapi-gen/refs/heads/main/assets/swagger.png" alt-text="swagger">
206
211
  </td>
@@ -4,7 +4,7 @@ import path from "path";
4
4
  import traverse from "@babel/traverse";
5
5
  import { parse } from "@babel/parser";
6
6
  import { SchemaProcessor } from "./schema-processor.js";
7
- import { capitalize, extractJSDocComments, getOperationId } from "./utils.js";
7
+ import { capitalize, extractJSDocComments, extractPathParameters, getOperationId, } from "./utils.js";
8
8
  const HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
9
9
  const MUTATION_HTTP_METHODS = ["PATCH", "POST", "PUT"];
10
10
  export class RouteProcessor {
@@ -38,7 +38,8 @@ export class RouteProcessor {
38
38
  const dataTypes = extractJSDocComments(path);
39
39
  if (this.isRoute(declaration.id.name)) {
40
40
  // Don't bother adding routes for processing if only including OpenAPI routes and the route is not OpenAPI
41
- if (!this.config.includeOpenApiRoutes || (this.config.includeOpenApiRoutes && dataTypes.isOpenApi))
41
+ if (!this.config.includeOpenApiRoutes ||
42
+ (this.config.includeOpenApiRoutes && dataTypes.isOpenApi))
42
43
  this.addRouteToPaths(declaration.id.name, filePath, dataTypes);
43
44
  }
44
45
  }
@@ -48,7 +49,8 @@ export class RouteProcessor {
48
49
  if (this.isRoute(decl.id.name)) {
49
50
  const dataTypes = extractJSDocComments(path);
50
51
  // Don't bother adding routes for processing if only including OpenAPI routes and the route is not OpenAPI
51
- if (!this.config.includeOpenApiRoutes || (this.config.includeOpenApiRoutes && dataTypes.isOpenApi))
52
+ if (!this.config.includeOpenApiRoutes ||
53
+ (this.config.includeOpenApiRoutes && dataTypes.isOpenApi))
52
54
  this.addRouteToPaths(decl.id.name, filePath, dataTypes);
53
55
  }
54
56
  }
@@ -75,8 +77,10 @@ export class RouteProcessor {
75
77
  this.scanApiRoutes(filePath);
76
78
  // @ts-ignore
77
79
  }
78
- else if (file.endsWith(".ts")) {
79
- this.processFile(filePath);
80
+ else if (file.endsWith(".ts") || file.endsWith(".tsx")) {
81
+ if (file === "route.ts" || file === "route.tsx") {
82
+ this.processFile(filePath);
83
+ }
80
84
  }
81
85
  });
82
86
  }
@@ -113,7 +117,21 @@ export class RouteProcessor {
113
117
  definition.parameters =
114
118
  this.schemaProcessor.createRequestParamsSchema(params);
115
119
  }
116
- if (pathParams) {
120
+ // Add path parameters
121
+ const pathParamNames = extractPathParameters(routePath);
122
+ if (pathParamNames.length > 0) {
123
+ // If we have path parameters but no schema, create a default schema
124
+ if (!pathParams) {
125
+ const defaultPathParams = this.schemaProcessor.createDefaultPathParamsSchema(pathParamNames);
126
+ definition.parameters.push(...defaultPathParams);
127
+ }
128
+ else {
129
+ const moreParams = this.schemaProcessor.createRequestParamsSchema(pathParams, true);
130
+ definition.parameters.push(...moreParams);
131
+ }
132
+ }
133
+ else if (pathParams) {
134
+ // If no path parameters in route but we have a schema, use it
117
135
  const moreParams = this.schemaProcessor.createRequestParamsSchema(pathParams, true);
118
136
  definition.parameters.push(...moreParams);
119
137
  }
@@ -129,14 +147,29 @@ export class RouteProcessor {
129
147
  this.swaggerPaths[routePath][method] = definition;
130
148
  }
131
149
  getRoutePath(filePath) {
150
+ // First, check if it's an app router path
151
+ if (filePath.includes("/app/api/")) {
152
+ // Get the relative path from the api directory
153
+ const apiDirPos = filePath.indexOf("/app/api/");
154
+ let relativePath = filePath.substring(apiDirPos + "/app/api".length);
155
+ // Remove the /route.ts or /route.tsx suffix
156
+ relativePath = relativePath.replace(/\/route\.tsx?$/, "");
157
+ // Convert directory separators to URL path format
158
+ relativePath = relativePath.replaceAll("\\", "/");
159
+ // Convert Next.js dynamic route syntax to OpenAPI parameter syntax
160
+ relativePath = relativePath.replace(/\/\[([^\]]+)\]/g, "/{$1}");
161
+ // Handle catch-all routes ([...param])
162
+ relativePath = relativePath.replace(/\/\[\.\.\.(.*)\]/g, "/{$1}");
163
+ return relativePath;
164
+ }
165
+ // For pages router or other formats
132
166
  const suffixPath = filePath.split("api")[1];
133
167
  return suffixPath
134
- .replace("route.ts", "")
168
+ .replace(/route\.tsx?$/, "")
135
169
  .replaceAll("\\", "/")
136
170
  .replace(/\/$/, "")
137
- // Turns NextJS-style dynamic routes into OpenAPI-style dynamic routes
138
- .replaceAll("[", "{")
139
- .replaceAll("]", "}");
171
+ .replace(/\/\[([^\]]+)\]/g, "/{$1}") // Replace [param] with {param}
172
+ .replace(/\/\[\.\.\.(.*)\]/g, "/{$1}"); // Replace [...param] with {param}
140
173
  }
141
174
  getSortedPaths(paths) {
142
175
  function comparePaths(a, b) {
@@ -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)) {
@@ -111,22 +124,91 @@ export class SchemaProcessor {
111
124
  return false;
112
125
  }
113
126
  isDateObject(node) {
114
- return t.isNewExpression(node) && t.isIdentifier(node.callee, { name: "Date" });
127
+ return (t.isNewExpression(node) && t.isIdentifier(node.callee, { name: "Date" }));
115
128
  }
116
129
  isDateNode(node) {
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
@@ -199,9 +364,7 @@ export class SchemaProcessor {
199
364
  return enumSchema;
200
365
  }
201
366
  getPropertyOptions(node) {
202
- const key = node.key.name;
203
367
  const isOptional = !!node.optional; // check if property is optional
204
- const typeName = node.typeAnnotation?.typeAnnotation?.typeName?.name;
205
368
  let description = null;
206
369
  // get comments for field
207
370
  if (node.trailingComments && node.trailingComments.length) {
@@ -219,6 +382,71 @@ export class SchemaProcessor {
219
382
  }
220
383
  return options;
221
384
  }
385
+ /**
386
+ * Generate example values based on parameter type and name
387
+ */
388
+ getExampleForParam(paramName, type = "string") {
389
+ // Common ID-like parameters
390
+ if (paramName === "id" ||
391
+ paramName.endsWith("Id") ||
392
+ paramName.endsWith("_id")) {
393
+ return type === "string" ? "123abc" : 123;
394
+ }
395
+ // For specific common parameter names
396
+ switch (paramName.toLowerCase()) {
397
+ case "slug":
398
+ return "example-slug";
399
+ case "uuid":
400
+ return "123e4567-e89b-12d3-a456-426614174000";
401
+ case "username":
402
+ return "johndoe";
403
+ case "email":
404
+ return "user@example.com";
405
+ case "name":
406
+ return "example-name";
407
+ case "date":
408
+ return "2023-01-01";
409
+ case "page":
410
+ return 1;
411
+ default:
412
+ // Default examples by type
413
+ if (type === "string")
414
+ return "example";
415
+ if (type === "number")
416
+ return 1;
417
+ if (type === "boolean")
418
+ return true;
419
+ return "example";
420
+ }
421
+ }
422
+ /**
423
+ * Create a default schema for path parameters when no schema is defined
424
+ */
425
+ createDefaultPathParamsSchema(paramNames) {
426
+ return paramNames.map((paramName) => {
427
+ // Guess the parameter type based on the name
428
+ let type = "string";
429
+ if (paramName === "id" ||
430
+ paramName.endsWith("Id") ||
431
+ paramName === "page" ||
432
+ paramName === "limit" ||
433
+ paramName === "size" ||
434
+ paramName === "count") {
435
+ type = "number";
436
+ }
437
+ const example = this.getExampleForParam(paramName, type);
438
+ return {
439
+ name: paramName,
440
+ in: "path",
441
+ required: true,
442
+ schema: {
443
+ type: type,
444
+ },
445
+ example: example,
446
+ description: `Path parameter: ${paramName}`,
447
+ };
448
+ });
449
+ }
222
450
  createRequestParamsSchema(params, isPathParam = false) {
223
451
  const queryParams = [];
224
452
  if (params.properties) {
@@ -229,7 +457,7 @@ export class SchemaProcessor {
229
457
  schema: {
230
458
  type: value.type,
231
459
  },
232
- required: value.required,
460
+ required: isPathParam ? true : value.required, // Path parameters are always required
233
461
  };
234
462
  if (value.enum) {
235
463
  param.schema.enum = value.enum;
@@ -238,6 +466,11 @@ export class SchemaProcessor {
238
466
  param.description = value.description;
239
467
  param.schema.description = value.description;
240
468
  }
469
+ // Add examples for path parameters
470
+ if (isPathParam) {
471
+ const example = this.getExampleForParam(name, value.type);
472
+ param.example = example;
473
+ }
241
474
  queryParams.push(param);
242
475
  }
243
476
  }
@@ -264,7 +497,7 @@ export class SchemaProcessor {
264
497
  },
265
498
  };
266
499
  }
267
- getSchemaContent({ paramsType, pathParamsType, bodyType, responseType }) {
500
+ getSchemaContent({ paramsType, pathParamsType, bodyType, responseType, }) {
268
501
  let params = this.openapiDefinitions[paramsType];
269
502
  let pathParams = this.openapiDefinitions[pathParamsType];
270
503
  let body = this.openapiDefinitions[bodyType];
package/dist/lib/utils.js CHANGED
@@ -1,6 +1,19 @@
1
1
  export function capitalize(string) {
2
2
  return string.charAt(0).toUpperCase() + string.slice(1);
3
3
  }
4
+ /**
5
+ * Extract path parameters from a route path
6
+ * e.g. /users/{id}/posts/{postId} -> ['id', 'postId']
7
+ */
8
+ export function extractPathParameters(routePath) {
9
+ const paramRegex = /{([^}]+)}/g;
10
+ const params = [];
11
+ let match;
12
+ while ((match = paramRegex.exec(routePath)) !== null) {
13
+ params.push(match[1]);
14
+ }
15
+ return params;
16
+ }
4
17
  export function extractJSDocComments(path) {
5
18
  const comments = path.node.leadingComments;
6
19
  let summary = "";
@@ -16,8 +29,7 @@ export function extractJSDocComments(path) {
16
29
  const commentValue = cleanComment(comment.value);
17
30
  isOpenApi = commentValue.includes("@openapi");
18
31
  if (!summary) {
19
- const summaryIndex = isOpenApi ? 1 : 0;
20
- summary = commentValue.split("\n")[summaryIndex];
32
+ summary = commentValue.split("\n")[0];
21
33
  }
22
34
  if (commentValue.includes("@auth")) {
23
35
  const regex = /@auth:\s*(.*)/;
@@ -64,7 +76,7 @@ export function extractJSDocComments(path) {
64
76
  };
65
77
  }
66
78
  export function extractTypeFromComment(commentValue, tag) {
67
- return commentValue.match(new RegExp(`${tag}\\s*:\\s*(\\w+)`))?.[1] || "";
79
+ return commentValue.match(new RegExp(`${tag}\\s*\\s*(\\w+)`))?.[1] || "";
68
80
  }
69
81
  export function cleanComment(commentValue) {
70
82
  return commentValue.replace(/\*\s*/g, "").trim();
@@ -80,6 +92,35 @@ export function cleanSpec(spec) {
80
92
  ];
81
93
  const newSpec = { ...spec };
82
94
  propsToRemove.forEach((key) => delete newSpec[key]);
95
+ // Process paths to ensure good examples for path parameters
96
+ if (newSpec.paths) {
97
+ Object.keys(newSpec.paths).forEach((path) => {
98
+ // Check if path contains parameters
99
+ if (path.includes("{") && path.includes("}")) {
100
+ // For each HTTP method in this path
101
+ Object.keys(newSpec.paths[path]).forEach((method) => {
102
+ const operation = newSpec.paths[path][method];
103
+ // Set example properties for each path parameter
104
+ if (operation.parameters) {
105
+ operation.parameters.forEach((param) => {
106
+ if (param.in === "path" && !param.example) {
107
+ // Generate an example based on parameter name
108
+ if (param.name === "id" || param.name.endsWith("Id")) {
109
+ param.example = 123;
110
+ }
111
+ else if (param.name === "slug") {
112
+ param.example = "example-slug";
113
+ }
114
+ else {
115
+ param.example = "example";
116
+ }
117
+ }
118
+ });
119
+ }
120
+ });
121
+ }
122
+ });
123
+ }
83
124
  return newSpec;
84
125
  }
85
126
  export function getOperationId(routePath, method) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-openapi-gen",
3
- "version": "0.2.2",
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",