UNPKG

4bnode

Version:

4bnode is a CLI-powered backend development platform with a built-in visual dashboard to generate, manage, and test Node.js/Express APIs faster.

105 lines (94 loc) 4.61 kB
#!/usr/bin/env node import fs from "fs"; import path from "path"; import { getProjectRoot, listRoutes, listModels, getModelSchemaDetailed } from "./lib/project.js"; import { addRouteRegistration, readIndex } from "./lib/indexFile.js"; import { showHeader, showTaskDone, showError, showFileAction, showInfo } from "./lib/ui.js"; import { extractRequestFields, extractFileFields, requiredFieldsFromZod, buildOpenApiSpec, buildDocsRouter } from "./lib/codegen.js"; function collectEndpoints() { const root = getProjectRoot(); const routesDir = path.join(root, "src", "routes"); if (!fs.existsSync(routesDir)) return []; const indexContent = readIndex(); const out = []; for (const file of listRoutes()) { const name = path.basename(file, ".js"); const content = fs.readFileSync(path.join(routesDir, file), "utf8"); let prefix = "/api/" + name; const reg = indexContent.match(new RegExp(`app\\.use\\(['"]([^'"]+)['"]\\s*,\\s*${name}Router`)); if (reg) prefix = reg[1]; const modelImport = content.match(/import\s+(\w+)\s+from\s+['"]\.\.\/models\/(\w+)(?:\.js)?['"]/); const modelFields = modelImport ? getModelSchemaDetailed(modelImport[2]) : []; // Required-ness for the API comes from the Zod validator (the request gate), // not the Mongoose model. Parse the route's validator so docs match the contract. const validatorImport = content.match(/from\s+['"]\.\.\/validators\/([\w-]+)(?:\.js)?['"]/); let validatorReq = null; if (validatorImport) { const vPath = path.join(root, "src", "validators", validatorImport[1] + ".js"); if (fs.existsSync(vPath)) { try { validatorReq = requiredFieldsFromZod(fs.readFileSync(vPath, "utf8")); } catch {} } } const epRegex = /router\.(get|post|put|patch|delete)\(\s*['"`](\/[^'"`]*?)['"`]\s*,([\s\S]*?)(?=\nrouter\.|$)/gi; let m; while ((m = epRegex.exec(content)) !== null) { const method = m[1].toUpperCase(); const epath = m[2]; const handler = m[3] || ""; const usesPartial = /\.partial\s*\(\s*\)/.test(handler); // PATCH: all optional const bodyFields = extractRequestFields(handler, "body").map((fn) => { const meta = modelFields.find((x) => x.name === fn); let required; if (usesPartial) required = false; else if (validatorReq && Object.prototype.hasOwnProperty.call(validatorReq, fn)) required = validatorReq[fn]; else required = meta?.required || false; return { name: fn, type: meta?.type || "String", required }; }); const queryFields = extractRequestFields(handler, "query").map((fn) => ({ name: fn, type: "String" })); const params = (epath.match(/:(\w+)/g) || []).map((p) => p.slice(1)); const fileFields = extractFileFields(handler); const fullPath = (prefix + (epath === "/" ? "" : epath)).replace(/\/+/g, "/"); out.push({ method, path: epath, fullPath, bodyFields, queryFields, params, fileFields, hasAuth: /\bauth\b\s*,/.test(m[0]) }); } } return out; } function projectTitle() { try { const pkg = JSON.parse(fs.readFileSync(path.join(getProjectRoot(), "package.json"), "utf8")); return (pkg.name || "API") + " API"; } catch { return "API"; } } async function main() { showHeader("add-docs", "Generate OpenAPI docs + Swagger UI"); const root = getProjectRoot(); const srcDir = path.join(root, "src"); fs.mkdirSync(srcDir, { recursive: true }); const docsPath = path.join(srcDir, "docs.js"); if (!fs.existsSync(docsPath)) { fs.writeFileSync(docsPath, buildDocsRouter(), "utf8"); showFileAction("created", "src/docs.js"); } else { showInfo("src/docs.js already exists — keeping it."); } const endpoints = collectEndpoints(); const models = listModels().map((f) => { const name = path.basename(f, ".js"); return { name, fields: getModelSchemaDetailed(name) }; }); const spec = buildOpenApiSpec({ title: projectTitle(), endpoints, models }); fs.writeFileSync(path.join(srcDir, "openapi.json"), JSON.stringify(spec, null, 2), "utf8"); showFileAction("created", "src/openapi.json"); addRouteRegistration("import docsRouter from './src/docs.js';", "app.use('/docs', docsRouter);"); showFileAction("updated", "index.js"); showTaskDone("API docs configured", [ `${endpoints.length} endpoint${endpoints.length === 1 ? "" : "s"} documented`, "Swagger UI at /docs", "Re-run after changing routes to refresh the spec", ]); } main().catch((err) => { if (err.name === "ExitPromptError") process.exit(0); showError(err.message); process.exit(1); });