next-openapi-gen 0.2.2 → 0.3.1

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) {
@@ -111,7 +111,7 @@ export class SchemaProcessor {
111
111
  return false;
112
112
  }
113
113
  isDateObject(node) {
114
- return t.isNewExpression(node) && t.isIdentifier(node.callee, { name: "Date" });
114
+ return (t.isNewExpression(node) && t.isIdentifier(node.callee, { name: "Date" }));
115
115
  }
116
116
  isDateNode(node) {
117
117
  return this.isDateString(node) || this.isDateObject(node);
@@ -199,9 +199,7 @@ export class SchemaProcessor {
199
199
  return enumSchema;
200
200
  }
201
201
  getPropertyOptions(node) {
202
- const key = node.key.name;
203
202
  const isOptional = !!node.optional; // check if property is optional
204
- const typeName = node.typeAnnotation?.typeAnnotation?.typeName?.name;
205
203
  let description = null;
206
204
  // get comments for field
207
205
  if (node.trailingComments && node.trailingComments.length) {
@@ -219,6 +217,71 @@ export class SchemaProcessor {
219
217
  }
220
218
  return options;
221
219
  }
220
+ /**
221
+ * Generate example values based on parameter type and name
222
+ */
223
+ getExampleForParam(paramName, type = "string") {
224
+ // Common ID-like parameters
225
+ if (paramName === "id" ||
226
+ paramName.endsWith("Id") ||
227
+ paramName.endsWith("_id")) {
228
+ return type === "string" ? "123abc" : 123;
229
+ }
230
+ // For specific common parameter names
231
+ switch (paramName.toLowerCase()) {
232
+ case "slug":
233
+ return "example-slug";
234
+ case "uuid":
235
+ return "123e4567-e89b-12d3-a456-426614174000";
236
+ case "username":
237
+ return "johndoe";
238
+ case "email":
239
+ return "user@example.com";
240
+ case "name":
241
+ return "example-name";
242
+ case "date":
243
+ return "2023-01-01";
244
+ case "page":
245
+ return 1;
246
+ default:
247
+ // Default examples by type
248
+ if (type === "string")
249
+ return "example";
250
+ if (type === "number")
251
+ return 1;
252
+ if (type === "boolean")
253
+ return true;
254
+ return "example";
255
+ }
256
+ }
257
+ /**
258
+ * Create a default schema for path parameters when no schema is defined
259
+ */
260
+ createDefaultPathParamsSchema(paramNames) {
261
+ return paramNames.map((paramName) => {
262
+ // Guess the parameter type based on the name
263
+ let type = "string";
264
+ if (paramName === "id" ||
265
+ paramName.endsWith("Id") ||
266
+ paramName === "page" ||
267
+ paramName === "limit" ||
268
+ paramName === "size" ||
269
+ paramName === "count") {
270
+ type = "number";
271
+ }
272
+ const example = this.getExampleForParam(paramName, type);
273
+ return {
274
+ name: paramName,
275
+ in: "path",
276
+ required: true,
277
+ schema: {
278
+ type: type,
279
+ },
280
+ example: example,
281
+ description: `Path parameter: ${paramName}`,
282
+ };
283
+ });
284
+ }
222
285
  createRequestParamsSchema(params, isPathParam = false) {
223
286
  const queryParams = [];
224
287
  if (params.properties) {
@@ -229,7 +292,7 @@ export class SchemaProcessor {
229
292
  schema: {
230
293
  type: value.type,
231
294
  },
232
- required: value.required,
295
+ required: isPathParam ? true : value.required, // Path parameters are always required
233
296
  };
234
297
  if (value.enum) {
235
298
  param.schema.enum = value.enum;
@@ -238,6 +301,11 @@ export class SchemaProcessor {
238
301
  param.description = value.description;
239
302
  param.schema.description = value.description;
240
303
  }
304
+ // Add examples for path parameters
305
+ if (isPathParam) {
306
+ const example = this.getExampleForParam(name, value.type);
307
+ param.example = example;
308
+ }
241
309
  queryParams.push(param);
242
310
  }
243
311
  }
@@ -264,7 +332,7 @@ export class SchemaProcessor {
264
332
  },
265
333
  };
266
334
  }
267
- getSchemaContent({ paramsType, pathParamsType, bodyType, responseType }) {
335
+ getSchemaContent({ paramsType, pathParamsType, bodyType, responseType, }) {
268
336
  let params = this.openapiDefinitions[paramsType];
269
337
  let pathParams = this.openapiDefinitions[pathParamsType];
270
338
  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 = "";
@@ -80,6 +93,35 @@ export function cleanSpec(spec) {
80
93
  ];
81
94
  const newSpec = { ...spec };
82
95
  propsToRemove.forEach((key) => delete newSpec[key]);
96
+ // Process paths to ensure good examples for path parameters
97
+ if (newSpec.paths) {
98
+ Object.keys(newSpec.paths).forEach((path) => {
99
+ // Check if path contains parameters
100
+ if (path.includes("{") && path.includes("}")) {
101
+ // For each HTTP method in this path
102
+ Object.keys(newSpec.paths[path]).forEach((method) => {
103
+ const operation = newSpec.paths[path][method];
104
+ // Set example properties for each path parameter
105
+ if (operation.parameters) {
106
+ operation.parameters.forEach((param) => {
107
+ if (param.in === "path" && !param.example) {
108
+ // Generate an example based on parameter name
109
+ if (param.name === "id" || param.name.endsWith("Id")) {
110
+ param.example = 123;
111
+ }
112
+ else if (param.name === "slug") {
113
+ param.example = "example-slug";
114
+ }
115
+ else {
116
+ param.example = "example";
117
+ }
118
+ }
119
+ });
120
+ }
121
+ });
122
+ }
123
+ });
124
+ }
83
125
  return newSpec;
84
126
  }
85
127
  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.1",
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",