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
JavaScript
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);
});