@docubook/flame 2.0.0-beta.3 → 2.0.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/.docu/components/Pagination.tsx +12 -1
  2. package/.docu/lib/build.deno.js +1 -1
  3. package/.docu/lib/{build.impl-Bz_p421t.js → build.impl-CT-YLyLn.js} +22 -21
  4. package/.docu/lib/build.impl-CT-YLyLn.js.map +1 -0
  5. package/.docu/lib/build.impl-cJPJeQ8r.js +2 -0
  6. package/.docu/lib/build.node.js +1 -1
  7. package/.docu/lib/clean.js +1 -1
  8. package/.docu/lib/deploy.deno.js +1 -1
  9. package/.docu/lib/deploy.node.js +1 -1
  10. package/.docu/lib/{deploy.shared-fk8eM-r4.js → deploy.shared-CmOzm3b5.js} +2 -2
  11. package/.docu/lib/{deploy.shared-fk8eM-r4.js.map → deploy.shared-CmOzm3b5.js.map} +1 -1
  12. package/.docu/lib/{html.shared-FwgbE1WG.js → html.shared-WwQnhDty.js} +30 -24
  13. package/.docu/lib/html.shared-WwQnhDty.js.map +1 -0
  14. package/.docu/lib/{logger-CQyNTE6L.js → logger-Cz0CPMi8.js} +2 -2
  15. package/.docu/lib/{logger-CQyNTE6L.js.map → logger-Cz0CPMi8.js.map} +1 -1
  16. package/.docu/lib/preview.deno.js +1 -1
  17. package/.docu/lib/{preview.impl-CnsbLmDA.js → preview.impl-BaYhTRBW.js} +3 -3
  18. package/.docu/lib/{preview.impl-CnsbLmDA.js.map → preview.impl-BaYhTRBW.js.map} +1 -1
  19. package/.docu/lib/preview.node.js +1 -1
  20. package/.docu/lib/server.deno.js +1 -1
  21. package/.docu/lib/{server.impl-CXTzYqbF.js → server.impl-l8hXfV_0.js} +6 -6
  22. package/.docu/lib/server.impl-l8hXfV_0.js.map +1 -0
  23. package/.docu/lib/server.node.js +1 -1
  24. package/.docu/lib/{utils-DA17MyQG.js → utils-B_CoyKie.js} +28 -6
  25. package/.docu/lib/utils-B_CoyKie.js.map +1 -0
  26. package/.docu/node/build.impl.ts +42 -35
  27. package/.docu/node/build.ts +57 -41
  28. package/.docu/node/cache-key.ts +1 -1
  29. package/.docu/node/html.shared.ts +2 -1
  30. package/.docu/node/html.ts +2 -1
  31. package/.docu/node/plugin-builder.ts +10 -1
  32. package/.docu/node/route.ts +19 -21
  33. package/.docu/node/security.ts +18 -1
  34. package/.docu/node/server-routes.ts +3 -3
  35. package/.docu/node/utils.ts +22 -2
  36. package/package.json +6 -6
  37. package/.docu/lib/build.impl-Bz_p421t.js.map +0 -1
  38. package/.docu/lib/build.impl-CtPrlYAE.js +0 -2
  39. package/.docu/lib/html.shared-FwgbE1WG.js.map +0 -1
  40. package/.docu/lib/server.impl-CXTzYqbF.js.map +0 -1
  41. package/.docu/lib/utils-DA17MyQG.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"logger-CQyNTE6L.js","names":[],"sources":["../../package.json","../node/logger.ts"],"sourcesContent":["{\n \"name\": \"@docubook/flame\",\n \"version\": \"2.0.0-beta.3\",\n \"description\": \"A blazing-fast React + MDX framework powered by Bun, built for modern documentation experiences.\",\n \"type\": \"module\",\n \"bin\": {\n \"flame\": \"./bin/cli.js\"\n },\n \"files\": [\n \"bin\",\n \"template\",\n \"docu.schema.json\",\n \".docu\",\n \"!.docu/__tests__\",\n \"!.docu/dist\",\n \"!.docu/build-cache.json\",\n \"README.md\",\n \"LICENSE\"\n ],\n \"publishConfig\": {\n \"registry\": \"https://registry.npmjs.org/\",\n \"access\": \"public\"\n },\n \"engines\": {\n \"node\": \">=20.11\",\n \"bun\": \">=1.4.0\"\n },\n \"scripts\": {\n \"dev\": \"bun .docu/node/server.ts\",\n \"build\": \"NODE_ENV=production bun .docu/node/build.ts\",\n \"clean\": \"bun .docu/node/clean.ts\",\n \"preview\": \"bun .docu/node/preview.ts\",\n \"deploy\": \"bun .docu/node/deploy.ts\",\n \"compile:lib\": \"node bin/compile-lib.mjs\",\n \"test\": \"vitest run\",\n \"lint\": \"oxlint .\",\n \"prepublishOnly\": \"bun .docu/node/clean.ts && oxlint . && vitest run && node bin/compile-lib.mjs\"\n },\n \"keywords\": [\n \"docubook\",\n \"documentation\",\n \"static-site\",\n \"daisyui\",\n \"bun\",\n \"docs framework\",\n \"react\",\n \"react-dom\"\n ],\n \"homepage\": \"https://github.com/DocuBook/docubook/tree/main/packages/flame\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/DocuBook/docubook.git\",\n \"directory\": \"packages/flame\"\n },\n \"author\": \"wildan.nrs\",\n \"author-url\": \"https://wildan.dev\",\n \"license\": \"MIT\",\n \"dependencies\": {\n \"@docubook/core\": \"workspace:^\",\n \"@docubook/markdown\": \"workspace:^\",\n \"@docubook/themes-colors\": \"workspace:^\",\n \"@docubook/ui-react\": \"workspace:^\",\n \"@mdx-js/react\": \"^3.0.1\",\n \"@tailwindcss/cli\": \"4.3.0\",\n \"@tailwindcss/typography\": \"0.5.16\",\n \"bun-plugin-tailwind\": \"^0.1.2\",\n \"daisyui\": \"^5.5.19\",\n \"lucide-react\": \"^1.14.0\",\n \"react\": \"^19.2.7\",\n \"react-dom\": \"^19.2.7\",\n \"unified\": \"^11.0.0\",\n \"vite\": \"^8.2.1\",\n \"zod\": \"^4.4.3\"\n },\n \"peerDependencies\": {\n \"@sentry/bun\": \"^10.0.0\"\n },\n \"peerDependenciesMeta\": {\n \"@sentry/bun\": {\n \"optional\": true\n }\n },\n \"devDependencies\": {\n \"@types/node\": \"^22.0.0\",\n \"@types/react\": \"^19.0.0\",\n \"@types/react-dom\": \"^19.0.0\",\n \"bun-types\": \"^1.4.0\",\n \"tailwindcss\": \"^4.3.0\",\n \"typescript\": \"^7.0.2\"\n }\n}\n","/**\n * Interactive CLI logger with spinner and tree-style route display.\n * Inspired by Next.js/Turbopack build output.\n *\n * Supports structured logging via environment variables:\n * - LOG_LEVEL: debug | info | warn | error (default: info)\n * - LOG_FORMAT: json | pretty (default: pretty)\n */\n\nimport { loadDocuConfig } from \"./paths\";\nimport pkg from \"../../package.json\" with { type: \"json\" };\n\nconst docuConfig = loadDocuConfig();\nconst isCI = !!(process.env.CI || process.env.NO_COLOR || !process.stdout.isTTY);\nconst LOG_FORMAT = process.env.LOG_FORMAT || \"pretty\";\nconst isJSON = LOG_FORMAT === \"json\";\n\ntype LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\nconst LEVELS: Record<LogLevel, number> = { debug: 0, info: 1, warn: 2, error: 3 };\nconst currentLevel = LEVELS[(process.env.LOG_LEVEL as LogLevel) || \"info\"] ?? LEVELS.info;\n\nfunction shouldLog(level: LogLevel): boolean {\n return LEVELS[level] >= currentLevel;\n}\n\nfunction jsonLog(level: LogLevel, msg: string, meta?: Record<string, unknown>) {\n if (!shouldLog(level)) return;\n const entry = { ts: new Date().toISOString(), ...meta, level, msg };\n const out = level === \"error\" ? console.error : level === \"warn\" ? console.warn : console.log;\n out(JSON.stringify(entry));\n}\n\n/** Returns true if the caller should skip (already handled or filtered). */\nfunction guard(level: LogLevel, msg: string, meta?: Record<string, unknown>): boolean {\n if (isJSON) {\n jsonLog(level, msg, meta);\n return true;\n }\n return !shouldLog(level);\n}\n\nconst c =\n isCI || isJSON\n ? {\n reset: \"\",\n bold: \"\",\n dim: \"\",\n green: \"\",\n cyan: \"\",\n magenta: \"\",\n yellow: \"\",\n red: \"\",\n white: \"\",\n gray: \"\",\n }\n : {\n reset: \"\\x1b[0m\",\n bold: \"\\x1b[1m\",\n dim: \"\\x1b[2m\",\n green: \"\\x1b[32m\",\n cyan: \"\\x1b[36m\",\n magenta: \"\\x1b[35m\",\n yellow: \"\\x1b[33m\",\n red: \"\\x1b[31m\",\n white: \"\\x1b[37m\",\n gray: \"\\x1b[90m\",\n };\n\nconst SPINNER_FRAMES = [\"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\"];\n\nclass Spinner {\n private frame = 0;\n private message = \"\";\n private timer: ReturnType<typeof setInterval> | null = null;\n\n info(finalMsg: string) {\n if (this.timer) clearInterval(this.timer);\n this.timer = null;\n if (isCI) {\n console.log(`ℹ ${finalMsg}`);\n } else {\n process.stdout.write(`\\r\\x1b[K${c.cyan}ℹ${c.reset} ${finalMsg}\\n`);\n process.stdout.write(\"\\x1b[?25h\");\n }\n }\n\n start(msg: string) {\n this.message = msg;\n if (isCI) {\n console.log(`… ${msg}`);\n return;\n }\n this.frame = 0;\n process.stdout.write(\"\\x1b[?25l\");\n this.render();\n this.timer = setInterval(() => this.render(), 80);\n }\n\n stop(finalMsg: string) {\n if (this.timer) clearInterval(this.timer);\n this.timer = null;\n if (isCI) {\n console.log(`✓ ${finalMsg}`);\n return;\n }\n process.stdout.write(`\\r\\x1b[K${c.green}✓${c.reset} ${finalMsg}\\n`);\n process.stdout.write(\"\\x1b[?25h\");\n }\n\n private render() {\n const f = SPINNER_FRAMES[this.frame % SPINNER_FRAMES.length];\n process.stdout.write(`\\r${c.cyan}${f}${c.reset} ${this.message}`);\n this.frame++;\n }\n}\n\ninterface RouteItem {\n title: string;\n href: string;\n noLink?: boolean;\n items?: RouteItem[];\n}\n\nconst MAX_ROUTE_LINES = 5;\nlet routeLineCount = 0;\nlet routeTruncated = false;\n\nfunction printRouteTree(routes: RouteItem[], prefix = \"\", isRoot = true) {\n for (let i = 0; i < routes.length; i++) {\n if (routeTruncated) return;\n if (routeLineCount >= MAX_ROUTE_LINES) {\n routeTruncated = true;\n process.stdout.write(`${prefix}${c.dim}...${c.reset}\\n`);\n return;\n }\n\n const route = routes[i];\n const last = i === routes.length - 1;\n const connector = isRoot ? \"\" : last ? \"└── \" : \"├── \";\n const childPrefix = isRoot ? \"\" : last ? \" \" : \"│ \";\n\n const path = route.noLink\n ? `${c.dim}${route.href}${c.reset}`\n : `${c.white}${route.href}${c.reset}`;\n\n const label = route.noLink ? `${c.gray}(group)${c.reset}` : \"\";\n\n process.stdout.write(`${prefix}${connector}${path} ${label}\\n`);\n routeLineCount++;\n\n if (route.items?.length) {\n printRouteTree(route.items, prefix + childPrefix, false);\n }\n }\n}\n\nexport const logger = {\n spinner: new Spinner(),\n\n buildStart() {\n if (guard(\"info\", \"build_start\", { package: \"@docubook/flame\", version: pkg.version })) return;\n console.log(\n `\\n${c.bold}${c.cyan} 🔥 DocuBook Flame${c.reset} ${c.dim}v${pkg.version}${c.reset}\\n`\n );\n },\n\n bundleStart() {\n if (guard(\"info\", \"bundle_start\")) return;\n this.spinner.start(\"Building client bundle...\");\n },\n\n bundleDone(ms: number) {\n if (guard(\"info\", \"bundle_done\", { duration_ms: ms })) return;\n this.spinner.stop(`Client bundle compiled ${c.dim}(${ms}ms)${c.reset}`);\n },\n\n indexStart() {\n if (guard(\"info\", \"index_start\")) return;\n this.spinner.start(\"Generating search index...\");\n },\n\n indexDone(records: number, ms: number, skipped = false) {\n if (guard(\"info\", \"index_done\", { records, duration_ms: ms, skipped })) return;\n this.spinner.stop(\n skipped\n ? `Search index cached ${c.dim}(${ms}ms)${c.reset}`\n : `Search index generated ${c.dim}(${records} records, ${ms}ms)${c.reset}`\n );\n },\n\n routes() {\n if (guard(\"debug\", \"routes\")) return;\n const routes = docuConfig.routes as RouteItem[] | undefined;\n if (!routes?.length) return;\n\n console.log(`\\n${c.bold} Routes${c.reset} ${c.dim}(${countRoutes(routes)} pages)${c.reset}\\n`);\n routeLineCount = 0;\n routeTruncated = false;\n printRouteTree(routes, \" \");\n },\n\n ready(port: number, hmr = false) {\n if (guard(\"info\", \"server_ready\", { port, hmr })) return;\n console.log(\n `\\n${c.green}${c.bold} ✓ Ready${c.reset} in ${c.bold}http://localhost:${port}/docs/${c.reset}`\n );\n if (hmr) console.log(`${c.dim} HMR enabled — watching docs/ for changes${c.reset}`);\n console.log(\"\");\n },\n\n request(method: string, pathname: string, status: number, duration_ms?: number) {\n if (guard(\"info\", \"http_request\", { method, path: pathname, status, duration_ms })) return;\n const color = status >= 500 ? c.red : status >= 400 ? c.gray : c.green;\n const ts = new Date().toLocaleTimeString();\n const dur = duration_ms != null ? ` ${c.dim}${duration_ms}ms${c.reset}` : \"\";\n console.log(\n `${c.dim} ${ts}${c.reset} ${method} ${pathname} ${color}${status}${c.reset}${dur}`\n );\n },\n\n debug(msg: string, meta?: Record<string, unknown>) {\n if (guard(\"debug\", msg, meta)) return;\n console.log(`${c.gray} [debug] ${msg}${c.reset}`);\n },\n\n warn(msg: string, meta?: Record<string, unknown>) {\n if (guard(\"warn\", msg, meta)) return;\n console.warn(`${c.yellow} ⚠ ${msg}${c.reset}`);\n },\n\n error(msg: string, meta?: Record<string, unknown>) {\n if (guard(\"error\", msg, meta)) return;\n console.error(`${c.red} ✖ ${msg}${c.reset}`);\n },\n};\n\nfunction countRoutes(routes: RouteItem[]): number {\n let count = 0;\n for (const r of routes) {\n if (!r.noLink) count++;\n if (r.items) count += countRoutes(r.items);\n }\n return count;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"logger-Cz0CPMi8.js","names":[],"sources":["../../package.json","../node/logger.ts"],"sourcesContent":["{\n \"name\": \"@docubook/flame\",\n \"version\": \"2.0.0-beta.4\",\n \"description\": \"A blazing-fast React + MDX framework powered by Bun, built for modern documentation experiences.\",\n \"type\": \"module\",\n \"bin\": {\n \"flame\": \"./bin/cli.js\"\n },\n \"files\": [\n \"bin\",\n \"template\",\n \"docu.schema.json\",\n \".docu\",\n \"!.docu/__tests__\",\n \"!.docu/dist\",\n \"!.docu/build-cache.json\",\n \"README.md\",\n \"LICENSE\"\n ],\n \"publishConfig\": {\n \"registry\": \"https://registry.npmjs.org/\",\n \"access\": \"public\"\n },\n \"engines\": {\n \"node\": \">=20.11\",\n \"bun\": \">=1.4.0\"\n },\n \"scripts\": {\n \"dev\": \"bun .docu/node/server.ts\",\n \"build\": \"NODE_ENV=production bun .docu/node/build.ts\",\n \"clean\": \"bun .docu/node/clean.ts\",\n \"preview\": \"bun .docu/node/preview.ts\",\n \"deploy\": \"bun .docu/node/deploy.ts\",\n \"compile:lib\": \"node bin/compile-lib.mjs\",\n \"test\": \"vitest run\",\n \"lint\": \"oxlint .\",\n \"prepublishOnly\": \"bun .docu/node/clean.ts && oxlint . && vitest run && node bin/compile-lib.mjs\"\n },\n \"keywords\": [\n \"docubook\",\n \"documentation\",\n \"static-site\",\n \"daisyui\",\n \"bun\",\n \"docs framework\",\n \"react\",\n \"react-dom\"\n ],\n \"homepage\": \"https://github.com/DocuBook/docubook/tree/main/packages/flame\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/DocuBook/docubook.git\",\n \"directory\": \"packages/flame\"\n },\n \"author\": \"wildan.nrs\",\n \"author-url\": \"https://wildan.dev\",\n \"license\": \"MIT\",\n \"dependencies\": {\n \"@docubook/core\": \"workspace:^\",\n \"@docubook/markdown\": \"workspace:^\",\n \"@docubook/themes-colors\": \"workspace:^\",\n \"@docubook/ui-react\": \"workspace:^\",\n \"@mdx-js/react\": \"^3.0.1\",\n \"@tailwindcss/cli\": \"4.3.0\",\n \"@tailwindcss/typography\": \"0.5.16\",\n \"bun-plugin-tailwind\": \"^0.1.2\",\n \"daisyui\": \"^5.5.19\",\n \"lucide-react\": \"^1.14.0\",\n \"react\": \"^19.2.7\",\n \"react-dom\": \"^19.2.7\",\n \"unified\": \"^11.0.0\",\n \"vite\": \"^8.2.1\",\n \"zod\": \"^4.4.3\"\n },\n \"peerDependencies\": {\n \"@sentry/bun\": \"^10.0.0\"\n },\n \"peerDependenciesMeta\": {\n \"@sentry/bun\": {\n \"optional\": true\n }\n },\n \"devDependencies\": {\n \"@types/node\": \"^22.0.0\",\n \"@types/react\": \"^19.0.0\",\n \"@types/react-dom\": \"^19.0.0\",\n \"bun-types\": \"^1.4.0\",\n \"tailwindcss\": \"^4.3.0\",\n \"typescript\": \"^7.0.2\"\n }\n}\n","/**\n * Interactive CLI logger with spinner and tree-style route display.\n * Inspired by Next.js/Turbopack build output.\n *\n * Supports structured logging via environment variables:\n * - LOG_LEVEL: debug | info | warn | error (default: info)\n * - LOG_FORMAT: json | pretty (default: pretty)\n */\n\nimport { loadDocuConfig } from \"./paths\";\nimport pkg from \"../../package.json\" with { type: \"json\" };\n\nconst docuConfig = loadDocuConfig();\nconst isCI = !!(process.env.CI || process.env.NO_COLOR || !process.stdout.isTTY);\nconst LOG_FORMAT = process.env.LOG_FORMAT || \"pretty\";\nconst isJSON = LOG_FORMAT === \"json\";\n\ntype LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\nconst LEVELS: Record<LogLevel, number> = { debug: 0, info: 1, warn: 2, error: 3 };\nconst currentLevel = LEVELS[(process.env.LOG_LEVEL as LogLevel) || \"info\"] ?? LEVELS.info;\n\nfunction shouldLog(level: LogLevel): boolean {\n return LEVELS[level] >= currentLevel;\n}\n\nfunction jsonLog(level: LogLevel, msg: string, meta?: Record<string, unknown>) {\n if (!shouldLog(level)) return;\n const entry = { ts: new Date().toISOString(), ...meta, level, msg };\n const out = level === \"error\" ? console.error : level === \"warn\" ? console.warn : console.log;\n out(JSON.stringify(entry));\n}\n\n/** Returns true if the caller should skip (already handled or filtered). */\nfunction guard(level: LogLevel, msg: string, meta?: Record<string, unknown>): boolean {\n if (isJSON) {\n jsonLog(level, msg, meta);\n return true;\n }\n return !shouldLog(level);\n}\n\nconst c =\n isCI || isJSON\n ? {\n reset: \"\",\n bold: \"\",\n dim: \"\",\n green: \"\",\n cyan: \"\",\n magenta: \"\",\n yellow: \"\",\n red: \"\",\n white: \"\",\n gray: \"\",\n }\n : {\n reset: \"\\x1b[0m\",\n bold: \"\\x1b[1m\",\n dim: \"\\x1b[2m\",\n green: \"\\x1b[32m\",\n cyan: \"\\x1b[36m\",\n magenta: \"\\x1b[35m\",\n yellow: \"\\x1b[33m\",\n red: \"\\x1b[31m\",\n white: \"\\x1b[37m\",\n gray: \"\\x1b[90m\",\n };\n\nconst SPINNER_FRAMES = [\"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\"];\n\nclass Spinner {\n private frame = 0;\n private message = \"\";\n private timer: ReturnType<typeof setInterval> | null = null;\n\n info(finalMsg: string) {\n if (this.timer) clearInterval(this.timer);\n this.timer = null;\n if (isCI) {\n console.log(`ℹ ${finalMsg}`);\n } else {\n process.stdout.write(`\\r\\x1b[K${c.cyan}ℹ${c.reset} ${finalMsg}\\n`);\n process.stdout.write(\"\\x1b[?25h\");\n }\n }\n\n start(msg: string) {\n this.message = msg;\n if (isCI) {\n console.log(`… ${msg}`);\n return;\n }\n this.frame = 0;\n process.stdout.write(\"\\x1b[?25l\");\n this.render();\n this.timer = setInterval(() => this.render(), 80);\n }\n\n stop(finalMsg: string) {\n if (this.timer) clearInterval(this.timer);\n this.timer = null;\n if (isCI) {\n console.log(`✓ ${finalMsg}`);\n return;\n }\n process.stdout.write(`\\r\\x1b[K${c.green}✓${c.reset} ${finalMsg}\\n`);\n process.stdout.write(\"\\x1b[?25h\");\n }\n\n private render() {\n const f = SPINNER_FRAMES[this.frame % SPINNER_FRAMES.length];\n process.stdout.write(`\\r${c.cyan}${f}${c.reset} ${this.message}`);\n this.frame++;\n }\n}\n\ninterface RouteItem {\n title: string;\n href: string;\n noLink?: boolean;\n items?: RouteItem[];\n}\n\nconst MAX_ROUTE_LINES = 5;\nlet routeLineCount = 0;\nlet routeTruncated = false;\n\nfunction printRouteTree(routes: RouteItem[], prefix = \"\", isRoot = true) {\n for (let i = 0; i < routes.length; i++) {\n if (routeTruncated) return;\n if (routeLineCount >= MAX_ROUTE_LINES) {\n routeTruncated = true;\n process.stdout.write(`${prefix}${c.dim}...${c.reset}\\n`);\n return;\n }\n\n const route = routes[i];\n const last = i === routes.length - 1;\n const connector = isRoot ? \"\" : last ? \"└── \" : \"├── \";\n const childPrefix = isRoot ? \"\" : last ? \" \" : \"│ \";\n\n const path = route.noLink\n ? `${c.dim}${route.href}${c.reset}`\n : `${c.white}${route.href}${c.reset}`;\n\n const label = route.noLink ? `${c.gray}(group)${c.reset}` : \"\";\n\n process.stdout.write(`${prefix}${connector}${path} ${label}\\n`);\n routeLineCount++;\n\n if (route.items?.length) {\n printRouteTree(route.items, prefix + childPrefix, false);\n }\n }\n}\n\nexport const logger = {\n spinner: new Spinner(),\n\n buildStart() {\n if (guard(\"info\", \"build_start\", { package: \"@docubook/flame\", version: pkg.version })) return;\n console.log(\n `\\n${c.bold}${c.cyan} 🔥 DocuBook Flame${c.reset} ${c.dim}v${pkg.version}${c.reset}\\n`\n );\n },\n\n bundleStart() {\n if (guard(\"info\", \"bundle_start\")) return;\n this.spinner.start(\"Building client bundle...\");\n },\n\n bundleDone(ms: number) {\n if (guard(\"info\", \"bundle_done\", { duration_ms: ms })) return;\n this.spinner.stop(`Client bundle compiled ${c.dim}(${ms}ms)${c.reset}`);\n },\n\n indexStart() {\n if (guard(\"info\", \"index_start\")) return;\n this.spinner.start(\"Generating search index...\");\n },\n\n indexDone(records: number, ms: number, skipped = false) {\n if (guard(\"info\", \"index_done\", { records, duration_ms: ms, skipped })) return;\n this.spinner.stop(\n skipped\n ? `Search index cached ${c.dim}(${ms}ms)${c.reset}`\n : `Search index generated ${c.dim}(${records} records, ${ms}ms)${c.reset}`\n );\n },\n\n routes() {\n if (guard(\"debug\", \"routes\")) return;\n const routes = docuConfig.routes as RouteItem[] | undefined;\n if (!routes?.length) return;\n\n console.log(`\\n${c.bold} Routes${c.reset} ${c.dim}(${countRoutes(routes)} pages)${c.reset}\\n`);\n routeLineCount = 0;\n routeTruncated = false;\n printRouteTree(routes, \" \");\n },\n\n ready(port: number, hmr = false) {\n if (guard(\"info\", \"server_ready\", { port, hmr })) return;\n console.log(\n `\\n${c.green}${c.bold} ✓ Ready${c.reset} in ${c.bold}http://localhost:${port}/docs/${c.reset}`\n );\n if (hmr) console.log(`${c.dim} HMR enabled — watching docs/ for changes${c.reset}`);\n console.log(\"\");\n },\n\n request(method: string, pathname: string, status: number, duration_ms?: number) {\n if (guard(\"info\", \"http_request\", { method, path: pathname, status, duration_ms })) return;\n const color = status >= 500 ? c.red : status >= 400 ? c.gray : c.green;\n const ts = new Date().toLocaleTimeString();\n const dur = duration_ms != null ? ` ${c.dim}${duration_ms}ms${c.reset}` : \"\";\n console.log(\n `${c.dim} ${ts}${c.reset} ${method} ${pathname} ${color}${status}${c.reset}${dur}`\n );\n },\n\n debug(msg: string, meta?: Record<string, unknown>) {\n if (guard(\"debug\", msg, meta)) return;\n console.log(`${c.gray} [debug] ${msg}${c.reset}`);\n },\n\n warn(msg: string, meta?: Record<string, unknown>) {\n if (guard(\"warn\", msg, meta)) return;\n console.warn(`${c.yellow} ⚠ ${msg}${c.reset}`);\n },\n\n error(msg: string, meta?: Record<string, unknown>) {\n if (guard(\"error\", msg, meta)) return;\n console.error(`${c.red} ✖ ${msg}${c.reset}`);\n },\n};\n\nfunction countRoutes(routes: RouteItem[]): number {\n let count = 0;\n for (const r of routes) {\n if (!r.noLink) count++;\n if (r.items) count += countRoutes(r.items);\n }\n return count;\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { t as denoAdapter } from "./deno-DmEaKggj.js";
2
- import { t as runPreview } from "./preview.impl-CnsbLmDA.js";
2
+ import { t as runPreview } from "./preview.impl-BaYhTRBW.js";
3
3
  //#region .docu/node/preview.deno.ts
4
4
  await runPreview(denoAdapter);
5
5
  //#endregion
@@ -1,6 +1,6 @@
1
1
  import { r as DIST_DIR } from "./paths-Bl2cdp9E.js";
2
- import { c as SECURITY_HEADERS, f as injectNonce, i as getContentType, l as cspHeader, u as generateNonce } from "./utils-DA17MyQG.js";
3
- import { t as logger } from "./logger-CQyNTE6L.js";
2
+ import { a as getContentType, d as cspHeader, h as injectNonce, p as generateNonce, u as SECURITY_HEADERS } from "./utils-B_CoyKie.js";
3
+ import { t as logger } from "./logger-Cz0CPMi8.js";
4
4
  import { existsSync, readFileSync, statSync } from "node:fs";
5
5
  import { resolve } from "node:path";
6
6
  import { readFile } from "node:fs/promises";
@@ -74,4 +74,4 @@ async function runPreview(adapter) {
74
74
  //#endregion
75
75
  export { runPreview as t };
76
76
 
77
- //# sourceMappingURL=preview.impl-CnsbLmDA.js.map
77
+ //# sourceMappingURL=preview.impl-BaYhTRBW.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"preview.impl-CnsbLmDA.js","names":[],"sources":["../node/preview.impl.ts"],"sourcesContent":["/**\n * Runtime-neutral preview server — mirror of `preview.ts` (Bun-only,\n * protected) driven by a `RuntimeAdapter` and `node:fs` file reads.\n */\n\nimport { existsSync, statSync, readFileSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport type { RuntimeAdapter, ServerHandle } from \"./runtime\";\nimport { logger } from \"./logger\";\nimport { DIST_DIR } from \"./paths\";\nimport { getContentType } from \"./utils\";\nimport { SECURITY_HEADERS, generateNonce, cspHeader, injectNonce } from \"./security\";\n\nfunction resolveFile(pathname: string): string | null {\n const path = pathname.slice(1);\n const isWithinDist = (p: string) => p === DIST_DIR || p.startsWith(DIST_DIR + \"/\");\n\n if (!path.includes(\".\")) {\n const withIndex = resolve(DIST_DIR, path, \"index.html\");\n if (isWithinDist(withIndex) && existsSync(withIndex)) return withIndex;\n const withHtml = resolve(DIST_DIR, path + \".html\");\n if (isWithinDist(withHtml) && existsSync(withHtml)) return withHtml;\n }\n\n const exact = resolve(DIST_DIR, path);\n if (!isWithinDist(exact)) return null;\n try {\n if (statSync(exact).isFile()) return exact;\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") throw err;\n }\n return null;\n}\n\nexport async function runPreview(adapter: RuntimeAdapter): Promise<ServerHandle | null> {\n const PORT = parseInt(process.env.PORT || \"4173\", 10);\n\n logger.buildStart();\n\n if (!existsSync(DIST_DIR)) {\n logger.spinner.start(\"Checking build output...\");\n logger.spinner.info(\"dist not found. Run \\x1b[1mflame build\\x1b[0m first.\");\n process.exit(0);\n }\n\n const notFoundPath = resolve(DIST_DIR, \"404.html\");\n\n const handle = await adapter.serve(\n async (req) => {\n const url = new URL(req.url);\n const pathname = decodeURIComponent(url.pathname);\n\n const filePath = resolveFile(pathname);\n\n if (filePath) {\n const contentType = getContentType(filePath);\n if (contentType === \"text/html\") {\n const nonce = generateNonce();\n const html = await readFile(filePath, \"utf-8\");\n const modified = injectNonce(html, nonce);\n return new Response(modified, {\n headers: {\n \"Content-Type\": \"text/html\",\n ...SECURITY_HEADERS,\n \"Content-Security-Policy\": cspHeader(nonce),\n },\n });\n }\n return new Response(readFileSync(filePath), {\n headers: { \"Content-Type\": contentType },\n });\n }\n\n if (existsSync(notFoundPath)) {\n const nonce = generateNonce();\n const html = await readFile(notFoundPath, \"utf-8\");\n const modified = injectNonce(html, nonce);\n return new Response(modified, {\n status: 404,\n headers: {\n \"Content-Type\": \"text/html\",\n ...SECURITY_HEADERS,\n \"Content-Security-Policy\": cspHeader(nonce),\n },\n });\n }\n\n return new Response(\"404 - Not Found\", { status: 404 });\n },\n { port: PORT }\n );\n\n logger.ready(handle.port);\n return handle;\n}\n"],"mappings":";;;;;;;;;;;AAcA,SAAS,YAAY,UAAiC;CACpD,MAAM,OAAO,SAAS,MAAM,CAAC;CAC7B,MAAM,gBAAgB,MAAc,MAAM,YAAY,EAAE,WAAW,WAAW,GAAG;CAEjF,IAAI,CAAC,KAAK,SAAS,GAAG,GAAG;EACvB,MAAM,YAAY,QAAQ,UAAU,MAAM,YAAY;EACtD,IAAI,aAAa,SAAS,KAAK,WAAW,SAAS,GAAG,OAAO;EAC7D,MAAM,WAAW,QAAQ,UAAU,OAAO,OAAO;EACjD,IAAI,aAAa,QAAQ,KAAK,WAAW,QAAQ,GAAG,OAAO;CAC7D;CAEA,MAAM,QAAQ,QAAQ,UAAU,IAAI;CACpC,IAAI,CAAC,aAAa,KAAK,GAAG,OAAO;CACjC,IAAI;EACF,IAAI,SAAS,KAAK,CAAC,CAAC,OAAO,GAAG,OAAO;CACvC,SAAS,KAAK;EACZ,IAAK,IAA8B,SAAS,UAAU,MAAM;CAC9D;CACA,OAAO;AACT;AAEA,eAAsB,WAAW,SAAuD;CACtF,MAAM,OAAO,SAAS,QAAQ,IAAI,QAAQ,QAAQ,EAAE;CAEpD,OAAO,WAAW;CAElB,IAAI,CAAC,WAAW,QAAQ,GAAG;EACzB,OAAO,QAAQ,MAAM,0BAA0B;EAC/C,OAAO,QAAQ,KAAK,sDAAsD;EAC1E,QAAQ,KAAK,CAAC;CAChB;CAEA,MAAM,eAAe,QAAQ,UAAU,UAAU;CAEjD,MAAM,SAAS,MAAM,QAAQ,MAC3B,OAAO,QAAQ;EACb,MAAM,MAAM,IAAI,IAAI,IAAI,GAAG;EAG3B,MAAM,WAAW,YAFA,mBAAmB,IAAI,QAEX,CAAQ;EAErC,IAAI,UAAU;GACZ,MAAM,cAAc,eAAe,QAAQ;GAC3C,IAAI,gBAAgB,aAAa;IAC/B,MAAM,QAAQ,cAAc;IAC5B,MAAM,OAAO,MAAM,SAAS,UAAU,OAAO;IAC7C,MAAM,WAAW,YAAY,MAAM,KAAK;IACxC,OAAO,IAAI,SAAS,UAAU,EAC5B,SAAS;KACP,gBAAgB;KAChB,GAAG;KACH,2BAA2B,UAAU,KAAK;IAC5C,EACF,CAAC;GACH;GACA,OAAO,IAAI,SAAS,aAAa,QAAQ,GAAG,EAC1C,SAAS,EAAE,gBAAgB,YAAY,EACzC,CAAC;EACH;EAEA,IAAI,WAAW,YAAY,GAAG;GAC5B,MAAM,QAAQ,cAAc;GAC5B,MAAM,OAAO,MAAM,SAAS,cAAc,OAAO;GACjD,MAAM,WAAW,YAAY,MAAM,KAAK;GACxC,OAAO,IAAI,SAAS,UAAU;IAC5B,QAAQ;IACR,SAAS;KACP,gBAAgB;KAChB,GAAG;KACH,2BAA2B,UAAU,KAAK;IAC5C;GACF,CAAC;EACH;EAEA,OAAO,IAAI,SAAS,mBAAmB,EAAE,QAAQ,IAAI,CAAC;CACxD,GACA,EAAE,MAAM,KAAK,CACf;CAEA,OAAO,MAAM,OAAO,IAAI;CACxB,OAAO;AACT"}
1
+ {"version":3,"file":"preview.impl-BaYhTRBW.js","names":[],"sources":["../node/preview.impl.ts"],"sourcesContent":["/**\n * Runtime-neutral preview server — mirror of `preview.ts` (Bun-only,\n * protected) driven by a `RuntimeAdapter` and `node:fs` file reads.\n */\n\nimport { existsSync, statSync, readFileSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport type { RuntimeAdapter, ServerHandle } from \"./runtime\";\nimport { logger } from \"./logger\";\nimport { DIST_DIR } from \"./paths\";\nimport { getContentType } from \"./utils\";\nimport { SECURITY_HEADERS, generateNonce, cspHeader, injectNonce } from \"./security\";\n\nfunction resolveFile(pathname: string): string | null {\n const path = pathname.slice(1);\n const isWithinDist = (p: string) => p === DIST_DIR || p.startsWith(DIST_DIR + \"/\");\n\n if (!path.includes(\".\")) {\n const withIndex = resolve(DIST_DIR, path, \"index.html\");\n if (isWithinDist(withIndex) && existsSync(withIndex)) return withIndex;\n const withHtml = resolve(DIST_DIR, path + \".html\");\n if (isWithinDist(withHtml) && existsSync(withHtml)) return withHtml;\n }\n\n const exact = resolve(DIST_DIR, path);\n if (!isWithinDist(exact)) return null;\n try {\n if (statSync(exact).isFile()) return exact;\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") throw err;\n }\n return null;\n}\n\nexport async function runPreview(adapter: RuntimeAdapter): Promise<ServerHandle | null> {\n const PORT = parseInt(process.env.PORT || \"4173\", 10);\n\n logger.buildStart();\n\n if (!existsSync(DIST_DIR)) {\n logger.spinner.start(\"Checking build output...\");\n logger.spinner.info(\"dist not found. Run \\x1b[1mflame build\\x1b[0m first.\");\n process.exit(0);\n }\n\n const notFoundPath = resolve(DIST_DIR, \"404.html\");\n\n const handle = await adapter.serve(\n async (req) => {\n const url = new URL(req.url);\n const pathname = decodeURIComponent(url.pathname);\n\n const filePath = resolveFile(pathname);\n\n if (filePath) {\n const contentType = getContentType(filePath);\n if (contentType === \"text/html\") {\n const nonce = generateNonce();\n const html = await readFile(filePath, \"utf-8\");\n const modified = injectNonce(html, nonce);\n return new Response(modified, {\n headers: {\n \"Content-Type\": \"text/html\",\n ...SECURITY_HEADERS,\n \"Content-Security-Policy\": cspHeader(nonce),\n },\n });\n }\n return new Response(readFileSync(filePath), {\n headers: { \"Content-Type\": contentType },\n });\n }\n\n if (existsSync(notFoundPath)) {\n const nonce = generateNonce();\n const html = await readFile(notFoundPath, \"utf-8\");\n const modified = injectNonce(html, nonce);\n return new Response(modified, {\n status: 404,\n headers: {\n \"Content-Type\": \"text/html\",\n ...SECURITY_HEADERS,\n \"Content-Security-Policy\": cspHeader(nonce),\n },\n });\n }\n\n return new Response(\"404 - Not Found\", { status: 404 });\n },\n { port: PORT }\n );\n\n logger.ready(handle.port);\n return handle;\n}\n"],"mappings":";;;;;;;;;;;AAcA,SAAS,YAAY,UAAiC;CACpD,MAAM,OAAO,SAAS,MAAM,CAAC;CAC7B,MAAM,gBAAgB,MAAc,MAAM,YAAY,EAAE,WAAW,WAAW,GAAG;CAEjF,IAAI,CAAC,KAAK,SAAS,GAAG,GAAG;EACvB,MAAM,YAAY,QAAQ,UAAU,MAAM,YAAY;EACtD,IAAI,aAAa,SAAS,KAAK,WAAW,SAAS,GAAG,OAAO;EAC7D,MAAM,WAAW,QAAQ,UAAU,OAAO,OAAO;EACjD,IAAI,aAAa,QAAQ,KAAK,WAAW,QAAQ,GAAG,OAAO;CAC7D;CAEA,MAAM,QAAQ,QAAQ,UAAU,IAAI;CACpC,IAAI,CAAC,aAAa,KAAK,GAAG,OAAO;CACjC,IAAI;EACF,IAAI,SAAS,KAAK,CAAC,CAAC,OAAO,GAAG,OAAO;CACvC,SAAS,KAAK;EACZ,IAAK,IAA8B,SAAS,UAAU,MAAM;CAC9D;CACA,OAAO;AACT;AAEA,eAAsB,WAAW,SAAuD;CACtF,MAAM,OAAO,SAAS,QAAQ,IAAI,QAAQ,QAAQ,EAAE;CAEpD,OAAO,WAAW;CAElB,IAAI,CAAC,WAAW,QAAQ,GAAG;EACzB,OAAO,QAAQ,MAAM,0BAA0B;EAC/C,OAAO,QAAQ,KAAK,sDAAsD;EAC1E,QAAQ,KAAK,CAAC;CAChB;CAEA,MAAM,eAAe,QAAQ,UAAU,UAAU;CAEjD,MAAM,SAAS,MAAM,QAAQ,MAC3B,OAAO,QAAQ;EACb,MAAM,MAAM,IAAI,IAAI,IAAI,GAAG;EAG3B,MAAM,WAAW,YAFA,mBAAmB,IAAI,QAEX,CAAQ;EAErC,IAAI,UAAU;GACZ,MAAM,cAAc,eAAe,QAAQ;GAC3C,IAAI,gBAAgB,aAAa;IAC/B,MAAM,QAAQ,cAAc;IAC5B,MAAM,OAAO,MAAM,SAAS,UAAU,OAAO;IAC7C,MAAM,WAAW,YAAY,MAAM,KAAK;IACxC,OAAO,IAAI,SAAS,UAAU,EAC5B,SAAS;KACP,gBAAgB;KAChB,GAAG;KACH,2BAA2B,UAAU,KAAK;IAC5C,EACF,CAAC;GACH;GACA,OAAO,IAAI,SAAS,aAAa,QAAQ,GAAG,EAC1C,SAAS,EAAE,gBAAgB,YAAY,EACzC,CAAC;EACH;EAEA,IAAI,WAAW,YAAY,GAAG;GAC5B,MAAM,QAAQ,cAAc;GAC5B,MAAM,OAAO,MAAM,SAAS,cAAc,OAAO;GACjD,MAAM,WAAW,YAAY,MAAM,KAAK;GACxC,OAAO,IAAI,SAAS,UAAU;IAC5B,QAAQ;IACR,SAAS;KACP,gBAAgB;KAChB,GAAG;KACH,2BAA2B,UAAU,KAAK;IAC5C;GACF,CAAC;EACH;EAEA,OAAO,IAAI,SAAS,mBAAmB,EAAE,QAAQ,IAAI,CAAC;CACxD,GACA,EAAE,MAAM,KAAK,CACf;CAEA,OAAO,MAAM,OAAO,IAAI;CACxB,OAAO;AACT"}
@@ -1,5 +1,5 @@
1
1
  import { t as nodeAdapter } from "./node-DPTySdwG.js";
2
- import { t as runPreview } from "./preview.impl-CnsbLmDA.js";
2
+ import { t as runPreview } from "./preview.impl-BaYhTRBW.js";
3
3
  //#region .docu/node/preview.node.ts
4
4
  await runPreview(nodeAdapter);
5
5
  //#endregion
@@ -1,5 +1,5 @@
1
1
  import { t as denoAdapter } from "./deno-DmEaKggj.js";
2
- import { t as runServer } from "./server.impl-CXTzYqbF.js";
2
+ import { t as runServer } from "./server.impl-l8hXfV_0.js";
3
3
  //#region .docu/node/server.deno.ts
4
4
  await runServer(denoAdapter);
5
5
  //#endregion
@@ -1,7 +1,7 @@
1
1
  import { a as DOCS_DIR, c as PROJECT_ROOT, d as loadDocuConfig, r as DIST_DIR } from "./paths-Bl2cdp9E.js";
2
- import { D as loadPlugins, E as BuildPluginBuilder, a as IndexPage, b as buildClientBundle, c as captureException, f as compileMdx, i as DocsLayout, l as initSentry, m as frontmatterField, n as hmrScript, o as NotFoundPage, r as htmlShell, s as DocsPage, t as errorHtml, u as generateSearchIndex, x as computeInlineThemeCss } from "./html.shared-FwgbE1WG.js";
3
- import { c as SECURITY_HEADERS, d as htmlResponse, g as wrapPluginResponse, i as getContentType, m as isSlugSafe, p as isPathSafe, s as stripDocsHtmlSuffix, u as generateNonce } from "./utils-DA17MyQG.js";
4
- import { t as logger } from "./logger-CQyNTE6L.js";
2
+ import { D as loadPlugins, E as BuildPluginBuilder, a as IndexPage, b as buildClientBundle, c as captureException, f as compileMdx, i as DocsLayout, l as initSentry, m as frontmatterField, n as hmrScript, o as NotFoundPage, r as htmlShell, s as DocsPage, t as errorHtml, u as generateSearchIndex, x as computeInlineThemeCss } from "./html.shared-WwQnhDty.js";
3
+ import { _ as isSlugSafe, a as getContentType, g as isPathSafe, l as stripDocsHtmlSuffix, m as htmlResponse, p as generateNonce, u as SECURITY_HEADERS, y as wrapPluginResponse } from "./utils-B_CoyKie.js";
4
+ import { t as logger } from "./logger-Cz0CPMi8.js";
5
5
  import { createRequire } from "node:module";
6
6
  import { existsSync, readFileSync, statSync, watch } from "node:fs";
7
7
  import { dirname, join, resolve } from "node:path";
@@ -11,7 +11,7 @@ import { renderToString } from "react-dom/server";
11
11
  //#region .docu/node/server-routes.ts
12
12
  function createHtmlResponse(title, description, body, status, state, depth = 0) {
13
13
  const nonce = generateNonce();
14
- const favicon = state.docuConfig.meta?.favicon || "/favicon.ico";
14
+ const favicon = state.docuConfig.meta?.favicon || "/docs/assets/images/favicon.ico";
15
15
  const html = htmlShell({
16
16
  title,
17
17
  description,
@@ -114,7 +114,7 @@ async function renderDocsServerPage(doc, slug, pathname, state) {
114
114
  const headExtra = state.builder.collectHead(ctx);
115
115
  const bodyExtra = state.builder.collectBody(ctx);
116
116
  const nonce = generateNonce();
117
- const favicon = state.docuConfig.meta?.favicon || "/favicon.ico";
117
+ const favicon = state.docuConfig.meta?.favicon || "/docs/assets/images/favicon.ico";
118
118
  let html = htmlShell({
119
119
  title,
120
120
  description,
@@ -355,4 +355,4 @@ async function runServer(adapter) {
355
355
  //#endregion
356
356
  export { runServer as t };
357
357
 
358
- //# sourceMappingURL=server.impl-CXTzYqbF.js.map
358
+ //# sourceMappingURL=server.impl-l8hXfV_0.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.impl-l8hXfV_0.js","names":[],"sources":["../node/server-routes.ts","../node/server.impl.ts"],"sourcesContent":["import { readFile, stat } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport { readFileSync, statSync } from \"node:fs\";\nimport React, { type ReactNode } from \"react\";\nimport { renderToString } from \"react-dom/server\";\nimport { compileMdx, frontmatterField } from \"./mdx\";\nimport { DEFAULT_FAVICON, getContentType } from \"./utils\";\nimport { DOCS_DIR, DIST_DIR, PROJECT_ROOT } from \"./paths\";\nimport { BuildPluginBuilder } from \"./plugin-builder\";\nimport type { PageContext } from \"./plugin\";\nimport type { DocuConfig, TocItem } from \"./types\";\nimport DocsPage from \"../pages/docs/[[...slug]]\";\nimport NotFoundPage from \"../pages/404\";\nimport IndexPage from \"../pages/index\";\nimport { DocsLayout } from \"../components/DocsLayout\";\nimport { generateNonce, isPathSafe, isSlugSafe, htmlResponse, SECURITY_HEADERS } from \"./security\";\nimport { htmlShell as createHtmlShell, hmrScript, errorHtml } from \"./html.shared\";\n\nexport interface ServerState {\n docuConfig: DocuConfig;\n assetManifest: { css: string; js: string };\n inlineThemeCss?: string;\n builder: BuildPluginBuilder | null;\n}\n\nfunction createHtmlResponse(\n title: string,\n description: string,\n body: string,\n status: number,\n state: ServerState,\n depth = 0\n): Response {\n const nonce = generateNonce();\n const favicon = state.docuConfig.meta?.favicon || DEFAULT_FAVICON;\n const html = createHtmlShell({\n title,\n description,\n body,\n favicon,\n css: state.assetManifest.css,\n js: state.assetManifest.js,\n nonce,\n extraScripts: hmrScript(nonce),\n themeCss: state.inlineThemeCss,\n depth,\n // 404 pages can be requested at arbitrary depths (e.g. a noLink section\n // path typed in the address bar) — relative asset paths would resolve\n // against the wrong directory and break CSS/JS.\n absoluteAssets: status === 404,\n });\n /** Dev-only: serves compiledSource → MDXRemote eval path (no CSP in production). */\n return htmlResponse(html, nonce, status, true);\n}\n\n// Dev-only memo: compileMdx per request is ~70ms; repeat navigations with\n// an unchanged file (same mtime) reuse the compiled result. The dev server\n// has no other per-page cache — without this every navigation re-parses MDX.\n// ponytail: unbounded Map is fine — docs sites have a handful of pages; cap\n// with an LRU only if a project exceeds thousands of routes.\nconst docsCache = new Map<\n string,\n { mtimeMs: number; doc: NonNullable<Awaited<ReturnType<typeof getDocsForSlug>>> }\n>();\n\nasync function getDocsForSlug(\n slug: string,\n state: ServerState\n): Promise<{\n content: ReactNode;\n compiledSource: string;\n frontmatter: Record<string, unknown>;\n tocs: TocItem[];\n filePath: string;\n resolvedContent: string;\n} | null> {\n if (!isSlugSafe(slug, DOCS_DIR)) return null;\n\n const paths = [\n resolve(DOCS_DIR, slug, \"index.mdx\"),\n resolve(DOCS_DIR, `${slug}.mdx`),\n resolve(DOCS_DIR, slug, \"index.md\"),\n resolve(DOCS_DIR, `${slug}.md`),\n ];\n\n const resolvedDocsDir = resolve(DOCS_DIR);\n let filePath: string | null = null;\n let raw: string | null = null;\n for (const p of paths) {\n const resolvedCandidate = resolve(p);\n if (\n resolvedCandidate !== resolvedDocsDir &&\n !resolvedCandidate.startsWith(resolvedDocsDir + \"/\")\n ) {\n continue;\n }\n try {\n raw = await readFile(resolvedCandidate, \"utf-8\");\n filePath = resolvedCandidate;\n break;\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") throw err;\n }\n }\n if (!filePath || !raw) return null;\n\n const relPath = filePath.replace(PROJECT_ROOT + \"/\", \"\");\n\n const mtimeMs = (await stat(filePath)).mtimeMs;\n const cached = docsCache.get(relPath);\n if (cached && cached.mtimeMs === mtimeMs) {\n return cached.doc;\n }\n\n let content = raw;\n if (state.builder) {\n const transformed = await state.builder.runOnLoad(relPath, content);\n if (transformed?.contents) {\n content = transformed.contents;\n }\n }\n\n const remarkPlugins = state.builder?.collectRemarkPlugins();\n const rehypePlugins = state.builder?.collectRehypePlugins();\n const result = await compileMdx(content, relPath, undefined, remarkPlugins, rehypePlugins);\n\n let frontmatter = result.frontmatter as Record<string, unknown>;\n if (state.builder) {\n frontmatter = await state.builder.runTransformFrontmatterChain(frontmatter, {\n slug: slug || \"/\",\n filePath: relPath,\n content,\n });\n }\n\n const doc = {\n content: result.content,\n compiledSource: result.compiledSource,\n frontmatter,\n tocs: result.tocs,\n filePath: relPath,\n resolvedContent: content,\n };\n docsCache.set(relPath, { mtimeMs, doc });\n return doc;\n}\n\nasync function renderDocsServerPage(\n doc: NonNullable<Awaited<ReturnType<typeof getDocsForSlug>>>,\n slug: string[],\n pathname: string,\n state: ServerState\n): Promise<Response> {\n const title = frontmatterField(doc.frontmatter, \"title\") || slug.join(\"/\") || \"Docs\";\n const description = frontmatterField(doc.frontmatter, \"description\");\n\n const page = React.createElement(\n DocsLayout,\n { repoUrl: state.docuConfig.repo?.url, pathname },\n React.createElement(DocsPage, {\n slug,\n title,\n description,\n date: frontmatterField(doc.frontmatter, \"date\") || undefined,\n // Same root-relative SSR as build — see build.ts.\n content: renderToString(doc.content),\n tocs: doc.tocs,\n filePath: doc.filePath,\n repoUrl: state.docuConfig.repo?.url,\n compiledSource: doc.compiledSource,\n })\n );\n\n const body = renderToString(page);\n\n // Match build.ts depth calculation: slug.split(\"/\").length, fallback to 1 for empty\n const depth = slug.length || 1;\n\n if (state.builder) {\n const ctx: PageContext = {\n slug: slug.join(\"/\") || \"/\",\n filePath: doc.filePath,\n frontmatter: doc.frontmatter,\n content: doc.resolvedContent,\n config: state.docuConfig,\n };\n const headExtra = state.builder.collectHead(ctx);\n const bodyExtra = state.builder.collectBody(ctx);\n const nonce = generateNonce();\n const favicon = state.docuConfig.meta?.favicon || DEFAULT_FAVICON;\n let html = createHtmlShell({\n title,\n description,\n body,\n favicon,\n css: state.assetManifest.css,\n js: state.assetManifest.js,\n nonce,\n extraScripts: hmrScript(nonce),\n themeCss: state.inlineThemeCss,\n headExtra,\n bodyExtra,\n depth,\n });\n html = await state.builder.runTransformHtmlChain(html, ctx);\n /** Dev-only: serves compiledSource → MDXRemote eval path (no CSP in production). */\n return htmlResponse(html, nonce, 200, true);\n }\n\n return createHtmlResponse(title, description, body, 200, state, depth);\n}\n\nfunction renderPage(\n Component: React.ComponentType<Record<string, unknown>>,\n title: string,\n description: string,\n status: number,\n state: ServerState,\n props: Record<string, unknown> = {},\n depth = 0\n): Response {\n const page = React.createElement(\n DocsLayout,\n { repoUrl: state.docuConfig.repo?.url, pathname: \"/docs\" },\n React.createElement(Component, props)\n );\n const body = renderToString(page);\n return createHtmlResponse(title, description, body, status, state, depth);\n}\n\nexport async function handleDocsIndex(state: ServerState): Promise<Response> {\n const doc = await getDocsForSlug(\"\", state);\n if (!doc) return renderPage(NotFoundPage, \"404 - Not Found\", \"\", 404, state, {}, 1);\n return renderDocsServerPage(doc, [], \"/docs\", state);\n}\n\nexport async function handleDocsRoute(slug: string[], state: ServerState): Promise<Response> {\n const path = slug.join(\"/\");\n const doc = await getDocsForSlug(path, state);\n if (!doc)\n return renderPage(NotFoundPage, \"404 - Not Found\", \"\", 404, state, {}, slug.length || 1);\n return renderDocsServerPage(doc, slug, `/docs/${path}`, state);\n}\n\nexport function handleIndex(state: ServerState): Response {\n const page = React.createElement(IndexPage);\n const body = renderToString(page);\n return createHtmlResponse(\n state.docuConfig.meta?.title || \"DocuBook\",\n state.docuConfig.meta?.description || \"\",\n body,\n 200,\n state\n );\n}\n\nexport function handleNotFound(state: ServerState, depth = 0): Response {\n return renderPage(NotFoundPage, \"404 - Not Found\", \"\", 404, state, {}, depth);\n}\n\nexport function serveStatic(pathname: string): Response | null {\n let decoded: string;\n try {\n decoded = decodeURIComponent(pathname);\n } catch {\n return null;\n }\n if (!isPathSafe(pathname, DIST_DIR)) return null;\n const assetPath = resolve(DIST_DIR, decoded.slice(1));\n try {\n const s = statSync(assetPath);\n if (s.isFile()) {\n return new Response(readFileSync(assetPath), {\n headers: { \"Content-Type\": getContentType(pathname) },\n });\n }\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") throw err;\n }\n\n if (decoded.startsWith(\"/docs/assets/\")) {\n const docsAssetsDir = resolve(DOCS_DIR, \"assets\");\n const requestedRelative = decoded.slice(\"/docs/assets/\".length);\n const docsAsset = resolve(docsAssetsDir, requestedRelative);\n const docsAssetsDirWithSep = docsAssetsDir.endsWith(\"/\") ? docsAssetsDir : docsAssetsDir + \"/\";\n if (docsAsset !== docsAssetsDir && !docsAsset.startsWith(docsAssetsDirWithSep)) return null;\n try {\n const s = statSync(docsAsset);\n if (s.isFile()) {\n return new Response(readFileSync(docsAsset), {\n headers: { \"Content-Type\": getContentType(pathname) },\n });\n }\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") throw err;\n }\n }\n return null;\n}\n\nexport function serverErrorResponse(error: unknown): Response {\n const msg = error instanceof Error ? error.message : \"Unknown error\";\n const st =\n process.env.NODE_ENV !== \"production\" && error instanceof Error ? error.stack : undefined;\n return new Response(errorHtml(msg, st), {\n status: 500,\n headers: {\n \"Content-Type\": \"text/html\",\n ...SECURITY_HEADERS,\n \"Content-Security-Policy\": \"default-src 'none'; style-src 'unsafe-inline'\",\n },\n });\n}\n","/**\n * Runtime-neutral dev server — mirror of `server.ts` (Bun-only, protected)\n * driven by a `RuntimeAdapter` (./runtime) instead of `Bun.serve`,\n * with manual route matching instead of `Bun.FileSystemRouter`. The page set\n * is static (`/`, `/docs/[[...slug]]`, `/404`), so a router is unnecessary.\n */\n\nimport { watch, existsSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { createRequire } from \"node:module\";\nimport type { RuntimeAdapter, ServerHandle } from \"./runtime\";\nimport { DOCS_DIR, loadDocuConfig } from \"./paths\";\nimport { loadPlugins } from \"./plugin-loader\";\nimport { BuildPluginBuilder } from \"./plugin-builder\";\nimport { buildClientBundle, computeInlineThemeCss } from \"./hydrate.node\";\nimport { generateSearchIndex } from \"./search-indexer\";\nimport { logger } from \"./logger\";\nimport { initSentry, captureException } from \"./sentry\";\nimport {\n serveStatic,\n handleDocsIndex,\n handleDocsRoute,\n handleIndex,\n handleNotFound,\n serverErrorResponse,\n type ServerState,\n} from \"./server-routes\";\nimport { wrapPluginResponse } from \"./security\";\nimport { stripDocsHtmlSuffix } from \"./utils\";\n\nexport async function runServer(adapter: RuntimeAdapter): Promise<ServerHandle> {\n const docuConfig = loadDocuConfig();\n\n const parsedPort = parseInt(process.env.PORT ?? \"3000\", 10);\n const PORT =\n Number.isInteger(parsedPort) && parsedPort > 0 && parsedPort <= 65535 ? parsedPort : 3000;\n\n logger.buildStart();\n\n await initSentry();\n\n logger.bundleStart();\n let t = performance.now();\n const assetManifest = await buildClientBundle();\n logger.bundleDone(Math.round(performance.now() - t));\n\n const inlineThemeCss = computeInlineThemeCss();\n\n logger.indexStart();\n t = performance.now();\n const records = await generateSearchIndex();\n logger.indexDone(records, Math.round(performance.now() - t));\n\n logger.routes();\n\n // Plugin setup — all hooks active (onLoad, remark/rehype, frontmatter, head/body, html transform, handleRequest)\n const pluginsConfig = docuConfig.plugins ?? [];\n const builder = pluginsConfig.length > 0 ? new BuildPluginBuilder(docuConfig) : null;\n\n if (builder) {\n const plugins = await loadPlugins(pluginsConfig);\n\n for (const plugin of plugins) {\n await plugin.setup(builder);\n }\n }\n\n const state: ServerState = {\n docuConfig,\n assetManifest,\n inlineThemeCss,\n builder,\n };\n\n const hmrClients = new Set<ReadableStreamDefaultController>();\n\n const require = createRequire(import.meta.url);\n /** Locate the installed @docubook/markdown source dir (workspace link), if any. */\n function resolveMdxContentSrc(): string | null {\n try {\n const pkgDir = dirname(require.resolve(\"@docubook/markdown/package.json\"));\n const src = join(pkgDir, \"src\");\n return existsSync(src) ? src : null;\n } catch {\n return null;\n }\n }\n\n let hmrTimeout: ReturnType<typeof setTimeout> | null = null;\n const watcher = watch(DOCS_DIR, { recursive: true }, (_event, filename) => {\n if (!filename || (!filename.endsWith(\".mdx\") && !filename.endsWith(\".md\"))) return;\n if (hmrTimeout) clearTimeout(hmrTimeout);\n hmrTimeout = setTimeout(() => {\n for (const client of [...hmrClients]) {\n try {\n client.enqueue(new TextEncoder().encode(\"data: reload\\n\\n\"));\n } catch {\n hmrClients.delete(client);\n }\n }\n }, 300);\n });\n\n // Rebuild the client bundle when component sources change (e.g. the\n // @docubook/markdown workspace package) so interactive islands pick up\n // edits, then reload connected clients. MDX edits are handled by the watcher\n // above — no rebuild needed, content recompiles per request.\n let srcHmrTimeout: ReturnType<typeof setTimeout> | null = null;\n const mdxContentSrc = resolveMdxContentSrc();\n const srcWatcher = mdxContentSrc\n ? watch(mdxContentSrc, { recursive: true }, (_event, filename) => {\n if (!filename || !/\\.(ts|tsx|js|jsx|css)$/.test(filename)) return;\n if (srcHmrTimeout) clearTimeout(srcHmrTimeout);\n srcHmrTimeout = setTimeout(async () => {\n try {\n state.assetManifest = await buildClientBundle();\n logger.warn(\"[hmr] client bundle rebuilt (component change)\");\n } catch (e) {\n logger.warn(\n `[hmr] bundle rebuild failed: ${e instanceof Error ? e.message : String(e)}`\n );\n }\n for (const client of [...hmrClients]) {\n try {\n client.enqueue(new TextEncoder().encode(\"data: reload\\n\\n\"));\n } catch {\n hmrClients.delete(client);\n }\n }\n }, 300);\n })\n : null;\n\n process.on(\"SIGINT\", () => {\n watcher.close();\n srcWatcher?.close();\n process.exit(0);\n });\n process.on(\"SIGTERM\", () => {\n watcher.close();\n srcWatcher?.close();\n process.exit(0);\n });\n\n let handle: ServerHandle | null = null;\n\n const fetchHandler = async (req: Request): Promise<Response> => {\n const url = new URL(req.url);\n // Generated links carry `.html` (matching the static build output);\n // route them to the same handler as their extensionless form.\n const pathname = stripDocsHtmlSuffix(url.pathname);\n const startTime = performance.now();\n\n if (builder) {\n const pluginResponse = await builder.runHandleRequest(req, {\n port: handle?.port ?? PORT,\n hostname: handle?.hostname ?? \"localhost\",\n });\n if (pluginResponse) {\n const securedResponse = wrapPluginResponse(pluginResponse, true);\n logger.request(\n req.method,\n pathname,\n securedResponse.status,\n Math.round(performance.now() - startTime)\n );\n return securedResponse;\n }\n }\n\n try {\n if (pathname === \"/__hmr\") {\n const stream = new ReadableStream({\n start(controller) {\n hmrClients.add(controller);\n controller.enqueue(new TextEncoder().encode(\"data: connected\\n\\n\"));\n },\n cancel(controller) {\n hmrClients.delete(controller);\n },\n });\n return new Response(stream, {\n headers: {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n },\n });\n }\n\n if (pathname.startsWith(\"/assets/\") || /\\.\\w+$/.test(pathname)) {\n const staticRes = serveStatic(pathname);\n if (staticRes) return staticRes;\n }\n\n let response: Response;\n\n // Manual route matching — same routes Bun.FileSystemRouter derives\n // from `.docu/pages/` (\"/\", \"/docs/[[...slug]]\", \"/404\").\n if (pathname === \"/\") {\n response = handleIndex(state);\n } else if (pathname === \"/docs\" || pathname === \"/docs/\") {\n response = await handleDocsIndex(state);\n } else if (pathname.startsWith(\"/docs/\")) {\n const slug = pathname.slice(\"/docs/\".length).split(\"/\").filter(Boolean);\n response =\n slug.length === 0 ? await handleDocsIndex(state) : await handleDocsRoute(slug, state);\n } else {\n response = handleNotFound(state);\n }\n\n logger.request(\n req.method,\n pathname,\n response.status,\n Math.round(performance.now() - startTime)\n );\n return response;\n } catch (err) {\n captureException(err, { method: req.method, pathname });\n logger.request(req.method, pathname, 500, Math.round(performance.now() - startTime));\n return serverErrorResponse(err);\n }\n };\n\n handle = await adapter.serve(fetchHandler, { port: PORT, idleTimeout: 255 });\n\n logger.ready(handle.port, true);\n return handle;\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { t as nodeAdapter } from "./node-DPTySdwG.js";
2
- import { t as runServer } from "./server.impl-CXTzYqbF.js";
2
+ import { t as runServer } from "./server.impl-l8hXfV_0.js";
3
3
  //#region .docu/node/server.node.ts
4
4
  await runServer(nodeAdapter);
5
5
  //#endregion
@@ -1,4 +1,4 @@
1
- import { realpathSync } from "node:fs";
1
+ import { existsSync, realpathSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
3
  import { readdir, stat } from "node:fs/promises";
4
4
  import { randomBytes } from "node:crypto";
@@ -26,6 +26,14 @@ function cspHeader(nonce, allowEval = false) {
26
26
  "frame-ancestors 'none'"
27
27
  ].join("; ");
28
28
  }
29
+ /**
30
+ * Strip `frame-ancestors` for `<meta http-equiv="Content-Security-Policy">`.
31
+ * Browsers ignore `frame-ancestors` in meta CSP (header-only directive)
32
+ * and log a console warning. HTTP header from `cspHeader()` keeps it.
33
+ */
34
+ function cspMeta(csp) {
35
+ return csp.replace(/;\s*frame-ancestors 'none'/, "");
36
+ }
29
37
  function isPathSafe(pathname, baseDir) {
30
38
  const resolved = resolve(baseDir, decodeURIComponent(pathname).slice(1));
31
39
  const baseDirSlash = baseDir.endsWith("/") ? baseDir : baseDir + "/";
@@ -68,7 +76,7 @@ function injectNonce(html, nonce) {
68
76
  return html.replace(/<script\b(?![^>]*\bsrc\s*=)([^>]*)>/gi, (match) => {
69
77
  if (/nonce\s*=/i.test(match)) return match.replace(/nonce="[^"]*"/i, `nonce="${nonce}"`);
70
78
  return match.replace(/>$/, ` nonce="${nonce}">`);
71
- });
79
+ }).replace(/<meta\b[^>]*Content-Security-Policy[^>]*>/gi, (tag) => tag.replace(/'nonce-[^']*'|&#(?:x27;|39;)nonce-[^&]*(?:&#(?:x27;|39;))/i, (match) => match.startsWith("&#") ? `&#x27;nonce-${nonce}&#x27;` : `'nonce-${nonce}'`));
72
80
  }
73
81
  /**
74
82
  * Wrap a plugin response with security headers.
@@ -105,7 +113,8 @@ function htmlResponse(html, nonce, status = 200, allowEval = false) {
105
113
  //#region .docu/node/utils.ts
106
114
  /**
107
115
  * Scan a directory recursively for MDX/MD files.
108
- * Skips "assets" directories, hidden directories (dot-prefixed), and root-level index.mdx.
116
+ * Skips "assets" directories, hidden directories (dot-prefixed), and root-level
117
+ * index.mdx/index.md (the docs root renders separately in build).
109
118
  * Shared between build.ts and search-indexer.ts.
110
119
  */
111
120
  async function scanMdxFiles(dir, baseDir = "") {
@@ -118,7 +127,7 @@ async function scanMdxFiles(dir, baseDir = "") {
118
127
  if (entry.name === "assets" || entry.name.startsWith(".")) continue;
119
128
  files.push(...await scanMdxFiles(fullPath, relativePath));
120
129
  } else if (entry.name.endsWith(".mdx") || entry.name.endsWith(".md")) {
121
- if (entry.name === "index.mdx" && !baseDir) continue;
130
+ if (!baseDir && (entry.name === "index.mdx" || entry.name === "index.md")) continue;
122
131
  const stats = await stat(fullPath);
123
132
  let path = relativePath.replace(/\.(mdx|md)$/, "");
124
133
  if (/\/index$/.test(path)) path = path.replace(/\/index$/, "");
@@ -134,6 +143,19 @@ async function scanMdxFiles(dir, baseDir = "") {
134
143
  function isExternalUrl(url) {
135
144
  return /^(https?:\/\/|\/\/)/.test(url);
136
145
  }
146
+ /**
147
+ * Resolve the docs root index source — docs/index.mdx preferred, docs/index.md
148
+ * as fallback (mirrors the dev server's getDocsForSlug extension handling).
149
+ */
150
+ function resolveDocsIndexSource(docsDir) {
151
+ for (const ext of [".mdx", ".md"]) {
152
+ const candidate = join(docsDir, `index${ext}`);
153
+ if (existsSync(candidate)) return candidate;
154
+ }
155
+ }
156
+ /** Default favicon — resolves in both build output (`docs/assets/` is copied
157
+ * to `dist/docs/assets/`) and dev (served from `docs/assets/` via fallback). */
158
+ var DEFAULT_FAVICON = "/docs/assets/images/favicon.ico";
137
159
  /** Suffix an internal docs link with `.html` to match the flat static build output. */
138
160
  function docsHtmlHref(path) {
139
161
  return `${path}.html`;
@@ -162,6 +184,6 @@ function getContentType(pathname) {
162
184
  return MIME_TYPES[pathname.split(".").pop()?.toLowerCase() || ""] || "application/octet-stream";
163
185
  }
164
186
  //#endregion
165
- export { isExternalUrl as a, SECURITY_HEADERS as c, htmlResponse as d, injectNonce as f, wrapPluginResponse as g, normalizeImporterPath as h, getContentType as i, cspHeader as l, isSlugSafe as m, docsHtmlHref as n, scanMdxFiles as o, isPathSafe as p, formatDate2 as r, stripDocsHtmlSuffix as s, cn as t, generateNonce as u };
187
+ export { isSlugSafe as _, getContentType as a, scanMdxFiles as c, cspHeader as d, cspMeta as f, isPathSafe as g, injectNonce as h, formatDate2 as i, stripDocsHtmlSuffix as l, htmlResponse as m, cn as n, isExternalUrl as o, generateNonce as p, docsHtmlHref as r, resolveDocsIndexSource as s, DEFAULT_FAVICON as t, SECURITY_HEADERS as u, normalizeImporterPath as v, wrapPluginResponse as y };
166
188
 
167
- //# sourceMappingURL=utils-DA17MyQG.js.map
189
+ //# sourceMappingURL=utils-B_CoyKie.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils-B_CoyKie.js","names":[],"sources":["../node/security.ts","../node/utils.ts"],"sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport { resolve } from \"node:path\";\nimport { realpathSync } from \"node:fs\";\n\nexport const SECURITY_HEADERS: Record<string, string> = {\n \"Strict-Transport-Security\": \"max-age=63072000; includeSubDomains; preload\",\n \"X-Frame-Options\": \"DENY\",\n \"X-Content-Type-Options\": \"nosniff\",\n \"Referrer-Policy\": \"strict-origin-when-cross-origin\",\n \"Permissions-Policy\": \"camera=(), microphone=(), geolocation=()\",\n};\n\nexport function generateNonce(): string {\n return randomBytes(16).toString(\"base64\");\n}\n\nexport function cspHeader(nonce: string, allowEval = false): string {\n const scriptSrc = allowEval\n ? `script-src 'self' 'nonce-${nonce}' 'unsafe-eval'`\n : `script-src 'self' 'nonce-${nonce}'`;\n return [\n \"default-src 'self'\",\n scriptSrc,\n \"style-src 'self' 'unsafe-inline'\",\n \"img-src 'self' https: data:\",\n \"font-src 'self' data:\",\n \"connect-src 'self' https:\",\n \"frame-src https://www.youtube-nocookie.com\",\n \"frame-ancestors 'none'\",\n ].join(\"; \");\n}\n\n/**\n * Strip `frame-ancestors` for `<meta http-equiv=\"Content-Security-Policy\">`.\n * Browsers ignore `frame-ancestors` in meta CSP (header-only directive)\n * and log a console warning. HTTP header from `cspHeader()` keeps it.\n */\nexport function cspMeta(csp: string): string {\n return csp.replace(/;\\s*frame-ancestors 'none'/, \"\");\n}\n\nexport function isPathSafe(pathname: string, baseDir: string): boolean {\n const decoded = decodeURIComponent(pathname);\n const resolved = resolve(baseDir, decoded.slice(1));\n const baseDirSlash = baseDir.endsWith(\"/\") ? baseDir : baseDir + \"/\";\n if (!(resolved === baseDir || resolved.startsWith(baseDirSlash))) {\n return false;\n }\n\n if (resolved === baseDir) return true;\n try {\n const real = realpathSync(resolved);\n const realBase = realpathSync(baseDir);\n const realBaseDirSlash = realBase.endsWith(\"/\") ? realBase : realBase + \"/\";\n return real.startsWith(realBaseDirSlash);\n } catch (err) {\n const nodeErr = err as NodeJS.ErrnoException;\n if (nodeErr.code === \"ENOENT\") {\n return true;\n }\n console.error(\n `[security] isPathSafe error for pathname=\"${pathname}\": ${nodeErr.message} (code=${nodeErr.code})`\n );\n return false;\n }\n}\n\nexport function isSlugSafe(slug: string, docsDir: string): boolean {\n const resolved = resolve(docsDir, slug);\n const docsDirSlash = docsDir.endsWith(\"/\") ? docsDir : docsDir + \"/\";\n if (!(resolved === docsDir || resolved.startsWith(docsDirSlash))) {\n return false;\n }\n\n if (resolved === docsDir) return true;\n try {\n const real = realpathSync(resolved);\n const realDocsDir = realpathSync(docsDir);\n const realDocsDirSlash = realDocsDir.endsWith(\"/\") ? realDocsDir : realDocsDir + \"/\";\n return real.startsWith(realDocsDirSlash);\n } catch (err) {\n const nodeErr = err as NodeJS.ErrnoException;\n if (nodeErr.code === \"ENOENT\") {\n return true;\n }\n console.error(\n `[security] isSlugSafe error for slug=\"${slug}\": ${nodeErr.message} (code=${nodeErr.code})`\n );\n return false;\n }\n}\n\n/** Normalize an esbuild importer path to a canonical forward-slash absolute form. */\nexport function normalizeImporterPath(importer: string): string {\n return resolve(importer).replace(/\\\\/g, \"/\");\n}\n\nexport function injectNonce(html: string, nonce: string): string {\n const scripts = html.replace(/<script\\b(?![^>]*\\bsrc\\s*=)([^>]*)>/gi, (match) => {\n if (/nonce\\s*=/i.test(match)) {\n return match.replace(/nonce=\"[^\"]*\"/i, `nonce=\"${nonce}\"`);\n }\n return match.replace(/>$/, ` nonce=\"${nonce}\">`);\n });\n // Keep the <meta> CSP nonce in sync: browsers intersect the meta policy\n // with the response-header policy, so a stale build-time nonce would block\n // the very scripts re-tagged above.\n return scripts.replace(/<meta\\b[^>]*Content-Security-Policy[^>]*>/gi, (tag) =>\n tag.replace(/'nonce-[^']*'|&#(?:x27;|39;)nonce-[^&]*(?:&#(?:x27;|39;))/i, (match) =>\n match.startsWith(\"&#\") ? `&#x27;nonce-${nonce}&#x27;` : `'nonce-${nonce}'`\n )\n );\n}\n\nexport interface PluginResponseLike {\n status: number;\n statusText?: string;\n headers: Headers;\n body: BodyInit | null;\n}\n\n/**\n * Wrap a plugin response with security headers.\n * - Fills in SECURITY_HEADERS defaults where plugin hasn't set a value\n * - Adds Content-Security-Policy for HTML responses (with optional unsafe-eval)\n * - Preserves plugin body, status, statusText unchanged\n */\nexport function wrapPluginResponse(\n pluginResponse: PluginResponseLike,\n allowEval = false\n): Response {\n const securedHeaders = new Headers(pluginResponse.headers);\n for (const [key, value] of Object.entries(SECURITY_HEADERS)) {\n if (!securedHeaders.has(key)) {\n securedHeaders.set(key, value);\n }\n }\n let body = pluginResponse.body;\n const contentType = securedHeaders.get(\"Content-Type\") || \"\";\n if (contentType.includes(\"text/html\") && !securedHeaders.has(\"Content-Security-Policy\")) {\n const nonce = generateNonce();\n securedHeaders.set(\"Content-Security-Policy\", cspHeader(nonce, allowEval));\n if (typeof body === \"string\") {\n body = injectNonce(body, nonce);\n }\n }\n return new Response(body, {\n status: pluginResponse.status,\n statusText: pluginResponse.statusText,\n headers: securedHeaders,\n });\n}\n\nexport function htmlResponse(\n html: string,\n nonce: string,\n status = 200,\n allowEval = false\n): Response {\n return new Response(html, {\n status,\n headers: {\n \"Content-Type\": \"text/html\",\n ...SECURITY_HEADERS,\n \"Content-Security-Policy\": cspHeader(nonce, allowEval),\n },\n });\n}\n","export { cn, parseDate, formatDate, formatDate2 } from \"@docubook/core\";\n\nimport { existsSync } from \"node:fs\";\nimport { readdir, stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nexport interface ScannedMdxFile {\n path: string;\n absPath: string;\n mtime: number;\n}\n\n/**\n * Scan a directory recursively for MDX/MD files.\n * Skips \"assets\" directories, hidden directories (dot-prefixed), and root-level\n * index.mdx/index.md (the docs root renders separately in build).\n * Shared between build.ts and search-indexer.ts.\n */\nexport async function scanMdxFiles(dir: string, baseDir = \"\"): Promise<ScannedMdxFile[]> {\n const files: ScannedMdxFile[] = [];\n const entries = await readdir(dir, { withFileTypes: true });\n\n for (const entry of entries) {\n const fullPath = join(dir, entry.name);\n const relativePath = baseDir ? `${baseDir}/${entry.name}` : entry.name;\n\n if (entry.isDirectory()) {\n if (entry.name === \"assets\" || entry.name.startsWith(\".\")) continue;\n files.push(...(await scanMdxFiles(fullPath, relativePath)));\n } else if (entry.name.endsWith(\".mdx\") || entry.name.endsWith(\".md\")) {\n // Root index renders separately (either extension) — scanning it would\n // produce a duplicate \"index\" page colliding with dist/docs/index.html.\n if (!baseDir && (entry.name === \"index.mdx\" || entry.name === \"index.md\")) continue;\n const stats = await stat(fullPath);\n let path = relativePath.replace(/\\.(mdx|md)$/, \"\");\n\n if (/\\/index$/.test(path)) {\n path = path.replace(/\\/index$/, \"\");\n }\n files.push({ path, absPath: fullPath, mtime: stats.mtimeMs });\n }\n }\n\n return files;\n}\n\nexport function isExternalUrl(url: string): boolean {\n return /^(https?:\\/\\/|\\/\\/)/.test(url);\n}\n\n/**\n * Resolve the docs root index source — docs/index.mdx preferred, docs/index.md\n * as fallback (mirrors the dev server's getDocsForSlug extension handling).\n */\nexport function resolveDocsIndexSource(docsDir: string): string | undefined {\n for (const ext of [\".mdx\", \".md\"]) {\n const candidate = join(docsDir, `index${ext}`);\n if (existsSync(candidate)) return candidate;\n }\n return undefined;\n}\n\n/** Default favicon — resolves in both build output (`docs/assets/` is copied\n * to `dist/docs/assets/`) and dev (served from `docs/assets/` via fallback). */\nexport const DEFAULT_FAVICON = \"/docs/assets/images/favicon.ico\";\n\n/** Suffix an internal docs link with `.html` to match the flat static build output. */\nexport function docsHtmlHref(path: string): string {\n return `${path}.html`;\n}\n\n/** Map a `/docs{.html,/*.html}` request back to its extensionless route (dev server).\n * Handles both `/docs/*.html` (pages) and `/docs.html` (edge case). */\nexport function stripDocsHtmlSuffix(pathname: string): string {\n if (pathname === \"/docs.html\") return \"/docs\";\n if (pathname.startsWith(\"/docs/\") && pathname.endsWith(\".html\"))\n return pathname.slice(0, -\".html\".length);\n return pathname;\n}\n\nexport function getPath(url: string): string {\n try {\n return new URL(url).pathname;\n } catch (err) {\n console.error(\"Failed to parse URL\", url, err);\n return url;\n }\n}\n\n/** Get git last modified date for a file */\nexport async function getGitLastModified(filePath: string): Promise<string | null> {\n try {\n const cleanPath = filePath.replace(/^\\//, \"\");\n if (\n !cleanPath ||\n !/^[a-zA-Z0-9\\-_/.\\s]+$/.test(cleanPath) ||\n /(^|\\/)\\.\\.($|\\/)/.test(cleanPath)\n )\n return null;\n const proc = Bun.spawn([\"git\", \"log\", \"-1\", \"--format=%cI\", \"--\", cleanPath], {\n stderr: \"ignore\",\n });\n const text = await new Response(proc.stdout).text();\n const date = text.trim();\n return date || null;\n } catch (err) {\n console.error(\"Failed to get git last modified for\", filePath, err);\n return null;\n }\n}\n\n/** Batch git last modified dates for multiple files in a single spawn */\nexport async function getGitLastModifiedBatch(filePaths: string[]): Promise<Map<string, string>> {\n const result = new Map<string, string>();\n if (filePaths.length === 0) return result;\n\n // Filter and validate paths — same guard as getGitLastModified\n const safePaths: string[] = [];\n for (const fp of filePaths) {\n const cleanPath = fp.replace(/^\\//, \"\");\n if (\n !cleanPath ||\n !/^[a-zA-Z0-9\\-_/.\\s]+$/.test(cleanPath) ||\n /(^|\\/)\\.\\.($|\\/)/.test(cleanPath)\n ) {\n console.warn(`[utils] getGitLastModifiedBatch: skipping invalid path \"${fp}\"`);\n continue;\n }\n safePaths.push(cleanPath);\n }\n\n if (safePaths.length === 0) return result;\n\n try {\n const proc = Bun.spawn(\n [\"git\", \"log\", \"--format=%cI\", \"--name-only\", \"--diff-filter=ACMR\", ...safePaths],\n { stderr: \"ignore\" }\n );\n const text = await new Response(proc.stdout).text();\n let currentDate = \"\";\n\n for (const line of text.split(\"\\n\")) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n if (/^\\d{4}-\\d{2}-\\d{2}T/.test(trimmed)) {\n currentDate = trimmed;\n } else if (currentDate && !result.has(trimmed)) {\n result.set(trimmed, currentDate);\n }\n }\n } catch (err) {\n console.error(\"Failed to get git last modified batch for\", filePaths, err);\n }\n\n return result;\n}\n\nconst MIME_TYPES: Record<string, string> = {\n html: \"text/html\",\n css: \"text/css\",\n js: \"application/javascript\",\n json: \"application/json\",\n png: \"image/png\",\n jpg: \"image/jpeg\",\n jpeg: \"image/jpeg\",\n svg: \"image/svg+xml\",\n ico: \"image/x-icon\",\n woff: \"font/woff\",\n woff2: \"font/woff2\",\n};\n\nexport function getContentType(pathname: string): string {\n const ext = pathname.split(\".\").pop()?.toLowerCase();\n return MIME_TYPES[ext || \"\"] || \"application/octet-stream\";\n}\n"],"mappings":"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"}
@@ -38,7 +38,7 @@ import { logger } from "./logger";
38
38
  import { initSentry, captureException } from "./sentry";
39
39
  import { loadPlugins } from "./plugin-loader";
40
40
  import { BuildPluginBuilder } from "./plugin-builder";
41
- import { scanMdxFiles } from "./utils";
41
+ import { scanMdxFiles, resolveDocsIndexSource, DEFAULT_FAVICON } from "./utils";
42
42
  import type { BuildCache, BuildCacheMeta, CliArgs } from "./types";
43
43
  import { isCacheEntry } from "./types";
44
44
  import {
@@ -110,9 +110,11 @@ type RebuildDecision = "yes" | "hash_check" | "no";
110
110
  function shouldRebuild(path: string, mtime: number, cache: BuildCache): RebuildDecision {
111
111
  const cached = cache[path];
112
112
  if (!isCacheEntry(cached)) return "yes";
113
- if (mtime > cached.builtAt + 2000) return "hash_check";
114
- if (Math.abs(mtime - cached.builtAt) <= 2000 && mtime !== cached.mtime) return "hash_check";
115
- if (mtime !== cached.mtime && mtime > cached.mtime) return "hash_check";
113
+ // Any mtime drift vs the recorded one — a newer edit, or mtime moving
114
+ // backwards (rsync -a, cp -p, snapshot restore) — falls through to the
115
+ // hash check: hashing an unchanged file is cheap, serving a stale page
116
+ // is not. Only an untouched mtime skips without reading the file.
117
+ if (mtime !== cached.mtime) return "hash_check";
116
118
  return "no";
117
119
  }
118
120
 
@@ -226,7 +228,7 @@ async function renderDocsPage(
226
228
  const bodyExtra = builder?.collectBody(ctx);
227
229
 
228
230
  const depth = slug ? slug.split("/").length : 1;
229
- const favicon = docuConfig.meta?.favicon || "/docs/assets/images/favicon.ico";
231
+ const favicon = docuConfig.meta?.favicon || DEFAULT_FAVICON;
230
232
  const seo = buildSeoMeta(docuConfig, frontmatter, slug || "");
231
233
  // MDX content hydrates from the bundled ESM module (mdx-hydrate), not
232
234
  // new Function — no 'unsafe-eval' needed in the CSP.
@@ -339,15 +341,16 @@ export async function runBuild(): Promise<void> {
339
341
  });
340
342
  await Promise.all(prePassTasks);
341
343
 
342
- // The docs root (index.mdx) renders with slug "" — mirror that key so the
343
- // index page hydrates too. Its render has its own try/catch; skip on error.
344
- const indexMdxPath = join(DOCS_DIR, "index.mdx");
345
- if (existsSync(indexMdxPath)) {
344
+ // The docs root (index.mdx, or index.md) renders with slug "" — mirror that
345
+ // key so the index page hydrates too. Its render has its own try/catch; skip
346
+ // on error.
347
+ const indexSource = resolveDocsIndexSource(DOCS_DIR);
348
+ if (indexSource) {
346
349
  try {
347
- const indexRaw = await readFile(indexMdxPath, "utf-8");
350
+ const indexRaw = await readFile(indexSource, "utf-8");
348
351
  let indexContent = indexRaw;
349
352
  if (builder) {
350
- const relPath = indexMdxPath.replace(PROJECT_ROOT + "/", "");
353
+ const relPath = indexSource.replace(PROJECT_ROOT + "/", "");
351
354
  const transformed = await builder.runOnLoad(relPath, indexContent);
352
355
  if (transformed?.contents) indexContent = transformed.contents;
353
356
  }
@@ -393,9 +396,8 @@ export async function runBuild(): Promise<void> {
393
396
 
394
397
  const allRelPaths = mdxFiles.map((f) => f.absPath.replace(PROJECT_ROOT + "/", ""));
395
398
 
396
- const indexMdxFull = join(DOCS_DIR, "index.mdx");
397
- if (existsSync(indexMdxFull)) {
398
- allRelPaths.push(indexMdxFull.replace(PROJECT_ROOT + "/", ""));
399
+ if (indexSource) {
400
+ allRelPaths.push(indexSource.replace(PROJECT_ROOT + "/", ""));
399
401
  }
400
402
  const gitDates = await getGitLastModifiedBatch(allRelPaths);
401
403
 
@@ -476,29 +478,34 @@ export async function runBuild(): Promise<void> {
476
478
  await Promise.all(buildTasks.slice(i, i + CONCURRENCY).map((fn) => fn()));
477
479
  }
478
480
 
479
- try {
480
- const indexMdxPath = join(DOCS_DIR, "index.mdx");
481
- const indexRaw = await readFile(indexMdxPath, "utf-8");
482
- const indexRelPath = indexMdxPath.replace(PROJECT_ROOT + "/", "");
483
- const indexHtml = await renderDocsPage(
484
- docuConfig,
485
- "",
486
- indexRaw,
487
- indexRelPath,
488
- gitDates,
489
- builder,
490
- generateNonce()
491
- );
492
- await mkdir(join(DIST_DIR, "docs"), { recursive: true });
493
- await writeFile(join(DIST_DIR, "docs", "index.html"), indexHtml);
494
- } catch (err) {
495
- const msg = err instanceof Error ? err.message : String(err);
496
- errors.push(`index.mdx: ${msg}`);
497
- console.error(`\n\u274C Failed to build index: ${msg}\n`);
481
+ if (indexSource) {
482
+ try {
483
+ const indexRaw = await readFile(indexSource, "utf-8");
484
+ const indexRelPath = indexSource.replace(PROJECT_ROOT + "/", "");
485
+ const indexHtml = await renderDocsPage(
486
+ docuConfig,
487
+ "",
488
+ indexRaw,
489
+ indexRelPath,
490
+ gitDates,
491
+ builder,
492
+ generateNonce()
493
+ );
494
+ await mkdir(join(DIST_DIR, "docs"), { recursive: true });
495
+ await writeFile(join(DIST_DIR, "docs", "index.html"), indexHtml);
496
+ } catch (err) {
497
+ const msg = err instanceof Error ? err.message : String(err);
498
+ errors.push(`index: ${msg}`);
499
+ console.error(`\n❌ Failed to build index: ${msg}\n`);
500
+ }
501
+ } else {
502
+ const msg = "docs root index: docs/index.mdx (or docs/index.md) not found";
503
+ errors.push(`index: ${msg}`);
504
+ console.error(`\n❌ Failed to build index: ${msg}\n`);
498
505
  }
499
506
 
500
507
  const landingPage = React.createElement(IndexPage);
501
- const landingFavicon = docuConfig.meta?.favicon || "/docs/assets/images/favicon.ico";
508
+ const landingFavicon = docuConfig.meta?.favicon || DEFAULT_FAVICON;
502
509
  const landingSeo = buildSeoMeta(
503
510
  docuConfig,
504
511
  docuConfig.meta as unknown as Record<string, unknown>,
@@ -524,7 +531,7 @@ export async function runBuild(): Promise<void> {
524
531
  { repoUrl: docuConfig.repo?.url },
525
532
  React.createElement(NotFoundPage)
526
533
  );
527
- const notFoundFavicon = docuConfig.meta?.favicon || "/docs/assets/images/favicon.ico";
534
+ const notFoundFavicon = docuConfig.meta?.favicon || DEFAULT_FAVICON;
528
535
  const notFoundNonce = generateNonce();
529
536
  const notFoundHtml = htmlShell({
530
537
  title: "404 - Not Found",