mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
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>
263 lines
7.2 KiB
JavaScript
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`);
|