UNPKG

next-openapi-gen

Version:

Automatically generate OpenAPI 3.0 documentation from Next.js projects, with support for Zod schemas and TypeScript types.

169 lines (168 loc) 6.41 kB
import path from "path"; import fs from "fs"; import { RouteProcessor } from "./route-processor.js"; import { cleanSpec } from "./utils.js"; import { logger } from "./logger.js"; export class OpenApiGenerator { config; template; routeProcessor; constructor() { const templatePath = path.resolve("./next.openapi.json"); this.template = JSON.parse(fs.readFileSync(templatePath, "utf-8")); this.config = this.getConfig(); this.routeProcessor = new RouteProcessor(this.config); // Initialize logger logger.init(this.config); } getConfig() { // @ts-ignore const { apiDir, schemaDir, docsUrl, ui, outputFile, outputDir, includeOpenApiRoutes, schemaType = "typescript", defaultResponseSet, responseSets, errorConfig, debug } = this.template; return { apiDir: apiDir || "./src/app/api", schemaDir: schemaDir || "./src", docsUrl: docsUrl || "api-docs", ui: ui || "scalar", outputFile: outputFile || "openapi.json", outputDir: outputDir || "./public", includeOpenApiRoutes: includeOpenApiRoutes || false, schemaType, defaultResponseSet, responseSets, errorConfig, debug: debug || false, }; } generate() { logger.log("Starting OpenAPI generation..."); const { apiDir } = this.config; // Check if app router structure exists let appRouterApiDir = ""; if (fs.existsSync(path.join(path.dirname(apiDir), "app", "api"))) { appRouterApiDir = path.join(path.dirname(apiDir), "app", "api"); logger.debug(`Found app router API directory at ${appRouterApiDir}`); } // Scan pages router routes this.routeProcessor.scanApiRoutes(apiDir); // If app router directory exists, scan it as well if (appRouterApiDir) { this.routeProcessor.scanApiRoutes(appRouterApiDir); } this.template.paths = this.routeProcessor.getSwaggerPaths(); // Add server URL for examples if not already defined if (!this.template.servers || this.template.servers.length === 0) { this.template.servers = [ { url: this.template.basePath || "", description: "API server", }, ]; } // Ensure there's a components section if not already defined if (!this.template.components) { this.template.components = {}; } // Add schemas section if not already defined if (!this.template.components.schemas) { this.template.components.schemas = {}; } // Generate error responses using errorConfig or manual definitions if (!this.template.components.responses) { this.template.components.responses = {}; } const errorConfig = this.config.errorConfig; if (errorConfig) { this.generateErrorResponsesFromConfig(errorConfig); } else if (this.config.errorDefinitions) { // Use manual definitions (existing logic - if exists) Object.entries(this.config.errorDefinitions).forEach(([code, errorDef]) => { this.template.components.responses[code] = this.createErrorResponseComponent(code, errorDef); }); } // Get defined schemas from the processor const definedSchemas = this.routeProcessor .getSchemaProcessor() .getDefinedSchemas(); if (definedSchemas && Object.keys(definedSchemas).length > 0) { this.template.components.schemas = { ...this.template.components.schemas, ...definedSchemas, }; } const openapiSpec = cleanSpec(this.template); logger.log("OpenAPI generation completed"); return openapiSpec; } generateErrorResponsesFromConfig(errorConfig) { const { template, codes, variables: globalVars = {} } = errorConfig; Object.entries(codes).forEach(([errorCode, config]) => { const httpStatus = (config.httpStatus || this.guessHttpStatus(errorCode)).toString(); // Merge variables: global + per-code + built-in const allVariables = { ...globalVars, ...config.variables, ERROR_CODE: errorCode, DESCRIPTION: config.description, HTTP_STATUS: httpStatus, }; const processedSchema = this.processTemplate(template, allVariables); this.template.components.responses[httpStatus] = { description: config.description, content: { "application/json": { schema: processedSchema, }, }, }; }); } processTemplate(template, variables) { const jsonStr = JSON.stringify(template); let result = jsonStr; Object.entries(variables).forEach(([key, value]) => { result = result.replace(new RegExp(`{{${key}}}`, "g"), value); }); return JSON.parse(result); } guessHttpStatus(errorCode) { const numericCode = parseInt(errorCode); if (numericCode >= 100 && numericCode < 600) { return numericCode; } const statusMap = { bad: 400, invalid: 400, validation: 422, unauthorized: 401, auth: 401, forbidden: 403, permission: 403, not_found: 404, missing: 404, conflict: 409, duplicate: 409, rate_limit: 429, too_many: 429, server: 500, internal: 500, }; for (const [key, status] of Object.entries(statusMap)) { if (errorCode.toLowerCase().includes(key)) { return status; } } return 500; } createErrorResponseComponent(code, errorDef) { return { description: errorDef.description, content: { "application/json": { schema: errorDef.schema, }, }, }; } }