Files
OmniRoute/scripts/generate-docs-index.mjs
diegosouzapw ac39badc2e refactor(scripts): make docs index generator and checkers recurse subfolders
Update tooling for the new docs/<subfolder>/ layout:

- scripts/generate-docs-index.mjs walks the 8 subfolders in defined order and
  emits fileName values relative to docs/ (e.g. "architecture/ARCHITECTURE.md").
- scripts/check-docs-sync.mjs reads docs/reference/openapi.yaml.
- scripts/check-docs-counts-sync.mjs targets new doc paths.
- scripts/check-env-doc-sync.mjs reads docs/reference/ENVIRONMENT.md.
- scripts/gen-provider-reference.ts writes to docs/reference/PROVIDER_REFERENCE.md.
- scripts/pack-artifact-policy.ts allowlists docs/reference/openapi.yaml.
- New scripts/docs/{fix-internal-links,move-i18n-mirrors}.mjs are one-shot
  FASE 3 helpers, safe to delete after merge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 13:12:56 -03:00

263 lines
7.2 KiB
JavaScript

#!/usr/bin/env node
/**
* Build-time script: scans docs/<subfolder>/*.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/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 { 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]));
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);
}
function emitEmpty(reason) {
fs.mkdirSync(path.dirname(OUT_FILE), { recursive: true });
fs.writeFileSync(
OUT_FILE,
`// AUTO-GENERATED by scripts/generate-docs-index.mjs — DO NOT EDIT MANUALLY
// Regenerate with: node scripts/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[] = [];
`,
"utf8"
);
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);
}
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) {
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/generate-docs-index.mjs — DO NOT EDIT MANUALLY
// Regenerate with: node scripts/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
)};
`;
fs.writeFileSync(OUT_FILE, output, "utf8");
console.log(`✅ Generated ${OUT_FILE} with ${docs.length} docs, ${navSections.length} sections`);