docs-manifest.mjs
1 import fs from "node:fs"; 2 import path from "node:path"; 3 import { fileURLToPath } from "node:url"; 4 5 const __filename = fileURLToPath(import.meta.url); 6 const __dirname = path.dirname(__filename); 7 const repoRoot = path.resolve(__dirname, "../../../"); 8 const docsRoot = path.join(repoRoot, "docs"); 9 const generatedRoot = path.join(docsRoot, "generated"); 10 const manifestPath = path.join(docsRoot, ".vitepress", "generated", "manifest.json"); 11 12 const blobBaseUrl = "https://github.com/alibaba/OpenSandbox/blob/main"; 13 const treeBaseUrl = "https://github.com/alibaba/OpenSandbox/tree/main"; 14 const rawBaseUrl = "https://raw.githubusercontent.com/alibaba/OpenSandbox/main"; 15 16 const ignoredDirNames = new Set([ 17 ".git", 18 ".github", 19 "node_modules", 20 ".vitepress", 21 ".pytest_cache", 22 "generated", 23 ".venv", 24 "venv", 25 "__pycache__", 26 "dist", 27 "build", 28 "target", 29 "bin", 30 ]); 31 const zhReadmePattern = /^README(?:[-_](?:zh|zh-cn|zh_cn))?\.md$/i; 32 const standardReadmePattern = /^README\.md$/i; 33 34 const sectionDefinitions = [ 35 { 36 id: "modules", 37 scanRoots: ["server", "components", "sandboxes", "kubernetes", "specs", "sdks"], 38 includeDevelopment: true, 39 }, 40 { 41 id: "examples", 42 scanRoots: ["examples"], 43 includeDevelopment: false, 44 }, 45 { 46 id: "community", 47 scanRoots: ["oseps"], 48 includeDevelopment: false, 49 }, 50 ]; 51 52 const manualEntries = [ 53 { 54 key: "guide-home", 55 sectionId: "overview", 56 slug: "overview/home", 57 enPath: "README.md", 58 zhPath: "docs/README_zh.md", 59 titleEn: "OpenSandbox", 60 titleZh: "OpenSandbox", 61 }, 62 { 63 key: "guide-architecture", 64 sectionId: "overview", 65 slug: "overview/architecture", 66 enPath: "docs/architecture.md", 67 zhPath: null, 68 titleEn: "Architecture", 69 titleZh: "架构设计", 70 }, 71 { 72 key: "guide-network", 73 sectionId: "modules", 74 slug: "design/single-host-network", 75 enPath: "docs/single_host_network.md", 76 zhPath: null, 77 titleEn: "Single Host Network", 78 titleZh: "单机场景网络设计", 79 }, 80 { 81 key: "community-contributing", 82 sectionId: "community", 83 slug: "community/contributing", 84 enPath: "CONTRIBUTING.md", 85 zhPath: null, 86 titleEn: "Contributing", 87 titleZh: "参与贡献", 88 }, 89 { 90 key: "community-code-of-conduct", 91 sectionId: "community", 92 slug: "community/code-of-conduct", 93 enPath: "CODE_OF_CONDUCT.md", 94 zhPath: null, 95 titleEn: "Code of Conduct", 96 titleZh: "行为准则", 97 }, 98 ]; 99 100 const moduleGroupLabels = { 101 en: { 102 sdks: "SDKs", 103 specs: "Specs & API", 104 server: "Server", 105 components: "Components", 106 sandboxes: "Sandboxes", 107 kubernetes: "Kubernetes", 108 design: "Design", 109 }, 110 zh: { 111 sdks: "SDKs", 112 specs: "Specs & API", 113 server: "Server", 114 components: "Components", 115 sandboxes: "Sandboxes", 116 kubernetes: "Kubernetes", 117 design: "设计", 118 }, 119 }; 120 121 const communityGroupLabels = { 122 en: { 123 community: "Community", 124 oseps: "OSEPs", 125 }, 126 zh: { 127 community: "社区", 128 oseps: "OSEPs", 129 }, 130 }; 131 132 const shortTitleByPath = { 133 "sdks/code-interpreter/javascript/README.md": "Code Interpreter JS SDK", 134 "sdks/code-interpreter/kotlin/README.md": "Code Interpreter Kotlin SDK", 135 "sdks/code-interpreter/python/README.md": "Code Interpreter Python SDK", 136 "sdks/code-interpreter/csharp/README.md": "Code Interpreter C# SDK", 137 "sdks/sandbox/javascript/README.md": "Sandbox JS SDK", 138 "sdks/sandbox/kotlin/README.md": "Sandbox Kotlin SDK", 139 "sdks/sandbox/python/README.md": "Sandbox Python SDK", 140 "sdks/sandbox/csharp/README.md": "Sandbox C# SDK", 141 "sdks/mcp/sandbox/python/README.md": "MCP Sandbox Python SDK", 142 "cli/README.md": "CLI (Python)", 143 "sdks/sandbox/kotlin/sandbox-api/build/generated/api/execd/README.md": "Sandbox Execd API (Kotlin)", 144 "sdks/sandbox/kotlin/sandbox-api/build/generated/api/lifecycle/README.md": "Sandbox Lifecycle API (Kotlin)", 145 146 "examples/agent-sandbox/README.md": "Agent Sandbox", 147 "examples/aio-sandbox/README.md": "AIO Sandbox", 148 "examples/chrome/README.md": "Chrome", 149 "examples/claude-code/README.md": "Claude Code", 150 "examples/code-interpreter/README.md": "Code Interpreter", 151 "examples/codex-cli/README.md": "Codex CLI", 152 "examples/desktop/README.md": "Desktop (VNC)", 153 "examples/gemini-cli/README.md": "Gemini CLI", 154 "examples/google-adk/README.md": "Google ADK", 155 "examples/host-volume-mount/README.md": "Host Volume Mount", 156 "examples/langgraph/README.md": "LangGraph", 157 "examples/playwright/README.md": "Playwright", 158 "examples/README.md": "Examples Overview", 159 "examples/rl-training/README.md": "RL Training", 160 "examples/vscode/README.md": "VS Code", 161 162 "server/README.md": "Server", 163 "server/DEVELOPMENT.md": "Server Development", 164 "components/ingress/README.md": "Ingress", 165 "components/ingress/DEVELOPMENT.md": "Ingress Development", 166 "components/egress/README.md": "Egress Sidecar", 167 "components/execd/README.md": "execd", 168 "components/execd/DEVELOPMENT.md": "execd Development", 169 "sandboxes/code-interpreter/README.md": "Code Interpreter Runtime", 170 "kubernetes/README.md": "Kubernetes Controller", 171 "kubernetes/examples/task-executor/README.md": "Task Executor", 172 "kubernetes/examples/controller/README.md": "Controller Example", 173 "oseps/README.md": "OSEP Overview", 174 }; 175 176 const shortTitleByPathZh = { 177 "sdks/code-interpreter/javascript/README.md": "代码解释器 JS SDK", 178 "sdks/code-interpreter/kotlin/README.md": "代码解释器 Kotlin SDK", 179 "sdks/code-interpreter/python/README.md": "代码解释器 Python SDK", 180 "sdks/code-interpreter/csharp/README.md": "代码解释器 C# SDK", 181 "sdks/sandbox/javascript/README.md": "沙箱 JS SDK", 182 "sdks/sandbox/kotlin/README.md": "沙箱 Kotlin SDK", 183 "sdks/sandbox/python/README.md": "沙箱 Python SDK", 184 "sdks/sandbox/csharp/README.md": "沙箱 C# SDK", 185 "sdks/mcp/sandbox/python/README.md": "MCP 沙箱 Python SDK", 186 "cli/README.md": "CLI(Python)", 187 "sdks/sandbox/kotlin/sandbox-api/build/generated/api/execd/README.md": "沙箱 Execd API(Kotlin)", 188 "sdks/sandbox/kotlin/sandbox-api/build/generated/api/lifecycle/README.md": "沙箱生命周期 API(Kotlin)", 189 190 "examples/agent-sandbox/README.md": "Agent Sandbox", 191 "examples/aio-sandbox/README.md": "AIO 沙箱", 192 "examples/chrome/README.md": "Chrome", 193 "examples/claude-code/README.md": "Claude Code", 194 "examples/code-interpreter/README.md": "代码解释器", 195 "examples/codex-cli/README.md": "Codex CLI", 196 "examples/desktop/README.md": "桌面环境(VNC)", 197 "examples/gemini-cli/README.md": "Gemini CLI", 198 "examples/google-adk/README.md": "Google ADK", 199 "examples/host-volume-mount/README.md": "宿主机目录挂载", 200 "examples/langgraph/README.md": "LangGraph", 201 "examples/playwright/README.md": "Playwright", 202 "examples/README.md": "示例总览", 203 "examples/rl-training/README.md": "强化学习训练", 204 "examples/vscode/README.md": "VS Code", 205 206 "server/README.md": "Server", 207 "server/DEVELOPMENT.md": "Server 开发指南", 208 "components/ingress/README.md": "Ingress", 209 "components/ingress/DEVELOPMENT.md": "Ingress 开发指南", 210 "components/egress/README.md": "Egress Sidecar", 211 "components/execd/README.md": "execd", 212 "components/execd/DEVELOPMENT.md": "execd 开发指南", 213 "sandboxes/code-interpreter/README.md": "代码解释器运行时", 214 "kubernetes/README.md": "Kubernetes 控制器", 215 "kubernetes/examples/task-executor/README.md": "Task Executor", 216 "kubernetes/examples/controller/README.md": "Controller 示例", 217 "oseps/README.md": "OSEP 总览", 218 }; 219 220 function ensureDir(dirPath) { 221 fs.mkdirSync(dirPath, { recursive: true }); 222 } 223 224 function rmIfExists(targetPath) { 225 if (fs.existsSync(targetPath)) { 226 fs.rmSync(targetPath, { recursive: true, force: true, maxRetries: 5, retryDelay: 80 }); 227 } 228 } 229 230 function walkMarkdownFiles(absDirPath, acc = []) { 231 const entries = fs.readdirSync(absDirPath, { withFileTypes: true }); 232 for (const entry of entries) { 233 if (ignoredDirNames.has(entry.name)) { 234 continue; 235 } 236 const absPath = path.join(absDirPath, entry.name); 237 if (entry.isDirectory()) { 238 walkMarkdownFiles(absPath, acc); 239 continue; 240 } 241 if (!entry.isFile()) { 242 continue; 243 } 244 if (entry.name.endsWith(".md")) { 245 acc.push(absPath); 246 } 247 } 248 return acc; 249 } 250 251 function shouldIgnoreRepoPath(repoRelPath) { 252 const normalized = repoRelPath.replaceAll("\\", "/"); 253 const denylistFragments = [ 254 "/.venv/", 255 "/venv/", 256 "/node_modules/", 257 "/docs/.vitepress/", 258 "/docs/generated/", 259 "/.pytest_cache/", 260 "/__pycache__/", 261 "/dist/", 262 "/build/", 263 "/target/", 264 "/bin/", 265 ]; 266 return denylistFragments.some((fragment) => normalized.includes(fragment)); 267 } 268 269 function toRepoRelative(absPath) { 270 return path.relative(repoRoot, absPath).replaceAll(path.sep, "/"); 271 } 272 273 function readHeadingTitle(absPath, fallbackTitle) { 274 if (!fs.existsSync(absPath)) { 275 return fallbackTitle; 276 } 277 const content = fs.readFileSync(absPath, "utf8"); 278 const lines = content.split(/\r?\n/); 279 let inFence = false; 280 for (const line of lines) { 281 const trimmed = line.trimStart(); 282 if (trimmed.startsWith("```")) { 283 inFence = !inFence; 284 continue; 285 } 286 if (inFence) { 287 continue; 288 } 289 const matched = trimmed.match(/^#{1,3}\s+(.+)$/); 290 if (matched) { 291 return matched[1].trim(); 292 } 293 } 294 return fallbackTitle; 295 } 296 297 function normalizeTitleWhitespace(title) { 298 return title.replace(/\s+/g, " ").trim(); 299 } 300 301 function shortenOsepTitle(repoRelPath, title, locale = "en") { 302 const match = repoRelPath.match(/^oseps\/(0\d{3})-(.+)\.md$/i); 303 if (!match) { 304 return title; 305 } 306 const number = match[1]; 307 const slug = match[2].toLowerCase(); 308 if (locale === "zh") { 309 if (slug.includes("fqdn") && slug.includes("egress")) { 310 return `OSEP-${number}: FQDN 出口访问控制`; 311 } 312 if (slug.includes("agent-sandbox") || slug.includes("kubernetes-sigs")) { 313 return `OSEP-${number}: Kubernetes Agent Sandbox 支持`; 314 } 315 if (slug.includes("volume")) { 316 return `OSEP-${number}: Volume 与 VolumeBinding 支持`; 317 } 318 } 319 if (slug.includes("fqdn") && slug.includes("egress")) { 320 return `OSEP-${number}: FQDN Egress Control`; 321 } 322 if (slug.includes("agent-sandbox") || slug.includes("kubernetes-sigs")) { 323 return `OSEP-${number}: Agent Sandbox on Kubernetes`; 324 } 325 if (slug.includes("volume")) { 326 return `OSEP-${number}: Volume & VolumeBinding Support`; 327 } 328 const readable = slug 329 .split("-") 330 .map((part) => (part.length <= 3 ? part.toUpperCase() : part.charAt(0).toUpperCase() + part.slice(1))) 331 .join(" "); 332 return `OSEP-${number}: ${readable}`; 333 } 334 335 function shortenTitleByRule(title) { 336 let next = normalizeTitleWhitespace(title); 337 next = next.replace(/^Alibaba\s+/i, ""); 338 next = next.replace(/^OpenSandbox\s+/i, ""); 339 next = next.replace(/\bJavaScript\/TypeScript\b/g, "JS"); 340 next = next.replace(/\bJava\/Kotlin\b/g, "Kotlin"); 341 next = next.replace(/\s+Example$/i, ""); 342 next = next.replace(/\s+SDK for /i, " "); 343 return normalizeTitleWhitespace(next); 344 } 345 346 function shortenTitleByRuleZh(title) { 347 let next = normalizeTitleWhitespace(title); 348 next = next.replace(/^Alibaba\s+/i, ""); 349 next = next.replace(/^OpenSandbox\s+/i, ""); 350 next = next.replace(/\bJavaScript\/TypeScript\b/g, "JS"); 351 next = next.replace(/\bJava\/Kotlin\b/g, "Kotlin"); 352 next = next.replace(/\s+Example$/i, " 示例"); 353 next = next.replace(/\s+SDK for /i, " "); 354 return normalizeTitleWhitespace(next); 355 } 356 357 function getShortTitle(repoRelPath, currentTitle, locale = "en") { 358 if (locale === "zh" && shortTitleByPathZh[repoRelPath]) { 359 return shortTitleByPathZh[repoRelPath]; 360 } 361 if (locale !== "zh" && shortTitleByPath[repoRelPath]) { 362 return shortTitleByPath[repoRelPath]; 363 } 364 if (/^oseps\/0\d{3}-.+\.md$/i.test(repoRelPath)) { 365 return shortenOsepTitle(repoRelPath, currentTitle, locale); 366 } 367 if (locale === "zh") { 368 return shortenTitleByRuleZh(currentTitle); 369 } 370 return shortenTitleByRule(currentTitle); 371 } 372 373 function toYamlString(value) { 374 return `"${String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`; 375 } 376 377 function normalizeSlugFromPath(relPath) { 378 const normalized = relPath.replaceAll("\\", "/"); 379 const dirName = path.posix.dirname(normalized); 380 const baseName = path.posix.basename(normalized); 381 const lowerBase = baseName.toLowerCase(); 382 383 if (lowerBase === "readme.md" || zhReadmePattern.test(baseName)) { 384 return dirName === "." ? "overview/home" : `${dirName}/readme`; 385 } 386 if (lowerBase === "development.md") { 387 return `${dirName}/development`; 388 } 389 return normalized.replace(/\.md$/i, ""); 390 } 391 392 function resolveZhCandidate(repoRelPath, readmeCandidatesByDir) { 393 const dir = path.posix.dirname(repoRelPath); 394 const candidates = readmeCandidatesByDir.get(dir) ?? []; 395 for (const candidate of candidates) { 396 if (candidate.toLowerCase() !== "readme.md") { 397 return `${dir}/${candidate}`; 398 } 399 } 400 return null; 401 } 402 403 function buildGeneratedAssetPath(locale, routeSlug, resolvedRepoPath) { 404 const normalized = resolvedRepoPath.replaceAll("\\", "/"); 405 if (!normalized.startsWith("docs/assets/")) { 406 return null; 407 } 408 const generatedDir = path.posix.dirname(`generated/${locale}/${routeSlug}.md`); 409 const assetPath = normalized.replace(/^docs\//, ""); 410 let relativePath = path.posix.relative(generatedDir, assetPath); 411 if (!relativePath || relativePath === "") { 412 relativePath = "./"; 413 } 414 if (!relativePath.startsWith(".") && !relativePath.startsWith("/")) { 415 relativePath = `./${relativePath}`; 416 } 417 return relativePath; 418 } 419 420 function normalizeLinkTarget(target, sourceDirRel, isImage, routeSlug, locale) { 421 if ( 422 target.startsWith("http://") || 423 target.startsWith("https://") || 424 target.startsWith("mailto:") || 425 target.startsWith("#") || 426 target.startsWith("data:") || 427 target.startsWith("/") 428 ) { 429 return target; 430 } 431 432 const [rawPath, hashFragment] = target.split("#"); 433 const resolvedPath = path.posix.normalize(path.posix.join(sourceDirRel, rawPath)); 434 const localAssetPath = isImage ? buildGeneratedAssetPath(locale, routeSlug, resolvedPath) : null; 435 if (localAssetPath) { 436 if (hashFragment) { 437 return `${localAssetPath}#${hashFragment}`; 438 } 439 return localAssetPath; 440 } 441 442 const urlBase = isImage 443 ? `${rawBaseUrl}/${resolvedPath}` 444 : fs.existsSync(path.join(repoRoot, resolvedPath)) && 445 fs.statSync(path.join(repoRoot, resolvedPath)).isDirectory() 446 ? `${treeBaseUrl}/${resolvedPath}` 447 : `${blobBaseUrl}/${resolvedPath}`; 448 449 if (hashFragment) { 450 return `${urlBase}#${hashFragment}`; 451 } 452 return urlBase; 453 } 454 455 function rewriteRelativeLinks(markdown, sourceRelPath, routeSlug, locale) { 456 const sourceDirRel = path.posix.dirname(sourceRelPath); 457 458 const withMarkdownLinks = markdown.replace( 459 /(!?)\[([^\]]*?)\]\(([^)]+)\)/g, 460 (_match, imageMark, text, linkValue) => { 461 const trimmed = linkValue.trim(); 462 if (!trimmed) { 463 return _match; 464 } 465 const firstSpace = trimmed.search(/\s/); 466 const target = firstSpace === -1 ? trimmed : trimmed.slice(0, firstSpace); 467 const trailing = firstSpace === -1 ? "" : trimmed.slice(firstSpace); 468 const rewrittenTarget = normalizeLinkTarget(target, sourceDirRel, imageMark === "!", routeSlug, locale); 469 return `${imageMark}[${text}](${rewrittenTarget}${trailing})`; 470 }, 471 ); 472 473 return withMarkdownLinks.replace( 474 /<img([^>]*?)src=(["'])([^"']+)\2([^>]*)>/gi, 475 (matched, before, quote, src, after) => { 476 const rewritten = normalizeLinkTarget(src, sourceDirRel, true, routeSlug, locale); 477 return `<img${before}src=${quote}${rewritten}${quote}${after}>`; 478 }, 479 ); 480 } 481 482 function renderPageSource({ locale, title, sourceRelPath, routeSlug, passthrough = false }) { 483 const sourceAbsPath = path.join(repoRoot, sourceRelPath); 484 const sourceMarkdown = fs.readFileSync(sourceAbsPath, "utf8"); 485 const displayTitle = title || readHeadingTitle(sourceAbsPath, path.posix.basename(sourceRelPath, ".md")); 486 487 let body = sourceMarkdown; 488 if (!passthrough) { 489 body = rewriteRelativeLinks(sourceMarkdown, sourceRelPath, routeSlug, locale); 490 } 491 492 const sourceUrl = `${blobBaseUrl}/${sourceRelPath}`; 493 const sourceNotice = 494 locale === "zh" 495 ? `> 此页内容来自仓库源文件:[\`${sourceRelPath}\`](${sourceUrl})` 496 : `> This page is sourced from: [\`${sourceRelPath}\`](${sourceUrl})`; 497 498 499 return `---\ntitle: ${toYamlString(displayTitle)}\n---\n\n${body}\n\n---\n\n${sourceNotice}\n`; 500 } 501 502 function prettifyPathTitle(repoRelPath) { 503 const dirPath = path.posix.dirname(repoRelPath); 504 if (dirPath === "." || dirPath === "docs") { 505 return "Overview"; 506 } 507 return dirPath 508 .split("/") 509 .map((part) => 510 part 511 .replaceAll("-", " ") 512 .replaceAll("_", " ") 513 .replace(/\b\w/g, (ch) => ch.toUpperCase()), 514 ) 515 .join(" / "); 516 } 517 518 function collectAutoEntries() { 519 const readmeCandidatesByDir = new Map(); 520 const entries = []; 521 522 for (const section of sectionDefinitions) { 523 for (const scanRoot of section.scanRoots) { 524 const absScanRoot = path.join(repoRoot, scanRoot); 525 if (!fs.existsSync(absScanRoot)) { 526 continue; 527 } 528 const files = walkMarkdownFiles(absScanRoot); 529 for (const absPath of files) { 530 const repoRelPath = toRepoRelative(absPath); 531 if (shouldIgnoreRepoPath(repoRelPath)) { 532 continue; 533 } 534 const fileName = path.posix.basename(repoRelPath); 535 const dirName = path.posix.dirname(repoRelPath); 536 537 if (zhReadmePattern.test(fileName)) { 538 const arr = readmeCandidatesByDir.get(dirName) ?? []; 539 arr.push(fileName); 540 readmeCandidatesByDir.set(dirName, arr); 541 } 542 } 543 } 544 } 545 546 for (const section of sectionDefinitions) { 547 for (const scanRoot of section.scanRoots) { 548 const absScanRoot = path.join(repoRoot, scanRoot); 549 if (!fs.existsSync(absScanRoot)) { 550 continue; 551 } 552 const files = walkMarkdownFiles(absScanRoot); 553 for (const absPath of files) { 554 const repoRelPath = toRepoRelative(absPath); 555 if (shouldIgnoreRepoPath(repoRelPath)) { 556 continue; 557 } 558 const fileName = path.posix.basename(repoRelPath); 559 if (zhReadmePattern.test(fileName) && !standardReadmePattern.test(fileName)) { 560 continue; 561 } 562 563 const isReadme = standardReadmePattern.test(fileName); 564 const isDevelopment = fileName === "DEVELOPMENT.md"; 565 const isOsepDoc = section.id === "community" && /^0\d{3}-.+\.md$/i.test(fileName); 566 if (!isReadme && !(section.includeDevelopment && isDevelopment) && !isOsepDoc) { 567 continue; 568 } 569 570 const zhCandidate = isReadme ? resolveZhCandidate(repoRelPath, readmeCandidatesByDir) : null; 571 const entryKey = `auto:${section.id}:${repoRelPath}`; 572 const slug = normalizeSlugFromPath(repoRelPath); 573 const titleFallback = isDevelopment ? `${prettifyPathTitle(repoRelPath)} Development` : prettifyPathTitle(repoRelPath); 574 entries.push({ 575 key: entryKey, 576 sectionId: section.id, 577 slug, 578 enPath: repoRelPath, 579 zhPath: zhCandidate, 580 titleEn: getShortTitle(repoRelPath, readHeadingTitle(absPath, titleFallback), "en"), 581 titleZh: getShortTitle( 582 repoRelPath, 583 readHeadingTitle( 584 zhCandidate ? path.join(repoRoot, zhCandidate) : absPath, 585 readHeadingTitle(absPath, titleFallback), 586 ), 587 "zh", 588 ), 589 }); 590 } 591 } 592 } 593 594 const unique = new Map(); 595 for (const item of entries) { 596 if (!unique.has(item.key)) { 597 unique.set(item.key, item); 598 } 599 } 600 return [...unique.values()].sort((a, b) => a.slug.localeCompare(b.slug)); 601 } 602 603 function buildEntries() { 604 const autoEntries = collectAutoEntries(); 605 const all = [...manualEntries, ...autoEntries]; 606 const uniqueBySlug = new Map(); 607 608 for (const item of all) { 609 if (uniqueBySlug.has(item.slug)) { 610 continue; 611 } 612 uniqueBySlug.set(item.slug, item); 613 } 614 return [...uniqueBySlug.values()]; 615 } 616 617 function toSidebarItems(entries, locale) { 618 return entries 619 .map((entry) => ({ 620 text: locale === "zh" ? entry.titleZh || entry.titleEn : entry.titleEn, 621 link: locale === "zh" ? `/zh/${entry.slug}` : `/${entry.slug}`, 622 })) 623 .sort((a, b) => a.link.localeCompare(b.link)); 624 } 625 626 function buildOverviewSidebar(entries, locale) { 627 const overviewEntries = entries.filter((entry) => entry.sectionId === "overview"); 628 const slugOrder = ["overview/home", "overview/architecture"]; 629 const items = overviewEntries 630 .sort((a, b) => { 631 const ai = slugOrder.indexOf(a.slug); 632 const bi = slugOrder.indexOf(b.slug); 633 if (ai === -1 && bi === -1) return a.slug.localeCompare(b.slug); 634 if (ai === -1) return 1; 635 if (bi === -1) return -1; 636 return ai - bi; 637 }) 638 .map((entry) => ({ 639 text: locale === "zh" ? entry.titleZh || entry.titleEn : entry.titleEn, 640 link: locale === "zh" ? `/zh/${entry.slug}` : `/${entry.slug}`, 641 })); 642 if (items.length === 0) { 643 return []; 644 } 645 return [{ text: locale === "zh" ? "Overview" : "Overview", items }]; 646 } 647 648 function buildModulesSidebar(entries, locale) { 649 const modules = entries.filter((entry) => entry.sectionId === "modules"); 650 const byPrefix = new Map(); 651 for (const entry of modules) { 652 const prefix = entry.slug.split("/")[0]; 653 const arr = byPrefix.get(prefix) ?? []; 654 arr.push(entry); 655 byPrefix.set(prefix, arr); 656 } 657 658 const order = ["sdks", "specs", "design", "server", "components", "sandboxes", "kubernetes"]; 659 const blocks = []; 660 for (const prefix of order) { 661 const groupEntries = byPrefix.get(prefix); 662 if (!groupEntries || groupEntries.length === 0) { 663 continue; 664 } 665 blocks.push({ 666 text: moduleGroupLabels[locale][prefix], 667 items: toSidebarItems(groupEntries, locale), 668 }); 669 } 670 return blocks; 671 } 672 673 function buildExamplesSidebar(entries, locale) { 674 const items = toSidebarItems(entries.filter((entry) => entry.sectionId === "examples"), locale); 675 if (items.length === 0) { 676 return []; 677 } 678 return [{ text: locale === "zh" ? "示例" : "Examples", items }]; 679 } 680 681 function buildCommunitySidebar(entries, locale) { 682 const blocks = []; 683 const communityEntries = entries.filter( 684 (entry) => entry.sectionId === "community" && entry.slug.startsWith("community/"), 685 ); 686 if (communityEntries.length > 0) { 687 blocks.push({ 688 text: communityGroupLabels[locale].community, 689 items: toSidebarItems(communityEntries, locale), 690 }); 691 } 692 693 const osepReadmeEntries = entries.filter((entry) => entry.sectionId === "community" && entry.slug === "oseps/readme"); 694 const osepDocEntries = entries.filter( 695 (entry) => entry.sectionId === "community" && entry.slug.startsWith("oseps/") && entry.slug !== "oseps/readme", 696 ); 697 const sortedOsepDocs = osepDocEntries.sort((a, b) => a.slug.localeCompare(b.slug)); 698 const osepItems = [...toSidebarItems(osepReadmeEntries, locale), ...toSidebarItems(sortedOsepDocs, locale)]; 699 if (osepItems.length > 0) { 700 blocks.push({ 701 text: communityGroupLabels[locale].oseps, 702 items: osepItems, 703 }); 704 } 705 706 return blocks; 707 } 708 709 function buildSidebarByPath(entries, locale) { 710 const prefix = locale === "zh" ? "/zh" : ""; 711 const overviewSidebar = buildOverviewSidebar(entries, locale); 712 const modulesSidebar = buildModulesSidebar(entries, locale); 713 const examplesSidebar = buildExamplesSidebar(entries, locale); 714 const communitySidebar = buildCommunitySidebar(entries, locale); 715 716 const sidebar = { 717 [`${prefix}/`]: overviewSidebar, 718 [`${prefix}/overview/`]: overviewSidebar, 719 [`${prefix}/examples/`]: examplesSidebar, 720 [`${prefix}/community/`]: communitySidebar, 721 [`${prefix}/oseps/`]: communitySidebar, 722 }; 723 724 for (const modulesPrefix of ["server", "components", "sandboxes", "kubernetes", "specs", "sdks", "design"]) { 725 sidebar[`${prefix}/${modulesPrefix}/`] = modulesSidebar; 726 } 727 return sidebar; 728 } 729 730 function writeGeneratedPages(entries) { 731 rmIfExists(generatedRoot); 732 ensureDir(path.join(generatedRoot, "en")); 733 ensureDir(path.join(generatedRoot, "zh")); 734 735 const rewrites = {}; 736 const pages = []; 737 738 for (const entry of entries) { 739 const enSourcePath = entry.enPath; 740 const zhSourcePath = entry.zhPath || entry.enPath; 741 const enGeneratedRel = `generated/en/${entry.slug}.md`; 742 const zhGeneratedRel = `generated/zh/${entry.slug}.md`; 743 const enGeneratedAbs = path.join(docsRoot, enGeneratedRel); 744 const zhGeneratedAbs = path.join(docsRoot, zhGeneratedRel); 745 ensureDir(path.dirname(enGeneratedAbs)); 746 ensureDir(path.dirname(zhGeneratedAbs)); 747 748 fs.writeFileSync( 749 enGeneratedAbs, 750 renderPageSource({ 751 locale: "en", 752 title: entry.titleEn, 753 sourceRelPath: enSourcePath, 754 routeSlug: entry.slug, 755 passthrough: entry.passthrough === true, 756 }), 757 "utf8", 758 ); 759 760 fs.writeFileSync( 761 zhGeneratedAbs, 762 renderPageSource({ 763 locale: "zh", 764 title: entry.titleZh || entry.titleEn, 765 sourceRelPath: zhSourcePath, 766 routeSlug: entry.slug, 767 passthrough: entry.passthrough === true, 768 }), 769 "utf8", 770 ); 771 772 rewrites[enGeneratedRel] = `${entry.slug}.md`; 773 rewrites[zhGeneratedRel] = `zh/${entry.slug}.md`; 774 775 pages.push({ 776 key: entry.key, 777 slug: entry.slug, 778 en: enSourcePath, 779 zh: zhSourcePath, 780 }); 781 } 782 783 return { rewrites, pages }; 784 } 785 786 export function buildManifest() { 787 const entries = buildEntries(); 788 const { rewrites, pages } = writeGeneratedPages(entries); 789 const manifest = { 790 generatedAt: new Date().toISOString(), 791 pages, 792 nav: { 793 en: [ 794 { text: "Overview", link: "/overview/home" }, 795 { text: "Project", link: "/sdks/sandbox/python/readme" }, 796 { text: "Examples", link: "/examples/readme" }, 797 { text: "Community", link: "/community/contributing" }, 798 ], 799 zh: [ 800 { text: "Overview", link: "/zh/overview/home" }, 801 { text: "Project", link: "/zh/sdks/sandbox/python/readme" }, 802 { text: "Examples", link: "/zh/examples/readme" }, 803 { text: "Community", link: "/zh/community/contributing" }, 804 ], 805 }, 806 sidebar: { 807 en: buildSidebarByPath(entries, "en"), 808 zh: buildSidebarByPath(entries, "zh"), 809 }, 810 rewrites, 811 }; 812 813 ensureDir(path.dirname(manifestPath)); 814 fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8"); 815 return manifest; 816 } 817 818 export function loadManifest() { 819 try { 820 if (!fs.existsSync(manifestPath)) { 821 return buildManifest(); 822 } 823 const data = JSON.parse(fs.readFileSync(manifestPath, "utf8")); 824 if (!data || !data.generatedAt || !data.nav || !data.sidebar || !data.rewrites) { 825 return buildManifest(); 826 } 827 return buildManifest(); 828 } catch (_error) { 829 return buildManifest(); 830 } 831 } 832 833 if (process.argv[1] === fileURLToPath(import.meta.url)) { 834 const manifest = buildManifest(); 835 // Keep logging terse for CI output. 836 console.log(`docs manifest generated (${manifest.pages.length} pages)`); 837 }