next-openapi-gen 0.6.2 → 0.6.5

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
@@ -61,7 +61,8 @@ During initialization (`npx next-openapi-gen init`), a configuration file `next.
61
61
  "schemaType": "typescript", // or "zod" for Zod schemas
62
62
  "outputFile": "openapi.json",
63
63
  "docsUrl": "/api-docs",
64
- "includeOpenApiRoutes": false
64
+ "includeOpenApiRoutes": false,
65
+ "debug": false
65
66
  }
66
67
  ```
67
68
 
@@ -75,6 +76,10 @@ During initialization (`npx next-openapi-gen init`), a configuration file `next.
75
76
  | `outputFile` | Path to the OpenAPI output file |
76
77
  | `docsUrl` | API documentation URL (for Swagger UI) |
77
78
  | `includeOpenApiRoutes` | Whether to include only routes with @openapi tag |
79
+ | `defaultResponseSet` | Default error response set for all endpoints |
80
+ | `responseSets` | Named sets of error response codes |
81
+ | `errorConfig` | Error schema configuration |
82
+ | `debug` | Enable detailed logging during generation |
78
83
 
79
84
  ## Documenting Your API
80
85
 
@@ -154,6 +159,8 @@ export async function GET(
154
159
  | `@bodyDescription` | Request body description |
155
160
  | `@response` | Response type/schema |
156
161
  | `@responseDescription` | Response description |
162
+ | `@responseSet` | Override default response set (`public`, `auth`, `none`) |
163
+ | `@add` | Add custom response codes (`409:ConflictResponse`, `429`) |
157
164
  | `@contentType` | Request body content type (`application/json`, `multipart/form-data`) |
158
165
  | `@auth` | Authorization type (`bearer`, `basic`, `apikey`) |
159
166
  | `@tag` | Custom tag |
@@ -373,6 +380,89 @@ export async function POST() {
373
380
  }
374
381
  ```
375
382
 
383
+ ## Response Management
384
+
385
+ ### Zero Config + Response Sets
386
+
387
+ Configure reusable error sets in `next.openapi.json`:
388
+
389
+ ```json
390
+ {
391
+ "defaultResponseSet": "common",
392
+ "responseSets": {
393
+ "common": ["400", "401", "500"],
394
+ "public": ["400", "500"],
395
+ "auth": ["400", "401", "403", "500"]
396
+ }
397
+ }
398
+ ```
399
+
400
+ ### Usage Examples
401
+
402
+ ```typescript
403
+ /**
404
+ * Auto-default responses
405
+ * @response UserResponse
406
+ * @openapi
407
+ */
408
+ export async function GET() {}
409
+ // Generates: 200:UserResponse + common errors (400, 401, 500)
410
+
411
+ /**
412
+ * Override response set
413
+ * @response ProductResponse
414
+ * @responseSet public
415
+ * @openapi
416
+ */
417
+ export async function GET() {}
418
+ // Generates: 200:ProductResponse + public errors (400, 500)
419
+
420
+ /**
421
+ * Add custom responses
422
+ * @response 201:UserResponse
423
+ * @add 409:ConflictResponse
424
+ * @openapi
425
+ */
426
+ export async function POST() {}
427
+ // Generates: 201:UserResponse + common errors + 409:ConflictResponse
428
+
429
+ /**
430
+ * Combine multiple sets
431
+ * @response UserResponse
432
+ * @responseSet auth,crud
433
+ * @add 429:RateLimitResponse
434
+ * @openapi
435
+ */
436
+ export async function PUT() {}
437
+ // Combines: auth + crud errors + custom 429
438
+ ```
439
+
440
+ ### Error Schema Configuration
441
+
442
+ #### Define consistent error schemas using templates:
443
+
444
+ ```json
445
+ {
446
+ "errorConfig": {
447
+ "template": {
448
+ "type": "object",
449
+ "properties": {
450
+ "error": {
451
+ "type": "string",
452
+ "example": "{{ERROR_MESSAGE}}"
453
+ }
454
+ }
455
+ },
456
+ "codes": {
457
+ "invalid_request": {
458
+ "description": "Invalid request",
459
+ "variables": { "ERROR_MESSAGE": "Validation failed" }
460
+ }
461
+ }
462
+ }
463
+ }
464
+ ```
465
+
376
466
  ## Advanced Usage
377
467
 
378
468
  ### Automatic Path Parameter Detection
@@ -0,0 +1,39 @@
1
+ class Logger {
2
+ config = null;
3
+ init(config) {
4
+ this.config = config;
5
+ }
6
+ getCallerInfo() {
7
+ const stack = new Error().stack;
8
+ if (!stack)
9
+ return 'Unknown';
10
+ const lines = stack.split('\n');
11
+ // Skip: Error, getCallerInfo, log/warn/error
12
+ const callerLine = lines[3] || lines[2];
13
+ // Extract class/function name
14
+ const match = callerLine.match(/at (\w+)\.(\w+)|at (\w+)/);
15
+ if (match) {
16
+ return match[1] || match[3] || 'Unknown';
17
+ }
18
+ return 'Unknown';
19
+ }
20
+ log(message, ...args) {
21
+ if (this.config?.debug) {
22
+ const source = this.getCallerInfo();
23
+ console.log(`[${source}] ${message}`, ...args);
24
+ }
25
+ }
26
+ warn(message, ...args) {
27
+ if (this.config?.debug) {
28
+ const source = this.getCallerInfo();
29
+ console.warn(`[${source}] ${message}`, ...args);
30
+ }
31
+ }
32
+ error(message, ...args) {
33
+ if (this.config?.debug) {
34
+ const source = this.getCallerInfo();
35
+ console.error(`[${source}] ${message}`, ...args);
36
+ }
37
+ }
38
+ }
39
+ export const logger = new Logger();
@@ -2,6 +2,7 @@ import path from "path";
2
2
  import fs from "fs";
3
3
  import { RouteProcessor } from "./route-processor.js";
4
4
  import { cleanSpec } from "./utils.js";
5
+ import { logger } from "./logger.js";
5
6
  export class OpenApiGenerator {
6
7
  config;
7
8
  template;
@@ -11,10 +12,12 @@ export class OpenApiGenerator {
11
12
  this.template = JSON.parse(fs.readFileSync(templatePath, "utf-8"));
12
13
  this.config = this.getConfig();
13
14
  this.routeProcessor = new RouteProcessor(this.config);
15
+ // Initialize logger
16
+ logger.init(this.config);
14
17
  }
15
18
  getConfig() {
16
19
  // @ts-ignore
17
- const { apiDir, schemaDir, docsUrl, ui, outputFile, includeOpenApiRoutes, schemaType = "typescript" } = this.template;
20
+ const { apiDir, schemaDir, docsUrl, ui, outputFile, includeOpenApiRoutes, schemaType = "typescript", defaultResponseSet, responseSets, errorConfig, debug } = this.template;
18
21
  return {
19
22
  apiDir,
20
23
  schemaDir,
@@ -23,15 +26,20 @@ export class OpenApiGenerator {
23
26
  outputFile,
24
27
  includeOpenApiRoutes,
25
28
  schemaType,
29
+ defaultResponseSet,
30
+ responseSets,
31
+ errorConfig,
32
+ debug,
26
33
  };
27
34
  }
28
35
  generate() {
36
+ logger.log("Starting OpenAPI generation...");
29
37
  const { apiDir } = this.config;
30
38
  // Check if app router structure exists
31
39
  let appRouterApiDir = "";
32
40
  if (fs.existsSync(path.join(path.dirname(apiDir), "app", "api"))) {
33
41
  appRouterApiDir = path.join(path.dirname(apiDir), "app", "api");
34
- console.log(`Found app router API directory at ${appRouterApiDir}`);
42
+ logger.log(`Found app router API directory at ${appRouterApiDir}`);
35
43
  }
36
44
  // Scan pages router routes
37
45
  this.routeProcessor.scanApiRoutes(apiDir);
@@ -57,6 +65,21 @@ export class OpenApiGenerator {
57
65
  if (!this.template.components.schemas) {
58
66
  this.template.components.schemas = {};
59
67
  }
68
+ // Generate error responses using errorConfig or manual definitions
69
+ if (!this.template.components.responses) {
70
+ this.template.components.responses = {};
71
+ }
72
+ const errorConfig = this.config.errorConfig;
73
+ if (errorConfig) {
74
+ this.generateErrorResponsesFromConfig(errorConfig);
75
+ }
76
+ else if (this.config.errorDefinitions) {
77
+ // Use manual definitions (existing logic - if exists)
78
+ Object.entries(this.config.errorDefinitions).forEach(([code, errorDef]) => {
79
+ this.template.components.responses[code] =
80
+ this.createErrorResponseComponent(code, errorDef);
81
+ });
82
+ }
60
83
  // Get defined schemas from the processor
61
84
  const definedSchemas = this.routeProcessor
62
85
  .getSchemaProcessor()
@@ -68,6 +91,73 @@ export class OpenApiGenerator {
68
91
  };
69
92
  }
70
93
  const openapiSpec = cleanSpec(this.template);
94
+ logger.log("OpenAPI generation completed");
71
95
  return openapiSpec;
72
96
  }
97
+ generateErrorResponsesFromConfig(errorConfig) {
98
+ const { template, codes, variables: globalVars = {} } = errorConfig;
99
+ Object.entries(codes).forEach(([errorCode, config]) => {
100
+ const httpStatus = (config.httpStatus || this.guessHttpStatus(errorCode)).toString();
101
+ // Merge variables: global + per-code + built-in
102
+ const allVariables = {
103
+ ...globalVars,
104
+ ...config.variables,
105
+ ERROR_CODE: errorCode,
106
+ DESCRIPTION: config.description,
107
+ HTTP_STATUS: httpStatus,
108
+ };
109
+ const processedSchema = this.processTemplate(template, allVariables);
110
+ this.template.components.responses[httpStatus] = {
111
+ description: config.description,
112
+ content: {
113
+ "application/json": {
114
+ schema: processedSchema,
115
+ },
116
+ },
117
+ };
118
+ });
119
+ }
120
+ processTemplate(template, variables) {
121
+ const jsonStr = JSON.stringify(template);
122
+ let result = jsonStr;
123
+ Object.entries(variables).forEach(([key, value]) => {
124
+ result = result.replace(new RegExp(`{{${key}}}`, "g"), value);
125
+ });
126
+ return JSON.parse(result);
127
+ }
128
+ guessHttpStatus(errorCode) {
129
+ const statusMap = {
130
+ bad: 400,
131
+ invalid: 400,
132
+ validation: 422,
133
+ unauthorized: 401,
134
+ auth: 401,
135
+ forbidden: 403,
136
+ permission: 403,
137
+ not_found: 404,
138
+ missing: 404,
139
+ conflict: 409,
140
+ duplicate: 409,
141
+ rate_limit: 429,
142
+ too_many: 429,
143
+ server: 500,
144
+ internal: 500,
145
+ };
146
+ for (const [key, status] of Object.entries(statusMap)) {
147
+ if (errorCode.toLowerCase().includes(key)) {
148
+ return status;
149
+ }
150
+ }
151
+ return 500;
152
+ }
153
+ createErrorResponseComponent(code, errorDef) {
154
+ return {
155
+ description: errorDef.description,
156
+ content: {
157
+ "application/json": {
158
+ schema: errorDef.schema,
159
+ },
160
+ },
161
+ };
162
+ }
73
163
  }
@@ -5,6 +5,7 @@ import traverse from "@babel/traverse";
5
5
  import { parse } from "@babel/parser";
6
6
  import { SchemaProcessor } from "./schema-processor.js";
7
7
  import { capitalize, extractJSDocComments, extractPathParameters, getOperationId, } from "./utils.js";
8
+ import { logger } from "./logger.js";
8
9
  const HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
9
10
  const MUTATION_HTTP_METHODS = ["PATCH", "POST", "PUT"];
10
11
  export class RouteProcessor {
@@ -18,6 +19,91 @@ export class RouteProcessor {
18
19
  this.config = config;
19
20
  this.schemaProcessor = new SchemaProcessor(config.schemaDir, config.schemaType);
20
21
  }
22
+ buildResponsesFromConfig(dataTypes, method) {
23
+ const responses = {};
24
+ // 1. Add success response
25
+ const successCode = dataTypes.successCode || this.getDefaultSuccessCode(method);
26
+ if (dataTypes.responseType) {
27
+ const responseSchema = this.schemaProcessor.getSchemaContent({
28
+ responseType: dataTypes.responseType,
29
+ }).responses;
30
+ responses[successCode] = {
31
+ description: dataTypes.responseDescription || "Successful response",
32
+ content: {
33
+ "application/json": {
34
+ schema: responseSchema,
35
+ },
36
+ },
37
+ };
38
+ }
39
+ // 2. Add responses from ResponseSet
40
+ const responseSetName = dataTypes.responseSet || this.config.defaultResponseSet;
41
+ if (responseSetName && responseSetName !== "none") {
42
+ const responseSets = this.config.responseSets || {};
43
+ const setNames = responseSetName.split(",").map((s) => s.trim());
44
+ setNames.forEach((setName) => {
45
+ const responseSet = responseSets[setName];
46
+ if (responseSet) {
47
+ responseSet.forEach((errorCode) => {
48
+ // Use $ref for components/responses
49
+ responses[errorCode] = {
50
+ $ref: `#/components/responses/${errorCode}`,
51
+ };
52
+ });
53
+ }
54
+ });
55
+ }
56
+ // 3. Add custom responses (@add)
57
+ if (dataTypes.addResponses) {
58
+ const customResponses = dataTypes.addResponses
59
+ .split(",")
60
+ .map((s) => s.trim());
61
+ customResponses.forEach((responseRef) => {
62
+ const [code, ref] = responseRef.split(":");
63
+ if (ref) {
64
+ // Custom schema: "409:ConflictResponse"
65
+ responses[code] = {
66
+ description: this.getDefaultErrorDescription(code) || `HTTP ${code} response`,
67
+ content: {
68
+ "application/json": {
69
+ schema: { $ref: `#/components/schemas/${ref}` },
70
+ },
71
+ },
72
+ };
73
+ }
74
+ else {
75
+ // Only code: "409" - use $ref fro components/responses
76
+ responses[code] = {
77
+ $ref: `#/components/responses/${code}`,
78
+ };
79
+ }
80
+ });
81
+ }
82
+ return responses;
83
+ }
84
+ getDefaultSuccessCode(method) {
85
+ switch (method.toUpperCase()) {
86
+ case "POST":
87
+ return "201";
88
+ case "DELETE":
89
+ return "204";
90
+ default:
91
+ return "200";
92
+ }
93
+ }
94
+ getDefaultErrorDescription(code) {
95
+ const defaults = {
96
+ 400: "Bad Request",
97
+ 401: "Unauthorized",
98
+ 403: "Forbidden",
99
+ 404: "Not Found",
100
+ 409: "Conflict",
101
+ 422: "Unprocessable Entity",
102
+ 429: "Too Many Requests",
103
+ 500: "Internal Server Error",
104
+ };
105
+ return defaults[code] || `HTTP ${code}`;
106
+ }
21
107
  /**
22
108
  * Get the SchemaProcessor instance
23
109
  */
@@ -51,7 +137,7 @@ export class RouteProcessor {
51
137
  const pathParams = extractPathParameters(routePath);
52
138
  // If we have path parameters but no pathParamsType defined, we should log a warning
53
139
  if (pathParams.length > 0 && !dataTypes.pathParamsType) {
54
- console.warn(`Route ${routePath} contains path parameters ${pathParams.join(", ")} but no @pathParams type is defined.`);
140
+ logger.warn(`Route ${routePath} contains path parameters ${pathParams.join(", ")} but no @pathParams type is defined.`);
55
141
  }
56
142
  this.addRouteToPaths(declaration.id.name, filePath, dataTypes);
57
143
  }
@@ -68,7 +154,7 @@ export class RouteProcessor {
68
154
  const routePath = this.getRoutePath(filePath);
69
155
  const pathParams = extractPathParameters(routePath);
70
156
  if (pathParams.length > 0 && !dataTypes.pathParamsType) {
71
- console.warn(`Route ${routePath} contains path parameters ${pathParams.join(", ")} but no @pathParams type is defined.`);
157
+ logger.warn(`Route ${routePath} contains path parameters ${pathParams.join(", ")} but no @pathParams type is defined.`);
72
158
  }
73
159
  this.addRouteToPaths(decl.id.name, filePath, dataTypes);
74
160
  }
@@ -81,6 +167,7 @@ export class RouteProcessor {
81
167
  this.processFileTracker[filePath] = true;
82
168
  }
83
169
  scanApiRoutes(dir) {
170
+ logger.log(`Scanning API routes in: ${dir}`);
84
171
  let files = this.directoryCache[dir];
85
172
  if (!files) {
86
173
  files = fs.readdirSync(dir);
@@ -163,9 +250,13 @@ export class RouteProcessor {
163
250
  definition.requestBody = this.schemaProcessor.createRequestBodySchema(body, bodyDescription, dataTypes.contentType);
164
251
  }
165
252
  // Add responses
166
- definition.responses = responses
167
- ? this.schemaProcessor.createResponseSchema(responses, responseDescription)
168
- : {};
253
+ definition.responses = this.buildResponsesFromConfig(dataTypes, method);
254
+ // If there are no responses from config, use the old logic
255
+ if (Object.keys(definition.responses).length === 0) {
256
+ definition.responses = responses
257
+ ? this.schemaProcessor.createResponseSchema(responses, responseDescription)
258
+ : {};
259
+ }
169
260
  this.swaggerPaths[routePath][method] = definition;
170
261
  }
171
262
  getRoutePath(filePath) {
@@ -4,6 +4,7 @@ import { parse } from "@babel/parser";
4
4
  import traverse from "@babel/traverse";
5
5
  import * as t from "@babel/types";
6
6
  import { ZodSchemaConverter } from "./zod-converter.js";
7
+ import { logger } from "./logger.js";
7
8
  export class SchemaProcessor {
8
9
  schemaDir;
9
10
  typeDefinitions = {};
@@ -42,20 +43,20 @@ export class SchemaProcessor {
42
43
  this.contentType = contentType;
43
44
  // Check if we should use Zod schemas
44
45
  if (this.schemaType === "zod") {
45
- console.log(`Looking for Zod schema: ${schemaName}`);
46
+ logger.log(`Looking for Zod schema: ${schemaName}`);
46
47
  // Check type mapping first
47
48
  const mappedSchemaName = this.zodSchemaConverter.typeToSchemaMapping[schemaName];
48
49
  if (mappedSchemaName) {
49
- console.log(`Type '${schemaName}' is mapped to Zod schema '${mappedSchemaName}'`);
50
+ logger.log(`Type '${schemaName}' is mapped to Zod schema '${mappedSchemaName}'`);
50
51
  }
51
52
  // Try to convert Zod schema
52
53
  const zodSchema = this.zodSchemaConverter.convertZodSchemaToOpenApi(schemaName);
53
54
  if (zodSchema) {
54
- console.log(`Found and processed Zod schema: ${schemaName}`);
55
+ logger.log(`Found and processed Zod schema: ${schemaName}`);
55
56
  this.openapiDefinitions[schemaName] = zodSchema;
56
57
  return zodSchema;
57
58
  }
58
- console.log(`No Zod schema found for ${schemaName}, trying TypeScript fallback`);
59
+ logger.log(`No Zod schema found for ${schemaName}, trying TypeScript fallback`);
59
60
  }
60
61
  // Fall back to TypeScript types
61
62
  this.scanSchemaDir(this.schemaDir, schemaName);
@@ -394,7 +395,7 @@ export class SchemaProcessor {
394
395
  if (t.isTSTypeReference(node) && t.isIdentifier(node.typeName)) {
395
396
  return { $ref: `#/components/schemas/${node.typeName.name}` };
396
397
  }
397
- console.warn("Unrecognized TypeScript type node:", node);
398
+ logger.log("Unrecognized TypeScript type node:", node);
398
399
  return { type: "object" }; // By default we return an object
399
400
  }
400
401
  processSchemaFile(filePath, schemaName) {
@@ -417,7 +418,7 @@ export class SchemaProcessor {
417
418
  return definition;
418
419
  }
419
420
  catch (error) {
420
- console.error(`Error processing schema file ${filePath} for schema ${schemaName}:`, error);
421
+ logger.error(`Error processing schema file ${filePath} for schema ${schemaName}: ${error}`);
421
422
  return { type: "object" }; // By default we return an empty object on error
422
423
  }
423
424
  }
@@ -514,6 +515,26 @@ export class SchemaProcessor {
514
515
  }
515
516
  return "application/json";
516
517
  }
518
+ createMultipleResponsesSchema(responses, defaultDescription) {
519
+ const result = {};
520
+ Object.entries(responses).forEach(([code, response]) => {
521
+ if (typeof response === "string") {
522
+ // Reference do components/responses
523
+ result[code] = { $ref: `#/components/responses/${response}` };
524
+ }
525
+ else {
526
+ result[code] = {
527
+ description: response.description || defaultDescription || "Response",
528
+ content: {
529
+ "application/json": {
530
+ schema: response.schema || response,
531
+ },
532
+ },
533
+ };
534
+ }
535
+ });
536
+ return result;
537
+ }
517
538
  createFormDataSchema(body) {
518
539
  if (!body.properties) {
519
540
  return body;
package/dist/lib/utils.js CHANGED
@@ -22,13 +22,16 @@ export function extractJSDocComments(path) {
22
22
  let paramsType = "";
23
23
  let pathParamsType = "";
24
24
  let bodyType = "";
25
- let responseType = "";
26
25
  let auth = "";
27
26
  let isOpenApi = false;
28
27
  let deprecated = false;
29
28
  let bodyDescription = "";
30
- let responseDescription = "";
31
29
  let contentType = "";
30
+ let responseType = "";
31
+ let responseDescription = "";
32
+ let responseSet = "";
33
+ let addResponses = "";
34
+ let successCode = "";
32
35
  if (comments) {
33
36
  comments.forEach((comment) => {
34
37
  const commentValue = cleanComment(comment.value);
@@ -43,13 +46,6 @@ export function extractJSDocComments(path) {
43
46
  bodyDescription = match[1].trim();
44
47
  }
45
48
  }
46
- if (commentValue.includes("@responseDescription")) {
47
- const regex = /@responseDescription\s*(.*)/;
48
- const match = commentValue.match(regex);
49
- if (match && match[1]) {
50
- responseDescription = match[1].trim();
51
- }
52
- }
53
49
  if (!summary) {
54
50
  summary = commentValue.split("\n")[0];
55
51
  }
@@ -98,6 +94,38 @@ export function extractJSDocComments(path) {
98
94
  contentType = match[1].trim();
99
95
  }
100
96
  }
97
+ if (commentValue.includes("@responseDescription")) {
98
+ const regex = /@responseDescription\s*(.*)/;
99
+ const match = commentValue.match(regex);
100
+ if (match && match[1]) {
101
+ responseDescription = match[1].trim();
102
+ }
103
+ }
104
+ if (commentValue.includes("@responseSet")) {
105
+ const regex = /@responseSet\s*(.*)/;
106
+ const match = commentValue.match(regex);
107
+ if (match && match[1]) {
108
+ responseSet = match[1].trim();
109
+ }
110
+ }
111
+ if (commentValue.includes("@add")) {
112
+ const regex = /@add\s*(.*)/;
113
+ const match = commentValue.match(regex);
114
+ if (match && match[1]) {
115
+ addResponses = match[1].trim();
116
+ }
117
+ }
118
+ if (commentValue.includes("@response")) {
119
+ const responseMatch = commentValue.match(/@response\s+(?:(\d+):)?(\w+)(?:\s+(.*))?/);
120
+ if (responseMatch) {
121
+ const [, code, type] = responseMatch;
122
+ successCode = code || "";
123
+ responseType = type;
124
+ }
125
+ else {
126
+ responseType = extractTypeFromComment(commentValue, "@response");
127
+ }
128
+ }
101
129
  });
102
130
  }
103
131
  return {
@@ -108,12 +136,15 @@ export function extractJSDocComments(path) {
108
136
  paramsType,
109
137
  pathParamsType,
110
138
  bodyType,
111
- responseType,
112
139
  isOpenApi,
113
140
  deprecated,
114
141
  bodyDescription,
115
- responseDescription,
116
142
  contentType,
143
+ responseType,
144
+ responseDescription,
145
+ responseSet,
146
+ addResponses,
147
+ successCode,
117
148
  };
118
149
  }
119
150
  export function extractTypeFromComment(commentValue, tag) {
@@ -130,6 +161,11 @@ export function cleanSpec(spec) {
130
161
  "ui",
131
162
  "outputFile",
132
163
  "includeOpenApiRoutes",
164
+ "schemaType",
165
+ "defaultResponseSet",
166
+ "responseSets",
167
+ "errorConfig",
168
+ "debug",
133
169
  ];
134
170
  const newSpec = { ...spec };
135
171
  propsToRemove.forEach((key) => delete newSpec[key]);
@@ -3,6 +3,7 @@ import path from "path";
3
3
  import { parse } from "@babel/parser";
4
4
  import traverse from "@babel/traverse";
5
5
  import * as t from "@babel/types";
6
+ import { logger } from "./logger.js";
6
7
  /**
7
8
  * Class for converting Zod schemas to OpenAPI specifications
8
9
  */
@@ -23,11 +24,11 @@ export class ZodSchemaConverter {
23
24
  if (Object.keys(this.typeToSchemaMapping).length === 0) {
24
25
  this.preScanForTypeMappings();
25
26
  }
26
- console.log(`Looking for Zod schema: ${schemaName}`);
27
+ logger.log(`Looking for Zod schema: ${schemaName}`);
27
28
  // Check mapped types
28
29
  const mappedSchemaName = this.typeToSchemaMapping[schemaName];
29
30
  if (mappedSchemaName) {
30
- console.log(`Type '${schemaName}' is mapped to schema '${mappedSchemaName}'`);
31
+ logger.log(`Type '${schemaName}' is mapped to schema '${mappedSchemaName}'`);
31
32
  schemaName = mappedSchemaName;
32
33
  }
33
34
  // Check for circular references
@@ -46,7 +47,7 @@ export class ZodSchemaConverter {
46
47
  for (const routeFile of routeFiles) {
47
48
  this.processFileForZodSchema(routeFile, schemaName);
48
49
  if (this.zodSchemas[schemaName]) {
49
- console.log(`Found Zod schema '${schemaName}' in route file: ${routeFile}`);
50
+ logger.log(`Found Zod schema '${schemaName}' in route file: ${routeFile}`);
50
51
  return this.zodSchemas[schemaName];
51
52
  }
52
53
  }
@@ -54,10 +55,10 @@ export class ZodSchemaConverter {
54
55
  this.scanDirectoryForZodSchema(this.schemaDir, schemaName);
55
56
  // Return the schema if found, or null if not
56
57
  if (this.zodSchemas[schemaName]) {
57
- console.log(`Found and processed Zod schema: ${schemaName}`);
58
+ logger.log(`Found and processed Zod schema: ${schemaName}`);
58
59
  return this.zodSchemas[schemaName];
59
60
  }
60
- console.log(`Could not find Zod schema: ${schemaName}`);
61
+ logger.log(`Could not find Zod schema: ${schemaName}`);
61
62
  return null;
62
63
  }
63
64
  finally {
@@ -104,7 +105,7 @@ export class ZodSchemaConverter {
104
105
  }
105
106
  }
106
107
  catch (error) {
107
- console.error(`Error scanning directory ${dir} for route files:`, error);
108
+ logger.log(`Error scanning directory ${dir} for route files: ${error}`);
108
109
  }
109
110
  }
110
111
  /**
@@ -125,7 +126,7 @@ export class ZodSchemaConverter {
125
126
  }
126
127
  }
127
128
  catch (error) {
128
- console.error(`Error scanning directory ${dir}:`, error);
129
+ logger.log(`Error scanning directory ${dir}: ${error}`);
129
130
  }
130
131
  }
131
132
  /**
@@ -272,7 +273,7 @@ export class ZodSchemaConverter {
272
273
  ? prop.key.value
273
274
  : null;
274
275
  if (key && schema.properties) {
275
- console.log(`Removing property: ${key}`);
276
+ logger.log(`Removing property: ${key}`);
276
277
  delete schema.properties[key];
277
278
  if (schema.required) {
278
279
  schema.required = schema.required.filter((r) => r !== key);
@@ -357,20 +358,20 @@ export class ZodSchemaConverter {
357
358
  if (t.isCallExpression(path.node.init)) {
358
359
  const baseSchemaName = findBaseSchema(path.node.init);
359
360
  if (baseSchemaName && baseSchemaName !== "z") {
360
- console.log(`Found chained call starting from: ${baseSchemaName}`);
361
+ logger.log(`Found chained call starting from: ${baseSchemaName}`);
361
362
  // First make sure the underlying schema is processed
362
363
  if (!this.zodSchemas[baseSchemaName]) {
363
- console.log(`Base schema ${baseSchemaName} not found, processing it first`);
364
+ logger.log(`Base schema ${baseSchemaName} not found, processing it first`);
364
365
  this.processFileForZodSchema(filePath, baseSchemaName);
365
366
  }
366
367
  if (this.zodSchemas[baseSchemaName]) {
367
- console.log(`Base schema found, applying transformations`);
368
+ logger.log("Base schema found, applying transformations");
368
369
  // Copy base schema
369
370
  const baseSchema = JSON.parse(JSON.stringify(this.zodSchemas[baseSchemaName]));
370
371
  // Process the entire call chain
371
372
  const finalSchema = processChainedCall(path.node.init, baseSchema);
372
373
  this.zodSchemas[schemaName] = finalSchema;
373
- console.log(`Created ${schemaName} with properties:`, Object.keys(finalSchema.properties || {}));
374
+ logger.log(`Created ${schemaName} with properties: ${Object.keys(finalSchema.properties || {})}`);
374
375
  return;
375
376
  }
376
377
  }
@@ -412,7 +413,7 @@ export class ZodSchemaConverter {
412
413
  const referencedSchemaName = param.exprName.name;
413
414
  // Save mapping: TypeName -> SchemaName
414
415
  this.typeToSchemaMapping[typeName] = referencedSchemaName;
415
- console.log(`Mapped type '${typeName}' to schema '${referencedSchemaName}'`);
416
+ logger.log(`Mapped type '${typeName}' to schema '${referencedSchemaName}'`);
416
417
  // Process the referenced schema if not already processed
417
418
  if (!this.zodSchemas[referencedSchemaName]) {
418
419
  this.processFileForZodSchema(filePath, referencedSchemaName);
@@ -1222,7 +1223,7 @@ export class ZodSchemaConverter {
1222
1223
  schema.description = baseSchema.description;
1223
1224
  }
1224
1225
  else {
1225
- console.log(`Warning: Could not resolve base schema for extend`);
1226
+ logger.warn("Could not resolve base schema for extend");
1226
1227
  schema = extendedProps || { type: "object" };
1227
1228
  }
1228
1229
  }
@@ -1313,7 +1314,7 @@ export class ZodSchemaConverter {
1313
1314
  * Pre-scan all files to build type mappings
1314
1315
  */
1315
1316
  preScanForTypeMappings() {
1316
- console.log("Pre-scanning for type mappings...");
1317
+ logger.log("Pre-scanning for type mappings...");
1317
1318
  // Scan route files
1318
1319
  const routeFiles = this.findRouteFiles();
1319
1320
  for (const routeFile of routeFiles) {
@@ -1359,7 +1360,7 @@ export class ZodSchemaConverter {
1359
1360
  if (t.isTSTypeQuery(param) && t.isIdentifier(param.exprName)) {
1360
1361
  const referencedSchemaName = param.exprName.name;
1361
1362
  this.typeToSchemaMapping[typeName] = referencedSchemaName;
1362
- console.log(`Pre-scan: Mapped type '${typeName}' to schema '${referencedSchemaName}'`);
1363
+ logger.log(`Pre-scan: Mapped type '${typeName}' to schema '${referencedSchemaName}'`);
1363
1364
  }
1364
1365
  }
1365
1366
  }
@@ -1368,7 +1369,7 @@ export class ZodSchemaConverter {
1368
1369
  });
1369
1370
  }
1370
1371
  catch (error) {
1371
- console.error(`Error scanning file ${filePath} for type mappings:`, error);
1372
+ logger.log(`Error scanning file ${filePath} for type mappings: ${error}`);
1372
1373
  }
1373
1374
  }
1374
1375
  /**
@@ -1389,7 +1390,7 @@ export class ZodSchemaConverter {
1389
1390
  }
1390
1391
  }
1391
1392
  catch (error) {
1392
- console.error(`Error scanning directory ${dir} for type mappings:`, error);
1393
+ logger.error(`Error scanning directory ${dir} for type mappings: ${error}`);
1393
1394
  }
1394
1395
  }
1395
1396
  /**
@@ -1412,7 +1413,7 @@ export class ZodSchemaConverter {
1412
1413
  // Check if is Zos schema
1413
1414
  if (this.isZodSchema(declaration.init) &&
1414
1415
  !this.zodSchemas[schemaName]) {
1415
- console.log(`Pre-processing Zod schema: ${schemaName}`);
1416
+ logger.log(`Pre-processing Zod schema: ${schemaName}`);
1416
1417
  this.processingSchemas.add(schemaName);
1417
1418
  const schema = this.processZodNode(declaration.init);
1418
1419
  if (schema) {
@@ -1432,7 +1433,7 @@ export class ZodSchemaConverter {
1432
1433
  if (this.isZodSchema(declaration.init) &&
1433
1434
  !this.zodSchemas[schemaName] &&
1434
1435
  !this.processingSchemas.has(schemaName)) {
1435
- console.log(`Pre-processing Zod schema: ${schemaName}`);
1436
+ logger.log(`Pre-processing Zod schema: ${schemaName}`);
1436
1437
  this.processingSchemas.add(schemaName);
1437
1438
  const schema = this.processZodNode(declaration.init);
1438
1439
  if (schema) {
@@ -1446,7 +1447,7 @@ export class ZodSchemaConverter {
1446
1447
  });
1447
1448
  }
1448
1449
  catch (error) {
1449
- console.error(`Error pre-processing file ${filePath}:`, error);
1450
+ logger.error(`Error pre-processing file ${filePath}: ${error}`);
1450
1451
  }
1451
1452
  }
1452
1453
  /**
@@ -20,6 +20,49 @@ export default {
20
20
  },
21
21
  },
22
22
  },
23
+ defaultResponseSet: "common",
24
+ responseSets: {
25
+ common: ["400", "500"],
26
+ auth: ["401"],
27
+ },
28
+ errorConfig: {
29
+ template: {
30
+ type: "object",
31
+ properties: {
32
+ success: {
33
+ type: "boolean",
34
+ example: false,
35
+ },
36
+ error: {
37
+ type: "string",
38
+ example: "{{ERROR_MESSAGE}}",
39
+ },
40
+ },
41
+ },
42
+ codes: {
43
+ invalid: {
44
+ description: "Bad request",
45
+ httpStatus: 400,
46
+ variables: {
47
+ ERROR_MESSAGE: "Validation error",
48
+ },
49
+ },
50
+ auth: {
51
+ description: "Unauthorized",
52
+ httpStatus: 401,
53
+ variables: {
54
+ ERROR_MESSAGE: "Unathorized",
55
+ },
56
+ },
57
+ server_error: {
58
+ description: "Internal server error",
59
+ httpStatus: 500,
60
+ variables: {
61
+ ERROR_MESSAGE: "Something went wrong",
62
+ },
63
+ },
64
+ },
65
+ },
23
66
  apiDir: "./src/app/api",
24
67
  schemaDir: "./src",
25
68
  schemaType: "typescript",
@@ -27,4 +70,5 @@ export default {
27
70
  ui: "scalar",
28
71
  outputFile: "openapi.json",
29
72
  includeOpenApiRoutes: false,
73
+ debug: false,
30
74
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-openapi-gen",
3
- "version": "0.6.2",
3
+ "version": "0.6.5",
4
4
  "description": "Automatically generate OpenAPI 3.0 documentation from Next.js projects, with support for TypeScript types and Zod schemas.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",