#!/usr/bin/env node /** * Build-time script: scans docs//*.md and generates * src/app/docs/lib/docs-auto-generated.ts with static navigation + search data. * * This file is imported by both client and server components — NO fs/path imports. * Run via: node scripts/docs/generate-docs-index.mjs * Automatically runs as prebuild step. * * Sections come from the on-disk subfolder layout introduced in FASE 3 of the * platform overhaul. Order is fixed by SECTION_ORDER below. */ import fs from "node:fs"; import path from "node:path"; import matter from "gray-matter"; import { format } from "prettier"; import { fileURLToPath } from "node:url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.resolve(__dirname, "..", ".."); const DOCS_DIR = path.join(ROOT, "docs"); const OUT_FILE = path.join(ROOT, "src", "app", "docs", "lib", "docs-auto-generated.ts"); // Subfolder -> nav section title, in the order they should appear in the sidebar. const SECTION_ORDER = [ { dir: "architecture", title: "Architecture" }, { dir: "guides", title: "Guides" }, { dir: "reference", title: "Reference" }, { dir: "frameworks", title: "Frameworks" }, { dir: "routing", title: "Routing" }, { dir: "security", title: "Security" }, { dir: "compression", title: "Compression" }, { dir: "ops", title: "Ops" }, ]; const SECTION_INDEX = Object.fromEntries(SECTION_ORDER.map((s, i) => [s.title, i + 1])); async function writeGeneratedFile(content) { fs.mkdirSync(path.dirname(OUT_FILE), { recursive: true }); const formatted = await format(content, { parser: "typescript", printWidth: 100 }); fs.writeFileSync(OUT_FILE, formatted, "utf8"); } function extractTitleFromContent(content) { const match = content.match(/^#\s+(.+)$/m); if (match) { return match[1] .replace(/^📖\s*/, "") .replace(/^🌐\s*/, "") .replace(/\s*—\s*OmniRoute\s*$/i, "") .replace(/\s*—\s*OmniRoute Docs\s*$/i, "") .trim(); } return ""; } function extractHeadings(content) { const headings = []; const regex = /^(#{2,4})\s+(.+)$/gm; let match; while ((match = regex.exec(content)) !== null) { headings.push(match[2].replace(/\*\*/g, "").replace(/\*/g, "").replace(/`/g, "").trim()); } return headings.slice(0, 10); } function extractContentPreview(content) { const stripped = content .replace(/^---[\s\S]*?---/m, "") .replace(/^#{1,6}\s+.+$/gm, "") .replace(/```[\s\S]*?```/g, "") .replace(/!\[[^\]]*\]\([^)]+\)/g, "") .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") .replace(/[*_`~>#|]/g, "") .replace(/\s+/g, " ") .trim(); return stripped.slice(0, 300); } async function emitEmpty(reason) { await writeGeneratedFile( `// AUTO-GENERATED by scripts/docs/generate-docs-index.mjs — DO NOT EDIT MANUALLY // Regenerate with: node scripts/docs/generate-docs-index.mjs export interface AutoGenDocItem { slug: string; title: string; fileName: string; } export interface AutoGenNavSection { title: string; items: AutoGenDocItem[]; } export interface AutoGenSearchItem { slug: string; title: string; fileName: string; section: string; content: string; headings: string[]; } export const autoNavSections: AutoGenNavSection[] = []; export const autoSearchIndex: AutoGenSearchItem[] = []; export const autoAllSlugs: string[] = []; ` ); console.warn(`[generate-docs-index] ${reason}; generated empty docs index.`); } // ---------- Main ---------- if (!fs.existsSync(DOCS_DIR)) { if (fs.existsSync(OUT_FILE)) { console.warn( `[generate-docs-index] ${DOCS_DIR} not found; keeping existing generated docs index.` ); process.exit(0); } await emitEmpty(`${DOCS_DIR} not found`); process.exit(0); } const docs = []; for (const { dir, title } of SECTION_ORDER) { const fullDir = path.join(DOCS_DIR, dir); if (!fs.existsSync(fullDir)) continue; const entries = fs.readdirSync(fullDir, { withFileTypes: true }); for (const entry of entries) { if (!entry.isFile()) continue; if (!entry.name.endsWith(".md") && !entry.name.endsWith(".mdx")) continue; if (entry.name === "README.md") continue; // section index, not a doc page const absPath = path.join(fullDir, entry.name); const fileContent = fs.readFileSync(absPath, "utf8"); const { data: frontmatter, content } = matter(fileContent); const slug = frontmatter.slug || entry.name .replace(/\.mdx?$/i, "") .toLowerCase() .replace(/_/g, "-"); // fileName is relative to docs/ — used by the slug page to read content. const fileName = path.posix.join(dir, entry.name); const titleStr = frontmatter.title || extractTitleFromContent(content) || slug.replace(/-/g, " "); const section = frontmatter.section || title; const order = frontmatter.order ?? 999; const headings = extractHeadings(content); const contentPreview = frontmatter.description || extractContentPreview(content); docs.push({ slug, title: titleStr, fileName, section, order, content: contentPreview, headings, }); } } if (docs.length === 0) { await emitEmpty("no docs discovered in subfolders"); process.exit(0); } docs.sort((a, b) => { const sectionA = SECTION_INDEX[a.section] ?? 99; const sectionB = SECTION_INDEX[b.section] ?? 99; if (sectionA !== sectionB) return sectionA - sectionB; return a.order - b.order; }); // Build navigation sections in the configured order. const sectionMap = new Map(); for (const doc of docs) { const items = sectionMap.get(doc.section) || []; items.push(doc); sectionMap.set(doc.section, items); } const navSections = SECTION_ORDER.map(({ title }) => title) .filter((t) => sectionMap.has(t)) .map((title) => ({ title, items: sectionMap.get(title).map((doc) => ({ slug: doc.slug, title: doc.title, fileName: doc.fileName, })), })); // Build search index const searchIndex = docs.map((doc) => ({ slug: doc.slug, title: doc.title, fileName: doc.fileName, section: doc.section, content: doc.content, headings: doc.headings, })); // Add api-explorer synthetic entry if api-reference exists if (searchIndex.some((item) => item.slug === "api-reference")) { if (!searchIndex.some((item) => item.slug === "api-explorer")) { searchIndex.push({ slug: "api-explorer", title: "API Explorer", fileName: "reference/API_REFERENCE.md", section: "Reference", content: "interactive try it live api explorer endpoint test request response curl example", headings: ["Try It", "Endpoints"], }); } } // ---------- Write output ---------- const output = `// AUTO-GENERATED by scripts/docs/generate-docs-index.mjs — DO NOT EDIT MANUALLY // Regenerate with: node scripts/docs/generate-docs-index.mjs export interface AutoGenDocItem { slug: string; title: string; fileName: string; } export interface AutoGenNavSection { title: string; items: AutoGenDocItem[]; } export interface AutoGenSearchItem { slug: string; title: string; fileName: string; section: string; content: string; headings: string[]; } export const autoNavSections: AutoGenNavSection[] = ${JSON.stringify(navSections, null, 2)}; export const autoSearchIndex: AutoGenSearchItem[] = ${JSON.stringify(searchIndex, null, 2)}; export const autoAllSlugs: string[] = ${JSON.stringify( docs.map((d) => d.slug), null, 2 )}; `; await writeGeneratedFile(output); console.log(`✅ Generated ${OUT_FILE} with ${docs.length} docs, ${navSections.length} sections`);