mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 14:52:09 +03:00
Merge FASE 3: docs restructure into 8 subfolders
Reorganizes /docs into 8 subfolders (architecture, guides, reference, frameworks, routing, security, compression, ops). Resolves two conflicts: - scripts/docs/gen-provider-reference.ts: combined FASE 1's new __dirname-based ROOT (two levels up from scripts/docs/) with FASE 3's new output path (docs/reference/PROVIDER_REFERENCE.md). - scripts/check-env-doc-sync.mjs: deleted by FASE 1, modified by FASE 3; FASE 1's delete wins (file is at scripts/check/ now). The FASE 3 intent (point to docs/reference/ENVIRONMENT.md) was applied to the strict checker at the new path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
274
scripts/docs/fix-internal-links.mjs
Normal file
274
scripts/docs/fix-internal-links.mjs
Normal file
@@ -0,0 +1,274 @@
|
||||
#!/usr/bin/env node
|
||||
// One-shot: FASE 3 helper, safe to delete after merge.
|
||||
//
|
||||
// Rewrites doc-file references after the docs/ flat -> subfolder restructure.
|
||||
//
|
||||
// Modes:
|
||||
// --internal Rewrite relative links inside docs/<subfolder>/*.md to point at
|
||||
// the new subfolder paths (e.g. ./AUTO-COMBO.md -> ../routing/AUTO-COMBO.md).
|
||||
// --external Rewrite absolute-style `docs/<DOC>.md` references in files
|
||||
// outside docs/ (README, CLAUDE.md, .agents, .claude, scripts, src,
|
||||
// tests, etc.) to `docs/<subfolder>/<DOC>.md`.
|
||||
//
|
||||
// Usage:
|
||||
// node scripts/docs/fix-internal-links.mjs --internal
|
||||
// node scripts/docs/fix-internal-links.mjs --external
|
||||
// node scripts/docs/fix-internal-links.mjs --internal --external --dry
|
||||
//
|
||||
// Notes:
|
||||
// - Idempotent: rerunning does not double-rewrite (it only matches the old shape).
|
||||
// - openapi.yaml lives under docs/reference/ — also handled.
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = path.resolve(__dirname, "..", "..");
|
||||
const DOCS = path.join(ROOT, "docs");
|
||||
|
||||
const args = new Set(process.argv.slice(2));
|
||||
const RUN_INTERNAL = args.has("--internal");
|
||||
const RUN_EXTERNAL = args.has("--external");
|
||||
const DRY = args.has("--dry");
|
||||
if (!RUN_INTERNAL && !RUN_EXTERNAL) {
|
||||
console.error("Pass --internal and/or --external (and optionally --dry).");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Mapping: stem -> subfolder
|
||||
// ----------------------------------------------------------------------
|
||||
const DOC_TO_SUBFOLDER = {
|
||||
// architecture
|
||||
"ARCHITECTURE.md": "architecture",
|
||||
"CODEBASE_DOCUMENTATION.md": "architecture",
|
||||
"REPOSITORY_MAP.md": "architecture",
|
||||
"AUTHZ_GUIDE.md": "architecture",
|
||||
"RESILIENCE_GUIDE.md": "architecture",
|
||||
// guides
|
||||
"SETUP_GUIDE.md": "guides",
|
||||
"USER_GUIDE.md": "guides",
|
||||
"DOCKER_GUIDE.md": "guides",
|
||||
"ELECTRON_GUIDE.md": "guides",
|
||||
"TERMUX_GUIDE.md": "guides",
|
||||
"PWA_GUIDE.md": "guides",
|
||||
"TROUBLESHOOTING.md": "guides",
|
||||
"UNINSTALL.md": "guides",
|
||||
"I18N.md": "guides",
|
||||
"FEATURES.md": "guides",
|
||||
// reference
|
||||
"API_REFERENCE.md": "reference",
|
||||
"PROVIDER_REFERENCE.md": "reference",
|
||||
"openapi.yaml": "reference",
|
||||
"ENVIRONMENT.md": "reference",
|
||||
"CLI-TOOLS.md": "reference",
|
||||
"FREE_TIERS.md": "reference",
|
||||
// frameworks
|
||||
"MCP-SERVER.md": "frameworks",
|
||||
"A2A-SERVER.md": "frameworks",
|
||||
"AGENT_PROTOCOLS_GUIDE.md": "frameworks",
|
||||
"CLOUD_AGENT.md": "frameworks",
|
||||
"SKILLS.md": "frameworks",
|
||||
"MEMORY.md": "frameworks",
|
||||
"WEBHOOKS.md": "frameworks",
|
||||
"EVALS.md": "frameworks",
|
||||
// routing
|
||||
"AUTO-COMBO.md": "routing",
|
||||
"REASONING_REPLAY.md": "routing",
|
||||
// security
|
||||
"GUARDRAILS.md": "security",
|
||||
"COMPLIANCE.md": "security",
|
||||
"STEALTH_GUIDE.md": "security",
|
||||
// compression
|
||||
"COMPRESSION_GUIDE.md": "compression",
|
||||
"COMPRESSION_ENGINES.md": "compression",
|
||||
"COMPRESSION_RULES_FORMAT.md": "compression",
|
||||
"COMPRESSION_LANGUAGE_PACKS.md": "compression",
|
||||
"RTK_COMPRESSION.md": "compression",
|
||||
// ops
|
||||
"RELEASE_CHECKLIST.md": "ops",
|
||||
"COVERAGE_PLAN.md": "ops",
|
||||
"FLY_IO_DEPLOYMENT_GUIDE.md": "ops",
|
||||
"VM_DEPLOYMENT_GUIDE.md": "ops",
|
||||
"PROXY_GUIDE.md": "ops",
|
||||
"TUNNELS_GUIDE.md": "ops",
|
||||
};
|
||||
|
||||
// Build alternation regex (longest-first) of file basenames we know about.
|
||||
const FILES_ALT = Object.keys(DOC_TO_SUBFOLDER)
|
||||
.sort((a, b) => b.length - a.length)
|
||||
.map((s) => s.replace(/\./g, "\\."))
|
||||
.join("|");
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Internal rewriter (within docs/**)
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
function listDocFiles() {
|
||||
const out = [];
|
||||
// walk subfolders we created
|
||||
for (const sub of new Set(Object.values(DOC_TO_SUBFOLDER))) {
|
||||
const dir = path.join(DOCS, sub);
|
||||
if (!fs.existsSync(dir)) continue;
|
||||
for (const f of fs.readdirSync(dir)) {
|
||||
if (!f.endsWith(".md") && !f.endsWith(".yaml")) continue;
|
||||
out.push(path.join(dir, f));
|
||||
}
|
||||
}
|
||||
// also rewrite the new README and diagrams README
|
||||
out.push(path.join(DOCS, "README.md"));
|
||||
return out;
|
||||
}
|
||||
|
||||
function relativeFromTo(fromAbsFile, targetSub, targetBasename) {
|
||||
const fromDir = path.dirname(fromAbsFile);
|
||||
const toAbs = path.join(DOCS, targetSub, targetBasename);
|
||||
let rel = path.relative(fromDir, toAbs);
|
||||
// posix-style
|
||||
rel = rel.split(path.sep).join("/");
|
||||
if (!rel.startsWith(".")) rel = "./" + rel;
|
||||
return rel;
|
||||
}
|
||||
|
||||
function rewriteInternal(filePath) {
|
||||
const src = fs.readFileSync(filePath, "utf8");
|
||||
let out = src;
|
||||
|
||||
// Pattern A: relative refs like ./FOO.md, ../FOO.md, or bare FOO.md inside (... ).
|
||||
// We match `]( <optional ./ or ../+> <basename> )` and `]( <basename> )`.
|
||||
// Captures: prefix (=./ or ../ chains, may be empty), basename.
|
||||
const reA = new RegExp(`\\]\\(\\s*((?:\\.{1,2}/)*)(${FILES_ALT})((?:#[^\\s)]+)?)\\s*\\)`, "g");
|
||||
out = out.replace(reA, (full, prefix, basename, anchor) => {
|
||||
const subFolder = DOC_TO_SUBFOLDER[basename];
|
||||
if (!subFolder) return full;
|
||||
const newRel = relativeFromTo(filePath, subFolder, basename);
|
||||
return `](${newRel}${anchor || ""})`;
|
||||
});
|
||||
|
||||
// Pattern B: absolute-style `docs/FOO.md` inside markdown links — convert
|
||||
// to `docs/<sub>/FOO.md`. We avoid double-rewriting if a subfolder is
|
||||
// already present.
|
||||
const reB = new RegExp(`docs/(${FILES_ALT})((?:#[^\\s)\"']+)?)`, "g");
|
||||
out = out.replace(reB, (full, basename, anchor) => {
|
||||
const subFolder = DOC_TO_SUBFOLDER[basename];
|
||||
if (!subFolder) return full;
|
||||
// If preceded by `<sub>/` already, skip — but the regex won't capture that
|
||||
// because it only matches `docs/<basename>`. So this is safe.
|
||||
return `docs/${subFolder}/${basename}${anchor || ""}`;
|
||||
});
|
||||
|
||||
if (out !== src) {
|
||||
if (!DRY) fs.writeFileSync(filePath, out, "utf8");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// External rewriter (files outside docs/)
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
function listExternalFiles() {
|
||||
// We rewrite a curated set of paths to avoid wandering into vendor / build dirs.
|
||||
const candidates = [];
|
||||
|
||||
// 1) Root-level documentation files
|
||||
for (const f of fs.readdirSync(ROOT)) {
|
||||
const full = path.join(ROOT, f);
|
||||
if (!fs.statSync(full).isFile()) continue;
|
||||
if (/\.(md|txt)$/i.test(f)) candidates.push(full);
|
||||
}
|
||||
|
||||
// 2) Specific directories we know contain doc references
|
||||
const dirs = [
|
||||
".agents",
|
||||
".claude",
|
||||
".github",
|
||||
"bin",
|
||||
"electron",
|
||||
"open-sse",
|
||||
"scripts",
|
||||
"src",
|
||||
"tests",
|
||||
"vscode-extension",
|
||||
// i18n mirrors — root-level locale files (llm.txt, CHANGELOG.md, etc.) reference
|
||||
// the root /docs/ paths and must stay in sync after restructure.
|
||||
"docs/i18n",
|
||||
];
|
||||
for (const d of dirs) {
|
||||
const full = path.join(ROOT, d);
|
||||
if (!fs.existsSync(full)) continue;
|
||||
walk(full, candidates);
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
function walk(dir, out) {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
if (entry.name === "node_modules" || entry.name === ".next") continue;
|
||||
if (entry.name.startsWith(".git")) continue;
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walk(full, out);
|
||||
} else if (entry.isFile()) {
|
||||
// accept text-y file types
|
||||
if (/\.(md|txt|ts|tsx|mjs|cjs|js|json|yaml|yml|sh)$/i.test(entry.name)) {
|
||||
out.push(full);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function rewriteExternal(filePath) {
|
||||
const src = fs.readFileSync(filePath, "utf8");
|
||||
let out = src;
|
||||
|
||||
// Rewrite any `docs/FOO.md` (only the bare-basename form, not already
|
||||
// pointing into a subfolder) to `docs/<sub>/FOO.md`.
|
||||
// Word-boundary lookbehind/lookahead-ish: use (?<![\w/-]) so we don't touch
|
||||
// already-prefixed `docs/architecture/FOO.md`.
|
||||
const re = new RegExp(`(?<![A-Za-z0-9_./-])docs/(${FILES_ALT})((?:#[^\\s)\"'\`]+)?)`, "g");
|
||||
out = out.replace(re, (full, basename, anchor) => {
|
||||
const subFolder = DOC_TO_SUBFOLDER[basename];
|
||||
if (!subFolder) return full;
|
||||
return `docs/${subFolder}/${basename}${anchor || ""}`;
|
||||
});
|
||||
|
||||
if (out !== src) {
|
||||
if (!DRY) fs.writeFileSync(filePath, out, "utf8");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Run
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
let internalChanged = 0;
|
||||
let externalChanged = 0;
|
||||
let internalScanned = 0;
|
||||
let externalScanned = 0;
|
||||
|
||||
if (RUN_INTERNAL) {
|
||||
const files = listDocFiles();
|
||||
for (const f of files) {
|
||||
internalScanned++;
|
||||
if (rewriteInternal(f)) internalChanged++;
|
||||
}
|
||||
console.log(
|
||||
`[internal] scanned=${internalScanned} changed=${internalChanged}${DRY ? " (dry-run)" : ""}`
|
||||
);
|
||||
}
|
||||
|
||||
if (RUN_EXTERNAL) {
|
||||
const files = listExternalFiles();
|
||||
for (const f of files) {
|
||||
externalScanned++;
|
||||
if (rewriteExternal(f)) externalChanged++;
|
||||
}
|
||||
console.log(
|
||||
`[external] scanned=${externalScanned} changed=${externalChanged}${DRY ? " (dry-run)" : ""}`
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
// Generates docs/PROVIDER_REFERENCE.md from src/shared/constants/providers.ts.
|
||||
// Generates docs/reference/PROVIDER_REFERENCE.md from src/shared/constants/providers.ts.
|
||||
// Run: node --import tsx/esm scripts/docs/gen-provider-reference.ts
|
||||
|
||||
import fs from "node:fs";
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = path.resolve(__dirname, "..", "..");
|
||||
const OUT_FILE = path.join(ROOT, "docs", "PROVIDER_REFERENCE.md");
|
||||
const OUT_FILE = path.join(ROOT, "docs", "reference", "PROVIDER_REFERENCE.md");
|
||||
|
||||
type ProviderRecord = {
|
||||
id: string;
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Build-time script: scans docs/*.md and generates
|
||||
* 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/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";
|
||||
@@ -19,64 +22,19 @@ 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",
|
||||
],
|
||||
};
|
||||
// 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_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";
|
||||
}
|
||||
const SECTION_INDEX = Object.fromEntries(SECTION_ORDER.map((s, i) => [s.title, i + 1]));
|
||||
|
||||
function extractTitleFromContent(content) {
|
||||
const match = content.match(/^#\s+(.+)$/m);
|
||||
@@ -114,16 +72,7 @@ function extractContentPreview(content) {
|
||||
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);
|
||||
}
|
||||
|
||||
function emitEmpty(reason) {
|
||||
fs.mkdirSync(path.dirname(OUT_FILE), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
OUT_FILE,
|
||||
@@ -158,51 +107,88 @@ export const autoAllSlugs: string[] = [];
|
||||
`,
|
||||
"utf8"
|
||||
);
|
||||
console.warn(`[generate-docs-index] ${DOCS_DIR} not found; generated empty docs index.`);
|
||||
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 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);
|
||||
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
|
||||
|
||||
docs.push({ slug, title, fileName, section, order, content: contentPreview, headings });
|
||||
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_ORDER[a.section] ?? 99;
|
||||
const sectionB = SECTION_ORDER[b.section] ?? 99;
|
||||
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
|
||||
// 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 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))
|
||||
|
||||
const navSections = SECTION_ORDER.map(({ title }) => title)
|
||||
.filter((t) => sectionMap.has(t))
|
||||
.map((title) => ({
|
||||
title,
|
||||
items: sectionMap.get(title).map((doc) => ({
|
||||
@@ -228,8 +214,8 @@ if (searchIndex.some((item) => item.slug === "api-reference")) {
|
||||
searchIndex.push({
|
||||
slug: "api-explorer",
|
||||
title: "API Explorer",
|
||||
fileName: "API_REFERENCE.md",
|
||||
section: "API & Protocols",
|
||||
fileName: "reference/API_REFERENCE.md",
|
||||
section: "Reference",
|
||||
content: "interactive try it live api explorer endpoint test request response curl example",
|
||||
headings: ["Try It", "Endpoints"],
|
||||
});
|
||||
|
||||
139
scripts/docs/move-i18n-mirrors.mjs
Normal file
139
scripts/docs/move-i18n-mirrors.mjs
Normal file
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env node
|
||||
// One-shot: FASE 3 helper, safe to delete after merge.
|
||||
//
|
||||
// Moves existing i18n mirror docs from `docs/i18n/<lang>/docs/X.md` into the
|
||||
// matching subfolder `docs/i18n/<lang>/docs/<sub>/X.md`, mirroring the new
|
||||
// docs/ layout. Uses `git mv` to preserve history.
|
||||
//
|
||||
// Usage:
|
||||
// node scripts/docs/move-i18n-mirrors.mjs [--dry]
|
||||
//
|
||||
// Notes:
|
||||
// - Skips files that don't appear in DOC_TO_SUBFOLDER (e.g., the legacy
|
||||
// `cloudflare-zero-trust-guide.md` or `features/` subfolder — those will be
|
||||
// handled in FASE 5 when translations are regenerated).
|
||||
// - Idempotent: if the target already lives under a subfolder, the entry is
|
||||
// skipped.
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = path.resolve(__dirname, "..", "..");
|
||||
const I18N_DIR = path.join(ROOT, "docs", "i18n");
|
||||
|
||||
const DRY = process.argv.includes("--dry");
|
||||
|
||||
const DOC_TO_SUBFOLDER = {
|
||||
// architecture
|
||||
"ARCHITECTURE.md": "architecture",
|
||||
"CODEBASE_DOCUMENTATION.md": "architecture",
|
||||
"REPOSITORY_MAP.md": "architecture",
|
||||
"AUTHZ_GUIDE.md": "architecture",
|
||||
"RESILIENCE_GUIDE.md": "architecture",
|
||||
// guides
|
||||
"SETUP_GUIDE.md": "guides",
|
||||
"USER_GUIDE.md": "guides",
|
||||
"DOCKER_GUIDE.md": "guides",
|
||||
"ELECTRON_GUIDE.md": "guides",
|
||||
"TERMUX_GUIDE.md": "guides",
|
||||
"PWA_GUIDE.md": "guides",
|
||||
"TROUBLESHOOTING.md": "guides",
|
||||
"UNINSTALL.md": "guides",
|
||||
"I18N.md": "guides",
|
||||
"FEATURES.md": "guides",
|
||||
// reference
|
||||
"API_REFERENCE.md": "reference",
|
||||
"PROVIDER_REFERENCE.md": "reference",
|
||||
"openapi.yaml": "reference",
|
||||
"ENVIRONMENT.md": "reference",
|
||||
"CLI-TOOLS.md": "reference",
|
||||
"FREE_TIERS.md": "reference",
|
||||
// frameworks
|
||||
"MCP-SERVER.md": "frameworks",
|
||||
"A2A-SERVER.md": "frameworks",
|
||||
"AGENT_PROTOCOLS_GUIDE.md": "frameworks",
|
||||
"CLOUD_AGENT.md": "frameworks",
|
||||
"SKILLS.md": "frameworks",
|
||||
"MEMORY.md": "frameworks",
|
||||
"WEBHOOKS.md": "frameworks",
|
||||
"EVALS.md": "frameworks",
|
||||
// routing
|
||||
"AUTO-COMBO.md": "routing",
|
||||
"REASONING_REPLAY.md": "routing",
|
||||
// security
|
||||
"GUARDRAILS.md": "security",
|
||||
"COMPLIANCE.md": "security",
|
||||
"STEALTH_GUIDE.md": "security",
|
||||
// compression
|
||||
"COMPRESSION_GUIDE.md": "compression",
|
||||
"COMPRESSION_ENGINES.md": "compression",
|
||||
"COMPRESSION_RULES_FORMAT.md": "compression",
|
||||
"COMPRESSION_LANGUAGE_PACKS.md": "compression",
|
||||
"RTK_COMPRESSION.md": "compression",
|
||||
// ops
|
||||
"RELEASE_CHECKLIST.md": "ops",
|
||||
"COVERAGE_PLAN.md": "ops",
|
||||
"FLY_IO_DEPLOYMENT_GUIDE.md": "ops",
|
||||
"VM_DEPLOYMENT_GUIDE.md": "ops",
|
||||
"PROXY_GUIDE.md": "ops",
|
||||
"TUNNELS_GUIDE.md": "ops",
|
||||
};
|
||||
|
||||
let moved = 0;
|
||||
let skipped = 0;
|
||||
const seenLocales = [];
|
||||
|
||||
for (const locale of fs.readdirSync(I18N_DIR)) {
|
||||
const localeDir = path.join(I18N_DIR, locale);
|
||||
const stat = fs.statSync(localeDir);
|
||||
if (!stat.isDirectory()) continue;
|
||||
const docsDir = path.join(localeDir, "docs");
|
||||
if (!fs.existsSync(docsDir)) continue;
|
||||
seenLocales.push(locale);
|
||||
|
||||
for (const fname of fs.readdirSync(docsDir)) {
|
||||
const sub = DOC_TO_SUBFOLDER[fname];
|
||||
if (!sub) continue; // not in our mapping (e.g. features/, cloudflare-zero-trust-guide.md)
|
||||
|
||||
const src = path.join(docsDir, fname);
|
||||
if (!fs.statSync(src).isFile()) continue;
|
||||
|
||||
const subDir = path.join(docsDir, sub);
|
||||
const dst = path.join(subDir, fname);
|
||||
|
||||
if (fs.existsSync(dst)) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (DRY) {
|
||||
console.log(`would move: ${path.relative(ROOT, src)} -> ${path.relative(ROOT, dst)}`);
|
||||
moved++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(subDir)) fs.mkdirSync(subDir, { recursive: true });
|
||||
try {
|
||||
execSync(`git mv -k -- "${path.relative(ROOT, src)}" "${path.relative(ROOT, dst)}"`, {
|
||||
cwd: ROOT,
|
||||
stdio: "pipe",
|
||||
});
|
||||
moved++;
|
||||
} catch (e) {
|
||||
// fallback: copy + delete
|
||||
fs.renameSync(src, dst);
|
||||
execSync(
|
||||
`git rm --cached -- "${path.relative(ROOT, src)}" 2>/dev/null || true; git add -- "${path.relative(ROOT, dst)}"`,
|
||||
{ cwd: ROOT, stdio: "pipe", shell: "/bin/bash" }
|
||||
);
|
||||
moved++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[i18n-mirrors] locales=${seenLocales.length} moved=${moved} skipped=${skipped}${DRY ? " (dry-run)" : ""}`
|
||||
);
|
||||
Reference in New Issue
Block a user