next-openapi-gen 0.7.9 → 0.7.10
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 +58 -13
- package/dist/lib/openapi-generator.js +2 -1
- package/dist/lib/route-processor.js +42 -11
- package/dist/lib/schema-processor.js +4 -2
- package/dist/lib/utils.js +6 -0
- package/dist/lib/zod-converter.js +7 -5
- package/dist/openapi-template.js +1 -0
- package/package.json +11 -4
package/README.md
CHANGED
|
@@ -63,25 +63,27 @@ During initialization (`npx next-openapi-gen init`), a configuration file `next.
|
|
|
63
63
|
"outputDir": "./public",
|
|
64
64
|
"docsUrl": "/api-docs",
|
|
65
65
|
"includeOpenApiRoutes": false,
|
|
66
|
+
"ignoreRoutes": [],
|
|
66
67
|
"debug": false
|
|
67
68
|
}
|
|
68
69
|
```
|
|
69
70
|
|
|
70
71
|
### Configuration Options
|
|
71
72
|
|
|
72
|
-
| Option | Description
|
|
73
|
-
| ---------------------- |
|
|
74
|
-
| `apiDir` | Path to the API directory
|
|
75
|
-
| `schemaDir` | Path to the types/schemas directory
|
|
76
|
-
| `schemaType` | Schema type: `"zod"` or `"typescript"`
|
|
77
|
-
| `outputFile` | Name of the OpenAPI output file
|
|
78
|
-
| `outputDir` | Directory where OpenAPI file will be generated (default: `"./public"`)
|
|
79
|
-
| `docsUrl` | API documentation URL (for Swagger UI)
|
|
80
|
-
| `includeOpenApiRoutes` | Whether to include only routes with @openapi tag
|
|
81
|
-
| `
|
|
82
|
-
| `
|
|
83
|
-
| `
|
|
84
|
-
| `
|
|
73
|
+
| Option | Description |
|
|
74
|
+
| ---------------------- | -------------------------------------------------------------------------- |
|
|
75
|
+
| `apiDir` | Path to the API directory |
|
|
76
|
+
| `schemaDir` | Path to the types/schemas directory |
|
|
77
|
+
| `schemaType` | Schema type: `"zod"` or `"typescript"` |
|
|
78
|
+
| `outputFile` | Name of the OpenAPI output file |
|
|
79
|
+
| `outputDir` | Directory where OpenAPI file will be generated (default: `"./public"`) |
|
|
80
|
+
| `docsUrl` | API documentation URL (for Swagger UI) |
|
|
81
|
+
| `includeOpenApiRoutes` | Whether to include only routes with @openapi tag |
|
|
82
|
+
| `ignoreRoutes` | Array of route patterns to exclude from documentation (supports wildcards) |
|
|
83
|
+
| `defaultResponseSet` | Default error response set for all endpoints |
|
|
84
|
+
| `responseSets` | Named sets of error response codes |
|
|
85
|
+
| `errorConfig` | Error schema configuration |
|
|
86
|
+
| `debug` | Enable detailed logging during generation |
|
|
85
87
|
|
|
86
88
|
## Documenting Your API
|
|
87
89
|
|
|
@@ -168,6 +170,7 @@ export async function GET(
|
|
|
168
170
|
| `@tag` | Custom tag |
|
|
169
171
|
| `@deprecated` | Marks the route as deprecated |
|
|
170
172
|
| `@openapi` | Marks the route for inclusion in documentation (if includeOpenApiRoutes is enabled) |
|
|
173
|
+
| `@ignore` | Excludes the route from OpenAPI documentation |
|
|
171
174
|
|
|
172
175
|
## CLI Usage
|
|
173
176
|
|
|
@@ -537,6 +540,48 @@ export async function PUT() {}
|
|
|
537
540
|
}
|
|
538
541
|
```
|
|
539
542
|
|
|
543
|
+
## Ignoring Routes
|
|
544
|
+
|
|
545
|
+
You can exclude routes from OpenAPI documentation in two ways:
|
|
546
|
+
|
|
547
|
+
### Using @ignore Tag
|
|
548
|
+
|
|
549
|
+
Add the `@ignore` tag to any route you want to exclude:
|
|
550
|
+
|
|
551
|
+
```typescript
|
|
552
|
+
// src/app/api/internal/route.ts
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Internal route - not for documentation
|
|
556
|
+
* @ignore
|
|
557
|
+
*/
|
|
558
|
+
export async function GET() {
|
|
559
|
+
// This route will not appear in OpenAPI documentation
|
|
560
|
+
}
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
### Using ignoreRoutes Configuration
|
|
564
|
+
|
|
565
|
+
Add patterns to your `next.openapi.json` configuration file to exclude multiple routes at once:
|
|
566
|
+
|
|
567
|
+
```json
|
|
568
|
+
{
|
|
569
|
+
"openapi": "3.0.0",
|
|
570
|
+
"info": {
|
|
571
|
+
"title": "Next.js API",
|
|
572
|
+
"version": "1.0.0"
|
|
573
|
+
},
|
|
574
|
+
"apiDir": "src/app/api",
|
|
575
|
+
"ignoreRoutes": ["/internal/*", "/debug", "/admin/test/*"]
|
|
576
|
+
}
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
Pattern matching supports wildcards:
|
|
580
|
+
|
|
581
|
+
- `/internal/*` - Ignores all routes under `/internal/`
|
|
582
|
+
- `/debug` - Ignores only the `/debug` route
|
|
583
|
+
- `/admin/*/temp` - Ignores routes like `/admin/users/temp`, `/admin/posts/temp`
|
|
584
|
+
|
|
540
585
|
## Advanced Usage
|
|
541
586
|
|
|
542
587
|
### Automatic Path Parameter Detection
|
|
@@ -17,7 +17,7 @@ export class OpenApiGenerator {
|
|
|
17
17
|
}
|
|
18
18
|
getConfig() {
|
|
19
19
|
// @ts-ignore
|
|
20
|
-
const { apiDir, schemaDir, docsUrl, ui, outputFile, outputDir, includeOpenApiRoutes, schemaType = "typescript", defaultResponseSet, responseSets, errorConfig, debug } = this.template;
|
|
20
|
+
const { apiDir, schemaDir, docsUrl, ui, outputFile, outputDir, includeOpenApiRoutes, ignoreRoutes, schemaType = "typescript", defaultResponseSet, responseSets, errorConfig, debug } = this.template;
|
|
21
21
|
return {
|
|
22
22
|
apiDir: apiDir || "./src/app/api",
|
|
23
23
|
schemaDir: schemaDir || "./src",
|
|
@@ -26,6 +26,7 @@ export class OpenApiGenerator {
|
|
|
26
26
|
outputFile: outputFile || "openapi.json",
|
|
27
27
|
outputDir: outputDir || "./public",
|
|
28
28
|
includeOpenApiRoutes: includeOpenApiRoutes || false,
|
|
29
|
+
ignoreRoutes: ignoreRoutes || [],
|
|
29
30
|
schemaType,
|
|
30
31
|
defaultResponseSet,
|
|
31
32
|
responseSets,
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import * as t from "@babel/types";
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import path from "path";
|
|
4
|
-
import
|
|
4
|
+
import traverseModule from "@babel/traverse";
|
|
5
|
+
// Handle both ES modules and CommonJS
|
|
6
|
+
const traverse = traverseModule.default || traverseModule;
|
|
5
7
|
import { SchemaProcessor } from "./schema-processor.js";
|
|
6
8
|
import { capitalize, extractJSDocComments, parseTypeScriptFile, extractPathParameters, getOperationId, } from "./utils.js";
|
|
7
9
|
import { logger } from "./logger.js";
|
|
@@ -113,24 +115,49 @@ export class RouteProcessor {
|
|
|
113
115
|
isRoute(varName) {
|
|
114
116
|
return HTTP_METHODS.includes(varName);
|
|
115
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Check if a route should be ignored based on config patterns or @ignore tag
|
|
120
|
+
*/
|
|
121
|
+
shouldIgnoreRoute(routePath, dataTypes) {
|
|
122
|
+
// Check if route has @ignore tag
|
|
123
|
+
if (dataTypes.isIgnored) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
// Check if route matches any ignore patterns
|
|
127
|
+
const ignorePatterns = this.config.ignoreRoutes || [];
|
|
128
|
+
if (ignorePatterns.length === 0) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
return ignorePatterns.some((pattern) => {
|
|
132
|
+
// Support wildcards
|
|
133
|
+
const regexPattern = pattern.replace(/\*/g, ".*").replace(/\//g, "\\/");
|
|
134
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
135
|
+
return regex.test(routePath);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
116
138
|
processFile(filePath) {
|
|
117
139
|
// Check if the file has already been processed
|
|
118
140
|
if (this.processFileTracker[filePath])
|
|
119
141
|
return;
|
|
120
142
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
121
143
|
const ast = parseTypeScriptFile(content);
|
|
122
|
-
traverse
|
|
144
|
+
traverse(ast, {
|
|
123
145
|
ExportNamedDeclaration: (path) => {
|
|
124
146
|
const declaration = path.node.declaration;
|
|
125
147
|
if (t.isFunctionDeclaration(declaration) &&
|
|
126
148
|
t.isIdentifier(declaration.id)) {
|
|
127
149
|
const dataTypes = extractJSDocComments(path);
|
|
128
150
|
if (this.isRoute(declaration.id.name)) {
|
|
151
|
+
const routePath = this.getRoutePath(filePath);
|
|
152
|
+
// Skip if route should be ignored
|
|
153
|
+
if (this.shouldIgnoreRoute(routePath, dataTypes)) {
|
|
154
|
+
logger.debug(`Ignoring route: ${routePath}`);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
129
157
|
// Don't bother adding routes for processing if only including OpenAPI routes and the route is not OpenAPI
|
|
130
158
|
if (!this.config.includeOpenApiRoutes ||
|
|
131
159
|
(this.config.includeOpenApiRoutes && dataTypes.isOpenApi)) {
|
|
132
160
|
// Check for URL parameters in the route path
|
|
133
|
-
const routePath = this.getRoutePath(filePath);
|
|
134
161
|
const pathParams = extractPathParameters(routePath);
|
|
135
162
|
// If we have path parameters but no pathParamsType defined, we should log a warning
|
|
136
163
|
if (pathParams.length > 0 && !dataTypes.pathParamsType) {
|
|
@@ -145,10 +172,15 @@ export class RouteProcessor {
|
|
|
145
172
|
if (t.isVariableDeclarator(decl) && t.isIdentifier(decl.id)) {
|
|
146
173
|
if (this.isRoute(decl.id.name)) {
|
|
147
174
|
const dataTypes = extractJSDocComments(path);
|
|
175
|
+
const routePath = this.getRoutePath(filePath);
|
|
176
|
+
// Skip if route should be ignored
|
|
177
|
+
if (this.shouldIgnoreRoute(routePath, dataTypes)) {
|
|
178
|
+
logger.debug(`Ignoring route: ${routePath}`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
148
181
|
// Don't bother adding routes for processing if only including OpenAPI routes and the route is not OpenAPI
|
|
149
182
|
if (!this.config.includeOpenApiRoutes ||
|
|
150
183
|
(this.config.includeOpenApiRoutes && dataTypes.isOpenApi)) {
|
|
151
|
-
const routePath = this.getRoutePath(filePath);
|
|
152
184
|
const pathParams = extractPathParameters(routePath);
|
|
153
185
|
if (pathParams.length > 0 && !dataTypes.pathParamsType) {
|
|
154
186
|
logger.debug(`Route ${routePath} contains path parameters ${pathParams.join(", ")} but no @pathParams type is defined.`);
|
|
@@ -278,15 +310,15 @@ export class RouteProcessor {
|
|
|
278
310
|
this.swaggerPaths[routePath][method] = definition;
|
|
279
311
|
}
|
|
280
312
|
getRoutePath(filePath) {
|
|
313
|
+
// Normalize path separators first
|
|
314
|
+
const normalizedPath = filePath.replaceAll("\\", "/");
|
|
281
315
|
// First, check if it's an app router path
|
|
282
|
-
if (
|
|
316
|
+
if (normalizedPath.includes("/app/api/")) {
|
|
283
317
|
// Get the relative path from the api directory
|
|
284
|
-
const apiDirPos =
|
|
285
|
-
let relativePath =
|
|
318
|
+
const apiDirPos = normalizedPath.lastIndexOf("/app/api/");
|
|
319
|
+
let relativePath = normalizedPath.substring(apiDirPos + "/app/api".length);
|
|
286
320
|
// Remove the /route.ts or /route.tsx suffix
|
|
287
321
|
relativePath = relativePath.replace(/\/route\.tsx?$/, "");
|
|
288
|
-
// Convert directory separators to URL path format
|
|
289
|
-
relativePath = relativePath.replaceAll("\\", "/");
|
|
290
322
|
// Remove Next.js route groups (folders in parentheses like (authenticated), (marketing))
|
|
291
323
|
relativePath = relativePath.replace(/\/\([^)]+\)/g, "");
|
|
292
324
|
// Convert Next.js dynamic route syntax to OpenAPI parameter syntax
|
|
@@ -296,10 +328,9 @@ export class RouteProcessor {
|
|
|
296
328
|
return relativePath;
|
|
297
329
|
}
|
|
298
330
|
// For pages router or other formats
|
|
299
|
-
const suffixPath =
|
|
331
|
+
const suffixPath = normalizedPath.split("api")[1];
|
|
300
332
|
return suffixPath
|
|
301
333
|
.replace(/route\.tsx?$/, "")
|
|
302
|
-
.replaceAll("\\", "/")
|
|
303
334
|
.replace(/\/$/, "")
|
|
304
335
|
.replace(/\/\([^)]+\)/g, "") // Remove route groups for pages router too
|
|
305
336
|
.replace(/\/\[([^\]]+)\]/g, "/{$1}") // Replace [param] with {param}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
-
import
|
|
3
|
+
import traverseModule from "@babel/traverse";
|
|
4
4
|
import * as t from "@babel/types";
|
|
5
|
+
// Handle both ES modules and CommonJS
|
|
6
|
+
const traverse = traverseModule.default || traverseModule;
|
|
5
7
|
import { parseTypeScriptFile } from "./utils.js";
|
|
6
8
|
import { ZodSchemaConverter } from "./zod-converter.js";
|
|
7
9
|
import { logger } from "./logger.js";
|
|
@@ -96,7 +98,7 @@ export class SchemaProcessor {
|
|
|
96
98
|
});
|
|
97
99
|
}
|
|
98
100
|
collectTypeDefinitions(ast, schemaName) {
|
|
99
|
-
traverse
|
|
101
|
+
traverse(ast, {
|
|
100
102
|
VariableDeclarator: (path) => {
|
|
101
103
|
if (t.isIdentifier(path.node.id, { name: schemaName })) {
|
|
102
104
|
const name = path.node.id.name;
|
package/dist/lib/utils.js
CHANGED
|
@@ -25,6 +25,7 @@ export function extractJSDocComments(path) {
|
|
|
25
25
|
let bodyType = "";
|
|
26
26
|
let auth = "";
|
|
27
27
|
let isOpenApi = false;
|
|
28
|
+
let isIgnored = false;
|
|
28
29
|
let deprecated = false;
|
|
29
30
|
let bodyDescription = "";
|
|
30
31
|
let contentType = "";
|
|
@@ -37,6 +38,9 @@ export function extractJSDocComments(path) {
|
|
|
37
38
|
comments.forEach((comment) => {
|
|
38
39
|
const commentValue = cleanComment(comment.value);
|
|
39
40
|
isOpenApi = commentValue.includes("@openapi");
|
|
41
|
+
if (commentValue.includes("@ignore")) {
|
|
42
|
+
isIgnored = true;
|
|
43
|
+
}
|
|
40
44
|
if (commentValue.includes("@deprecated")) {
|
|
41
45
|
deprecated = true;
|
|
42
46
|
}
|
|
@@ -143,6 +147,7 @@ export function extractJSDocComments(path) {
|
|
|
143
147
|
pathParamsType,
|
|
144
148
|
bodyType,
|
|
145
149
|
isOpenApi,
|
|
150
|
+
isIgnored,
|
|
146
151
|
deprecated,
|
|
147
152
|
bodyDescription,
|
|
148
153
|
contentType,
|
|
@@ -170,6 +175,7 @@ export function cleanSpec(spec) {
|
|
|
170
175
|
"ui",
|
|
171
176
|
"outputFile",
|
|
172
177
|
"includeOpenApiRoutes",
|
|
178
|
+
"ignoreRoutes",
|
|
173
179
|
"schemaType",
|
|
174
180
|
"defaultResponseSet",
|
|
175
181
|
"responseSets",
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
-
import
|
|
3
|
+
import traverseModule from "@babel/traverse";
|
|
4
4
|
import * as t from "@babel/types";
|
|
5
|
+
// Handle both ES modules and CommonJS
|
|
6
|
+
const traverse = traverseModule.default || traverseModule;
|
|
5
7
|
import { parseTypeScriptFile } from "./utils.js";
|
|
6
8
|
import { logger } from "./logger.js";
|
|
7
9
|
/**
|
|
@@ -150,7 +152,7 @@ export class ZodSchemaConverter {
|
|
|
150
152
|
// Create a map to store imported modules
|
|
151
153
|
const importedModules = {};
|
|
152
154
|
// Look for all exported Zod schemas
|
|
153
|
-
traverse
|
|
155
|
+
traverse(ast, {
|
|
154
156
|
// Track imports for resolving local and imported schemas
|
|
155
157
|
ImportDeclaration: (path) => {
|
|
156
158
|
// Keep track of imports to resolve external schemas
|
|
@@ -457,7 +459,7 @@ export class ZodSchemaConverter {
|
|
|
457
459
|
try {
|
|
458
460
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
459
461
|
const ast = parseTypeScriptFile(content);
|
|
460
|
-
traverse
|
|
462
|
+
traverse(ast, {
|
|
461
463
|
ExportNamedDeclaration: (path) => {
|
|
462
464
|
if (t.isVariableDeclaration(path.node.declaration)) {
|
|
463
465
|
path.node.declaration.declarations.forEach((declaration) => {
|
|
@@ -1353,7 +1355,7 @@ export class ZodSchemaConverter {
|
|
|
1353
1355
|
try {
|
|
1354
1356
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
1355
1357
|
const ast = parseTypeScriptFile(content);
|
|
1356
|
-
traverse
|
|
1358
|
+
traverse(ast, {
|
|
1357
1359
|
TSTypeAliasDeclaration: (path) => {
|
|
1358
1360
|
if (t.isIdentifier(path.node.id)) {
|
|
1359
1361
|
const typeName = path.node.id.name;
|
|
@@ -1421,7 +1423,7 @@ export class ZodSchemaConverter {
|
|
|
1421
1423
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
1422
1424
|
const ast = parseTypeScriptFile(content);
|
|
1423
1425
|
// Collect all exported Zod schemas
|
|
1424
|
-
traverse
|
|
1426
|
+
traverse(ast, {
|
|
1425
1427
|
ExportNamedDeclaration: (path) => {
|
|
1426
1428
|
if (t.isVariableDeclaration(path.node.declaration)) {
|
|
1427
1429
|
path.node.declaration.declarations.forEach((declaration) => {
|
package/dist/openapi-template.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "next-openapi-gen",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.10",
|
|
4
4
|
"description": "Automatically generate OpenAPI 3.0 documentation from Next.js projects, with support for Zod schemas and TypeScript types.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -13,8 +13,13 @@
|
|
|
13
13
|
"dist"
|
|
14
14
|
],
|
|
15
15
|
"scripts": {
|
|
16
|
-
"
|
|
17
|
-
"
|
|
16
|
+
"clean": "rm -rf dist",
|
|
17
|
+
"build": "npm run clean && tsc",
|
|
18
|
+
"prepare": "npm run build",
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"test:watch": "vitest",
|
|
21
|
+
"test:ui": "vitest --ui",
|
|
22
|
+
"test:coverage": "vitest run --coverage"
|
|
18
23
|
},
|
|
19
24
|
"repository": {
|
|
20
25
|
"type": "git",
|
|
@@ -51,6 +56,8 @@
|
|
|
51
56
|
},
|
|
52
57
|
"devDependencies": {
|
|
53
58
|
"@types/node": "^24.3.0",
|
|
54
|
-
"
|
|
59
|
+
"@vitest/ui": "^3.2.4",
|
|
60
|
+
"typescript": "^5.9.2",
|
|
61
|
+
"vitest": "^3.2.4"
|
|
55
62
|
}
|
|
56
63
|
}
|