next-openapi-gen 0.2.1 → 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 +6 -1
- package/dist/lib/route-processor.js +43 -10
- package/dist/lib/schema-processor.js +73 -5
- package/dist/lib/utils.js +42 -0
- package/package.json +1 -1
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 ||
|
|
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 ||
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
168
|
+
.replace(/route\.tsx?$/, "")
|
|
135
169
|
.replaceAll("\\", "/")
|
|
136
170
|
.replace(/\/$/, "")
|
|
137
|
-
|
|
138
|
-
.
|
|
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