UNPKG

next-openapi-gen

Version:

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

130 lines (129 loc) 4.57 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, ScalarUI } from "../components/scalar.js"; import { swaggerDeps, SwaggerUI } from "../components/swagger.js"; import { redocDeps, RedocUI } from "../components/redoc.js"; import { stoplightDeps, StoplightUI } from "../components/stoplight.js"; import { rapidocDeps, RapidocUI } from "../components/rapidoc.js"; const execPromise = util.promisify(exec); const spinner = ora("Initializing project with OpenAPI template...\n"); 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(" "); } async function createDocsPage(ui, outputFile) { 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) { const packageManager = await getPackageManager(); const installCmd = `${packageManager} ${packageManager === "npm" ? "install" : "add"}`; const deps = getDocsPageDependencies(ui); const flags = getDocsPageInstallFlags(ui, packageManager); spinner.succeed(`Installing ${deps} dependencies...`); const resp = await execPromise(`${installCmd} ${deps} ${flags}`); spinner.succeed(`Successfully installed ${deps}.`); } function extendOpenApiTemplate(spec, options) { spec.ui = options.ui ?? spec.ui; spec.docsUrl = options.docsUrl ?? spec.docsUrl; spec.schemaType = options.schema ?? spec.schemaType; } export async function init(options) { const { ui } = options; spinner.start(); try { const outputPath = path.join(process.cwd(), "next.openapi.json"); const template = { ...openapiTemplate }; extendOpenApiTemplate(template, options); await fse.writeJson(outputPath, template, { spaces: 2 }); spinner.succeed(`Created OpenAPI template in next.openapi.json`); createDocsPage(ui, template.outputFile); installDependencies(ui); } catch (error) { spinner.fail(`Failed to initialize project: ${error.message}`); } }