Files
OmniRoute/scripts/docs/generate-docs-index.mjs
diegosouzapw f3b944a55a refactor(scripts): organize into build/dev/check/docs/i18n/ad-hoc subfolders
Reorganizes the 29 active scripts under scripts/ into purpose-driven
subfolders:

- scripts/build/    (11) — Build, install, publish, runtime env
- scripts/dev/      (13) — Dev servers, test runners, healthchecks
- scripts/check/    (10) — Lint/validation/coverage checks
- scripts/docs/      (2) — Docs index and provider reference generation
- scripts/i18n/     (+3) — Adds Python translation utilities (check/validate/autotranslate)
- scripts/ad-hoc/    (4) — One-shot maintenance utilities

Updates all references in package.json, electron/package.json,
.husky/pre-commit, .github/workflows/ci.yml, Dockerfile, src/,
tests/, scripts/ internal cross-imports, playwright.config.ts,
and English docs (CODEBASE_DOCUMENTATION, ENVIRONMENT, FEATURES,
RELEASE_CHECKLIST, COVERAGE_PLAN, ELECTRON_GUIDE, I18N, GEMINI).

Also patches scripts/build/pack-artifact-policy.ts so the npm pack
allowlist mirrors the new layout.

Validates with:
- npm run lint            (exit 0 — pre-existing minified-bundle errors only)
- npm run typecheck:core  (exit 0)
- npm run check:docs-all  (exit 0)
- unit tests for moved scripts (57 tests pass)

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

277 lines
7.3 KiB
JavaScript

#!/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.
*/
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");
const SECTION_CATEGORIES = {
"Getting Started": [
"SETUP_GUIDE",
"USER_GUIDE",
"CLI_TOOLS",
"ARCHITECTURE",
"QUICK_START",
"GETTING_STARTED",
],
Features: [
"FEATURES",
"AUTO_COMBO",
"COMPRESSION_GUIDE",
"RTK_COMPRESSION",
"COMPRESSION_ENGINES",
"COMPRESSION_RULES_FORMAT",
"COMPRESSION_LANGUAGE_PACKS",
"FREE_TIERS",
],
"API & Protocols": ["API_REFERENCE", "MCP_SERVER", "A2A_SERVER"],
Deployment: [
"DOCKER_GUIDE",
"VM_DEPLOYMENT_GUIDE",
"FLY_IO_DEPLOYMENT_GUIDE",
"TERMUX_GUIDE",
"PWA_GUIDE",
],
Operations: ["PROXY_GUIDE", "RESILIENCE_GUIDE", "ENVIRONMENT", "TROUBLESHOOTING"],
Development: [
"CODEBASE_DOCUMENTATION",
"COVERAGE_PLAN",
"I18N",
"RELEASE_CHECKLIST",
"UNINSTALL",
"CONTRIBUTING",
"CHANGELOG",
"CODE_OF_CONDUCT",
],
};
const SECTION_ORDER = {
"Getting Started": 1,
Features: 2,
"API & Protocols": 3,
Deployment: 4,
Operations: 5,
Development: 6,
};
function categorizeFile(fileName) {
const stem = fileName.replace(/\.md$/i, "").toUpperCase().replace(/-/g, "_");
for (const [section, patterns] of Object.entries(SECTION_CATEGORIES)) {
if (patterns.some((p) => stem === p)) {
return section;
}
}
return "Other";
}
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);
}
// ---------- 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);
}
fs.mkdirSync(path.dirname(OUT_FILE), { recursive: true });
fs.writeFileSync(
OUT_FILE,
`// 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[] = [];
`,
"utf8"
);
console.warn(`[generate-docs-index] ${DOCS_DIR} not found; generated empty docs index.`);
process.exit(0);
}
const files = fs.readdirSync(DOCS_DIR).filter((f) => f.endsWith(".md") || f.endsWith(".mdx"));
const docs = [];
for (const fileName of files) {
const filePath = path.join(DOCS_DIR, fileName);
const fileContent = fs.readFileSync(filePath, "utf8");
const { data: frontmatter, content } = matter(fileContent);
const slug =
frontmatter.slug ||
fileName
.replace(/\.mdx?$/i, "")
.toLowerCase()
.replace(/_/g, "-");
const title = frontmatter.title || extractTitleFromContent(content) || slug.replace(/-/g, " ");
const section = frontmatter.section || categorizeFile(fileName);
const order = frontmatter.order ?? 999;
const headings = extractHeadings(content);
const contentPreview = frontmatter.description || extractContentPreview(content);
docs.push({ slug, title, fileName, section, order, content: contentPreview, headings });
}
docs.sort((a, b) => {
const sectionA = SECTION_ORDER[a.section] ?? 99;
const sectionB = SECTION_ORDER[b.section] ?? 99;
if (sectionA !== sectionB) return sectionA - sectionB;
return a.order - b.order;
});
// Build navigation sections
const sectionMap = new Map();
for (const doc of docs) {
const items = sectionMap.get(doc.section) || [];
items.push(doc);
sectionMap.set(doc.section, items);
}
const orderedSections = [...new Set([...Object.keys(SECTION_ORDER), ...sectionMap.keys()])];
const navSections = orderedSections
.filter((s) => sectionMap.has(s))
.sort((a, b) => (SECTION_ORDER[a] ?? 99) - (SECTION_ORDER[b] ?? 99))
.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: "API_REFERENCE.md",
section: "API & Protocols",
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
)};
`;
fs.writeFileSync(OUT_FILE, output, "utf8");
console.log(`✅ Generated ${OUT_FILE} with ${docs.length} docs, ${navSections.length} sections`);