next-openapi-gen 0.7.9 → 0.7.11

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
@@ -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
- | `defaultResponseSet` | Default error response set for all endpoints |
82
- | `responseSets` | Named sets of error response codes |
83
- | `errorConfig` | Error schema configuration |
84
- | `debug` | Enable detailed logging during generation |
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 traverse from "@babel/traverse";
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.default(ast, {
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 (filePath.includes("/app/api/")) {
316
+ if (normalizedPath.includes("/app/api/")) {
283
317
  // Get the relative path from the api directory
284
- const apiDirPos = filePath.indexOf("/app/api/");
285
- let relativePath = filePath.substring(apiDirPos + "/app/api".length);
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 = filePath.split("api")[1];
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 traverse from "@babel/traverse";
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.default(ast, {
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 traverse from "@babel/traverse";
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.default(ast, {
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.default(ast, {
462
+ traverse(ast, {
461
463
  ExportNamedDeclaration: (path) => {
462
464
  if (t.isVariableDeclaration(path.node.declaration)) {
463
465
  path.node.declaration.declarations.forEach((declaration) => {
@@ -549,6 +551,45 @@ export class ZodSchemaConverter {
549
551
  return this.processZodPrimitive(node);
550
552
  }
551
553
  }
554
+ // Handle schema reference with method calls, e.g., Image.optional(), UserSchema.nullable()
555
+ if (t.isCallExpression(node) &&
556
+ t.isMemberExpression(node.callee) &&
557
+ t.isIdentifier(node.callee.object) &&
558
+ t.isIdentifier(node.callee.property) &&
559
+ node.callee.object.name !== "z" // Make sure it's not a z.* call
560
+ ) {
561
+ const schemaName = node.callee.object.name;
562
+ const methodName = node.callee.property.name;
563
+ // Process base schema first if not already processed
564
+ if (!this.zodSchemas[schemaName]) {
565
+ this.convertZodSchemaToOpenApi(schemaName);
566
+ }
567
+ // If the schema exists, create a reference and apply the method
568
+ if (this.zodSchemas[schemaName]) {
569
+ let schema = {
570
+ allOf: [{ $ref: `#/components/schemas/${schemaName}` }],
571
+ };
572
+ // Apply method-specific transformations
573
+ switch (methodName) {
574
+ case "optional":
575
+ case "nullable":
576
+ case "nullish":
577
+ // Don't add nullable flag here as it would be at the wrong level
578
+ // The fact that it's optional is handled by not including it in required array
579
+ break;
580
+ case "describe":
581
+ if (node.arguments.length > 0 &&
582
+ t.isStringLiteral(node.arguments[0])) {
583
+ schema.description = node.arguments[0].value;
584
+ }
585
+ break;
586
+ default:
587
+ // For other methods, process as a chain
588
+ return this.processZodChain(node);
589
+ }
590
+ return schema;
591
+ }
592
+ }
552
593
  // Handle chained methods, e.g., z.string().email().min(5)
553
594
  if (t.isCallExpression(node) &&
554
595
  t.isMemberExpression(node.callee) &&
@@ -1043,13 +1084,23 @@ export class ZodSchemaConverter {
1043
1084
  // Apply the current method
1044
1085
  switch (methodName) {
1045
1086
  case "optional":
1046
- schema.nullable = true;
1087
+ // Don't add nullable for schema references wrapped in allOf
1088
+ // as it doesn't make sense in that context
1089
+ if (!schema.allOf) {
1090
+ schema.nullable = true;
1091
+ }
1047
1092
  break;
1048
1093
  case "nullable":
1049
- schema.nullable = true;
1094
+ // Don't add nullable for schema references wrapped in allOf
1095
+ if (!schema.allOf) {
1096
+ schema.nullable = true;
1097
+ }
1050
1098
  break;
1051
1099
  case "nullish": // Handles both null and undefined
1052
- schema.nullable = true;
1100
+ // Don't add nullable for schema references wrapped in allOf
1101
+ if (!schema.allOf) {
1102
+ schema.nullable = true;
1103
+ }
1053
1104
  break;
1054
1105
  case "describe":
1055
1106
  if (node.arguments.length > 0 && t.isStringLiteral(node.arguments[0])) {
@@ -1353,7 +1404,7 @@ export class ZodSchemaConverter {
1353
1404
  try {
1354
1405
  const content = fs.readFileSync(filePath, "utf-8");
1355
1406
  const ast = parseTypeScriptFile(content);
1356
- traverse.default(ast, {
1407
+ traverse(ast, {
1357
1408
  TSTypeAliasDeclaration: (path) => {
1358
1409
  if (t.isIdentifier(path.node.id)) {
1359
1410
  const typeName = path.node.id.name;
@@ -1421,7 +1472,7 @@ export class ZodSchemaConverter {
1421
1472
  const content = fs.readFileSync(filePath, "utf-8");
1422
1473
  const ast = parseTypeScriptFile(content);
1423
1474
  // Collect all exported Zod schemas
1424
- traverse.default(ast, {
1475
+ traverse(ast, {
1425
1476
  ExportNamedDeclaration: (path) => {
1426
1477
  if (t.isVariableDeclaration(path.node.declaration)) {
1427
1478
  path.node.declaration.declarations.forEach((declaration) => {
@@ -93,5 +93,6 @@ export default {
93
93
  outputFile: "openapi.json",
94
94
  outputDir: "./public",
95
95
  includeOpenApiRoutes: false,
96
+ ignoreRoutes: [],
96
97
  debug: false,
97
98
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-openapi-gen",
3
- "version": "0.7.9",
3
+ "version": "0.7.11",
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
- "build": "tsc",
17
- "prepare": "npm run build"
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
- "typescript": "^5.9.2"
59
+ "@vitest/ui": "^3.2.4",
60
+ "typescript": "^5.9.2",
61
+ "vitest": "^3.2.4"
55
62
  }
56
63
  }