UNPKG

next-openapi-gen

Version:

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

195 lines (194 loc) 6.96 kB
import path from "path"; import fse from "fs-extra"; import fs from "fs"; import ora from "ora"; import { exec } from "child_process"; import util from "util"; import openapiTemplate from "../openapi-template.js"; import { scalarDeps, scalarDevDeps, ScalarUI } from "../components/scalar.js"; import { swaggerDeps, swaggerDevDeps, SwaggerUI } from "../components/swagger.js"; import { redocDeps, redocDevDeps, RedocUI } from "../components/redoc.js"; import { stoplightDeps, stoplightDevDeps, StoplightUI } from "../components/stoplight.js"; import { rapidocDeps, rapidocDevDeps, RapidocUI } from "../components/rapidoc.js"; const execPromise = util.promisify(exec); const spinner = ora("Initializing project with OpenAPI template...\n"); async function hasDependency(packageName) { try { const packageJsonPath = path.join(process.cwd(), "package.json"); const packageJson = await fse.readJson(packageJsonPath); return !!(packageJson.dependencies?.[packageName] || packageJson.devDependencies?.[packageName]); } catch { return false; } } const getPackageManager = async () => { let currentDir = process.cwd(); while (true) { // Check for Yarn lock file if (fs.existsSync(path.join(currentDir, "yarn.lock"))) { return "yarn"; } // Check for PNPM lock file if (fs.existsSync(path.join(currentDir, "pnpm-lock.yaml"))) { return "pnpm"; } // If we're at the root directory, break the loop const parentDir = path.dirname(currentDir); if (parentDir === currentDir) { break; // We've reached the root } currentDir = parentDir; // Move up one directory } // Default to npm if no lock files are found return "npm"; }; function getDocsPage(ui, outputFile) { let DocsComponent = ScalarUI; if (ui === "swagger") { DocsComponent = SwaggerUI; } else if (ui === "redoc") { DocsComponent = RedocUI; } else if (ui === "stoplight") { DocsComponent = StoplightUI; } else if (ui === "rapidoc") { DocsComponent = RapidocUI; } return DocsComponent(outputFile); } function getDocsPageInstallFlags(ui, packageManager) { let installFlags = ""; if (ui === "swagger") { // @temp: swagger-ui-react does not support React 19 now. if (packageManager === "pnpm") { installFlags = "--no-strict-peer-dependencies"; } else if (packageManager === "yarn") { installFlags = ""; // flag for legacy peer deps is not needed for yarn } else { installFlags = "--legacy-peer-deps"; } } return installFlags; } function getDocsPageDependencies(ui) { let deps = []; if (ui === "scalar") { deps = scalarDeps; } else if (ui === "swagger") { deps = swaggerDeps; } else if (ui === "redoc") { deps = redocDeps; } else if (ui === "stoplight") { deps = stoplightDeps; } else if (ui === "rapidoc") { deps = rapidocDeps; } return deps.join(" "); } function getDocsPageDevDependencies(ui) { let devDeps = []; if (ui === "scalar") { devDeps = scalarDevDeps; } else if (ui === "swagger") { devDeps = swaggerDevDeps; } else if (ui === "redoc") { devDeps = redocDevDeps; } else if (ui === "stoplight") { devDeps = stoplightDevDeps; } else if (ui === "rapidoc") { devDeps = rapidocDevDeps; } return devDeps.join(" "); } async function createDocsPage(ui, outputFile) { if (ui === "none") { return; } const paths = ["app", "api-docs"]; const srcPath = path.join(process.cwd(), "src"); if (fs.existsSync(srcPath)) { paths.unshift("src"); } const docsDir = path.join(process.cwd(), ...paths); await fs.promises.mkdir(docsDir, { recursive: true }); const docsPage = getDocsPage(ui, outputFile); const componentPath = path.join(docsDir, "page.tsx"); await fs.promises.writeFile(componentPath, docsPage.trim()); spinner.succeed(`Created ${paths.join("/")}/page.tsx for ${ui}.`); } async function installDependencies(ui, schema) { const packageManager = await getPackageManager(); const installCmd = `${packageManager} ${packageManager === "npm" ? "install" : "add"}`; // Install UI dependencies if (ui !== "none") { const deps = getDocsPageDependencies(ui); const devDeps = getDocsPageDevDependencies(ui); const flags = getDocsPageInstallFlags(ui, packageManager); if (deps) { spinner.succeed(`Installing ${deps} dependencies...`); await execPromise(`${installCmd} ${deps} ${flags}`); spinner.succeed(`Successfully installed ${deps}.`); } if (devDeps) { const devFlag = packageManager === "npm" ? "--save-dev" : "-D"; spinner.succeed(`Installing ${devDeps} dev dependencies...`); await execPromise(`${installCmd} ${devFlag} ${devDeps} ${flags}`); spinner.succeed(`Successfully installed ${devDeps}.`); } } // Install schema dependencies const schemaTypes = Array.isArray(schema) ? schema : [schema]; for (const schemaType of schemaTypes) { if (schemaType === "zod" && !(await hasDependency("zod"))) { spinner.succeed(`Installing zod...`); await execPromise(`${installCmd} zod`); spinner.succeed(`Successfully installed zod.`); } else if (schemaType === "typescript" && !(await hasDependency("typescript"))) { const devFlag = packageManager === "npm" ? "--save-dev" : "-D"; spinner.succeed(`Installing typescript...`); await execPromise(`${installCmd} ${devFlag} typescript`); spinner.succeed(`Successfully installed typescript.`); } } } function extendOpenApiTemplate(spec, options) { spec.ui = options.ui ?? spec.ui; spec.docsUrl = options.docsUrl ?? spec.docsUrl; spec.schemaType = options.schema ?? spec.schemaType; } function getOutputPath(output) { if (output) { return path.isAbsolute(output) ? output : path.join(process.cwd(), output); } return path.join(process.cwd(), "next.openapi.json"); } export async function init(options) { const { ui, output, schema } = options; spinner.start(); try { const outputPath = getOutputPath(output); const template = { ...openapiTemplate }; extendOpenApiTemplate(template, options); await fse.writeJson(outputPath, template, { spaces: 2 }); spinner.succeed(`Created OpenAPI template in ${outputPath}`); createDocsPage(ui, template.outputFile); installDependencies(ui, schema); } catch (error) { spinner.fail(`Failed to initialize project: ${error.message}`); } }