@docubook/flame 1.7.1 → 2.0.0-alpha.0

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 (57) hide show
  1. package/.docu/components/Context.tsx +1 -1
  2. package/.docu/components/Search.tsx +6 -6
  3. package/.docu/components/Sidebar.tsx +37 -8
  4. package/.docu/components/Theme.tsx +2 -2
  5. package/.docu/components/Toc.tsx +51 -5
  6. package/.docu/components/Typography.tsx +1 -1
  7. package/.docu/components/home/Hero.tsx +4 -4
  8. package/.docu/components/registry.ts +1 -1
  9. package/.docu/lib/build.deno.js +3 -3
  10. package/.docu/lib/{build.impl-TUVHCOIP.js → build.impl-WR24HDUT.js} +3 -3
  11. package/.docu/lib/build.node.js +3 -3
  12. package/.docu/lib/{chunk-ICNXW4Z4.js → chunk-2EYRWRDP.js} +66 -26
  13. package/.docu/lib/{chunk-PFXUCD2U.js → chunk-AJV2GEDF.js} +5 -4
  14. package/.docu/lib/{chunk-XMIEMHTQ.js → chunk-KEWRVASF.js} +3 -3
  15. package/.docu/lib/{chunk-B6LGUADD.js → chunk-QPF4DPZQ.js} +294 -177
  16. package/.docu/lib/chunk-UISOJ4RW.js +114 -0
  17. package/.docu/lib/{chunk-QPO4DS5F.js → chunk-VPV7KP7K.js} +56 -9
  18. package/.docu/lib/{chunk-QL6TQV3S.js → chunk-X7KPVCDN.js} +6 -6
  19. package/.docu/lib/clean.js +1 -1
  20. package/.docu/lib/deploy.deno.js +1 -1
  21. package/.docu/lib/deploy.node.js +1 -1
  22. package/.docu/lib/preview.deno.js +5 -3
  23. package/.docu/lib/preview.node.js +5 -3
  24. package/.docu/lib/server.deno.js +6 -4
  25. package/.docu/lib/server.node.js +6 -4
  26. package/.docu/node/build.impl.ts +72 -26
  27. package/.docu/node/build.ts +67 -17
  28. package/.docu/node/client.ts +64 -27
  29. package/.docu/node/deploy.shared.ts +5 -5
  30. package/.docu/node/deploy.ts +1 -1
  31. package/.docu/node/hydrate.node.ts +69 -4
  32. package/.docu/node/hydrate.ts +50 -1
  33. package/.docu/node/mdx-manifest.d.ts +5 -0
  34. package/.docu/node/mdx.ts +103 -17
  35. package/.docu/node/preview.deno.ts +1 -1
  36. package/.docu/node/preview.impl.ts +3 -3
  37. package/.docu/node/preview.node.ts +1 -1
  38. package/.docu/node/preview.ts +2 -2
  39. package/.docu/node/runtime/bun.ts +28 -0
  40. package/.docu/node/runtime/deno.ts +34 -0
  41. package/.docu/node/runtime/index.ts +4 -0
  42. package/.docu/node/runtime/node.ts +107 -0
  43. package/.docu/node/runtime/types.ts +19 -0
  44. package/.docu/node/search-indexer.ts +3 -2
  45. package/.docu/node/seo.ts +2 -2
  46. package/.docu/node/server-routes.ts +28 -13
  47. package/.docu/node/server.deno.ts +1 -1
  48. package/.docu/node/server.impl.ts +49 -3
  49. package/.docu/node/server.node.ts +1 -1
  50. package/.docu/node/server.ts +45 -1
  51. package/.docu/pages/docs/[[...slug]].tsx +13 -4
  52. package/.docu/pages/index.tsx +1 -1
  53. package/.docu/styles/globals.css +29 -20
  54. package/README.md +7 -48
  55. package/bin/cli.js +25 -7
  56. package/package.json +7 -6
  57. package/template/README.md +7 -48
@@ -0,0 +1,114 @@
1
+ // .docu/node/runtime/deno.ts
2
+ var denoAdapter = {
3
+ name: "deno",
4
+ serve(fetch, options) {
5
+ const server = Deno.serve(
6
+ {
7
+ port: options.port,
8
+ hostname: options.hostname,
9
+ onListen: () => {
10
+ }
11
+ },
12
+ fetch
13
+ );
14
+ return {
15
+ port: server.addr.port,
16
+ hostname: server.addr.hostname,
17
+ stop: () => server.shutdown()
18
+ };
19
+ }
20
+ };
21
+
22
+ // .docu/node/runtime/node.ts
23
+ import { createServer } from "node:http";
24
+ function toWebRequest(req, port, hostname) {
25
+ const host = req.headers.host ?? `${hostname}:${port}`;
26
+ const url = `http://${host}${req.url ?? "/"}`;
27
+ const headers = new Headers();
28
+ for (const [key, value] of Object.entries(req.headers)) {
29
+ if (value === void 0) continue;
30
+ if (Array.isArray(value)) {
31
+ for (const v of value) headers.append(key, v);
32
+ } else {
33
+ headers.set(key, value);
34
+ }
35
+ }
36
+ const method = req.method ?? "GET";
37
+ const hasBody = method !== "GET" && method !== "HEAD";
38
+ return new Request(url, {
39
+ method,
40
+ headers,
41
+ // IncomingMessage is an async iterable of Buffer chunks; Request accepts
42
+ // an async iterable body when half-duplex is declared.
43
+ body: hasBody ? req : void 0,
44
+ // @ts-expect-error -- required by undici for streaming request bodies
45
+ duplex: hasBody ? "half" : void 0
46
+ });
47
+ }
48
+ async function writeResponse(response, res) {
49
+ const headers = {};
50
+ const setCookie = response.headers.getSetCookie?.() ?? [];
51
+ response.headers.forEach((value, key) => {
52
+ if (key === "set-cookie") return;
53
+ headers[key] = value;
54
+ });
55
+ if (setCookie.length > 0) headers["set-cookie"] = setCookie;
56
+ res.writeHead(response.status, headers);
57
+ if (!response.body) {
58
+ res.end();
59
+ return;
60
+ }
61
+ const reader = response.body.getReader();
62
+ try {
63
+ for (; ; ) {
64
+ const { done, value } = await reader.read();
65
+ if (done) break;
66
+ const ok = res.write(value);
67
+ if (!ok) await new Promise((resolve) => res.once("drain", resolve));
68
+ }
69
+ res.end();
70
+ } catch {
71
+ await reader.cancel().catch(() => {
72
+ });
73
+ res.destroy();
74
+ }
75
+ }
76
+ var nodeAdapter = {
77
+ name: "node",
78
+ serve(fetch, options) {
79
+ const hostname = options.hostname ?? "localhost";
80
+ const server = createServer((req, res) => {
81
+ Promise.resolve().then(() => fetch(toWebRequest(req, options.port, hostname))).then((response) => writeResponse(response, res)).catch((err) => {
82
+ console.error(err);
83
+ if (!res.headersSent) {
84
+ res.writeHead(500, { "Content-Type": "text/plain" });
85
+ }
86
+ res.end("Internal Server Error");
87
+ });
88
+ });
89
+ if (options.idleTimeout !== void 0) {
90
+ server.timeout = options.idleTimeout * 1e3;
91
+ server.keepAliveTimeout = options.idleTimeout * 1e3;
92
+ }
93
+ return new Promise((resolve, reject) => {
94
+ server.once("error", reject);
95
+ server.listen(options.port, () => {
96
+ const address = server.address();
97
+ const port = typeof address === "object" && address ? address.port : options.port;
98
+ resolve({
99
+ port,
100
+ hostname,
101
+ stop: () => new Promise((res2, rej2) => {
102
+ server.closeAllConnections?.();
103
+ server.close((err) => err ? rej2(err) : res2());
104
+ })
105
+ });
106
+ });
107
+ });
108
+ }
109
+ };
110
+
111
+ export {
112
+ denoAdapter,
113
+ nodeAdapter
114
+ };
@@ -9,12 +9,13 @@ import {
9
9
  compileMdx,
10
10
  computeInlineThemeCss,
11
11
  errorHtml,
12
+ frontmatterField,
12
13
  generateSearchIndex,
13
14
  hmrScript,
14
15
  htmlShell,
15
16
  initSentry,
16
17
  loadPlugins
17
- } from "./chunk-B6LGUADD.js";
18
+ } from "./chunk-QPF4DPZQ.js";
18
19
  import {
19
20
  SECURITY_HEADERS,
20
21
  generateNonce,
@@ -27,7 +28,7 @@ import {
27
28
  } from "./chunk-EOK6KATZ.js";
28
29
  import {
29
30
  logger
30
- } from "./chunk-PFXUCD2U.js";
31
+ } from "./chunk-AJV2GEDF.js";
31
32
  import {
32
33
  DIST_DIR,
33
34
  DOCS_DIR,
@@ -36,10 +37,12 @@ import {
36
37
  } from "./chunk-4IQXHHPF.js";
37
38
 
38
39
  // .docu/node/server.impl.ts
39
- import { watch } from "node:fs";
40
+ import { watch, existsSync } from "node:fs";
41
+ import { dirname, join } from "node:path";
42
+ import { createRequire } from "node:module";
40
43
 
41
44
  // .docu/node/server-routes.ts
42
- import { readFile } from "node:fs/promises";
45
+ import { readFile, stat } from "node:fs/promises";
43
46
  import { resolve } from "node:path";
44
47
  import { readFileSync, statSync } from "node:fs";
45
48
  import React from "react";
@@ -61,6 +64,7 @@ function createHtmlResponse(title, description, body, status, state, depth = 0)
61
64
  });
62
65
  return htmlResponse(html, nonce, status, true);
63
66
  }
67
+ var docsCache = /* @__PURE__ */ new Map();
64
68
  async function getDocsForSlug(slug, state) {
65
69
  if (!isSlugSafe(slug, DOCS_DIR)) return null;
66
70
  const paths = [
@@ -87,6 +91,11 @@ async function getDocsForSlug(slug, state) {
87
91
  }
88
92
  if (!filePath || !raw) return null;
89
93
  const relPath = filePath.replace(PROJECT_ROOT + "/", "");
94
+ const mtimeMs = (await stat(filePath)).mtimeMs;
95
+ const cached = docsCache.get(relPath);
96
+ if (cached && cached.mtimeMs === mtimeMs) {
97
+ return cached.doc;
98
+ }
90
99
  let content = raw;
91
100
  if (state.builder) {
92
101
  const transformed = await state.builder.runOnLoad(relPath, content);
@@ -105,7 +114,7 @@ async function getDocsForSlug(slug, state) {
105
114
  content
106
115
  });
107
116
  }
108
- return {
117
+ const doc = {
109
118
  content: result.content,
110
119
  compiledSource: result.compiledSource,
111
120
  frontmatter,
@@ -113,10 +122,12 @@ async function getDocsForSlug(slug, state) {
113
122
  filePath: relPath,
114
123
  resolvedContent: content
115
124
  };
125
+ docsCache.set(relPath, { mtimeMs, doc });
126
+ return doc;
116
127
  }
117
128
  async function renderDocsServerPage(doc, slug, pathname, state) {
118
- const title = (typeof doc.frontmatter.title === "string" ? doc.frontmatter.title : "") || slug.join("/") || "Docs";
119
- const description = typeof doc.frontmatter.description === "string" ? doc.frontmatter.description : "";
129
+ const title = frontmatterField(doc.frontmatter, "title") || slug.join("/") || "Docs";
130
+ const description = frontmatterField(doc.frontmatter, "description");
120
131
  const page = React.createElement(
121
132
  DocsLayout,
122
133
  { repoUrl: state.docuConfig.repo?.url, pathname },
@@ -124,8 +135,9 @@ async function renderDocsServerPage(doc, slug, pathname, state) {
124
135
  slug,
125
136
  title,
126
137
  description,
127
- date: doc.frontmatter.date,
128
- content: doc.content,
138
+ date: frontmatterField(doc.frontmatter, "date") || void 0,
139
+ // Same root-relative SSR as build — see build.ts.
140
+ content: renderToString(doc.content),
129
141
  tocs: doc.tocs,
130
142
  filePath: doc.filePath,
131
143
  repoUrl: state.docuConfig.repo?.url,
@@ -283,6 +295,16 @@ async function runServer(adapter) {
283
295
  builder
284
296
  };
285
297
  const hmrClients = /* @__PURE__ */ new Set();
298
+ const require2 = createRequire(import.meta.url);
299
+ function resolveMdxContentSrc() {
300
+ try {
301
+ const pkgDir = dirname(require2.resolve("@docubook/markdown/package.json"));
302
+ const src = join(pkgDir, "src");
303
+ return existsSync(src) ? src : null;
304
+ } catch {
305
+ return null;
306
+ }
307
+ }
286
308
  let hmrTimeout = null;
287
309
  const watcher = watch(DOCS_DIR, { recursive: true }, (_event, filename) => {
288
310
  if (!filename || !filename.endsWith(".mdx") && !filename.endsWith(".md")) return;
@@ -297,12 +319,37 @@ async function runServer(adapter) {
297
319
  }
298
320
  }, 300);
299
321
  });
322
+ let srcHmrTimeout = null;
323
+ const mdxContentSrc = resolveMdxContentSrc();
324
+ const srcWatcher = mdxContentSrc ? watch(mdxContentSrc, { recursive: true }, (_event, filename) => {
325
+ if (!filename || !/\.(ts|tsx|js|jsx|css)$/.test(filename)) return;
326
+ if (srcHmrTimeout) clearTimeout(srcHmrTimeout);
327
+ srcHmrTimeout = setTimeout(async () => {
328
+ try {
329
+ state.assetManifest = await buildClientBundle();
330
+ logger.warn("[hmr] client bundle rebuilt (component change)");
331
+ } catch (e) {
332
+ logger.warn(
333
+ `[hmr] bundle rebuild failed: ${e instanceof Error ? e.message : String(e)}`
334
+ );
335
+ }
336
+ for (const client of [...hmrClients]) {
337
+ try {
338
+ client.enqueue(new TextEncoder().encode("data: reload\n\n"));
339
+ } catch {
340
+ hmrClients.delete(client);
341
+ }
342
+ }
343
+ }, 300);
344
+ }) : null;
300
345
  process.on("SIGINT", () => {
301
346
  watcher.close();
347
+ srcWatcher?.close();
302
348
  process.exit(0);
303
349
  });
304
350
  process.on("SIGTERM", () => {
305
351
  watcher.close();
352
+ srcWatcher?.close();
306
353
  process.exit(0);
307
354
  });
308
355
  let handle = null;
@@ -28,7 +28,7 @@ var NGINX_CONF = `server {
28
28
  add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
29
29
  add_header Referrer-Policy "strict-origin-when-cross-origin" always;
30
30
  add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
31
- add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; font-src 'self' data:; connect-src 'self' https:; frame-src https://www.youtube-nocookie.com; frame-ancestors 'none'" always;
31
+ add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; font-src 'self' data:; connect-src 'self' https:; frame-src https://www.youtube-nocookie.com; frame-ancestors 'none'" always;
32
32
 
33
33
  location /assets/ {
34
34
  expires 1y;
@@ -38,7 +38,7 @@ var NGINX_CONF = `server {
38
38
  add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
39
39
  add_header Referrer-Policy "strict-origin-when-cross-origin" always;
40
40
  add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
41
- add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; font-src 'self' data:; connect-src 'self' https:; frame-src https://www.youtube-nocookie.com; frame-ancestors 'none'" always;
41
+ add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; font-src 'self' data:; connect-src 'self' https:; frame-src https://www.youtube-nocookie.com; frame-ancestors 'none'" always;
42
42
  }
43
43
 
44
44
  location /docs/assets/ {
@@ -49,7 +49,7 @@ var NGINX_CONF = `server {
49
49
  add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
50
50
  add_header Referrer-Policy "strict-origin-when-cross-origin" always;
51
51
  add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
52
- add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; font-src 'self' data:; connect-src 'self' https:; frame-src https://www.youtube-nocookie.com; frame-ancestors 'none'" always;
52
+ add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; font-src 'self' data:; connect-src 'self' https:; frame-src https://www.youtube-nocookie.com; frame-ancestors 'none'" always;
53
53
  }
54
54
 
55
55
  location / {
@@ -113,7 +113,7 @@ var HEADERS_FILE = `/*
113
113
  Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
114
114
  Referrer-Policy: strict-origin-when-cross-origin
115
115
  Permissions-Policy: camera=(), microphone=(), geolocation=()
116
- Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; font-src 'self' data:; connect-src 'self' https:; frame-src https://www.youtube-nocookie.com; frame-ancestors 'none'
116
+ Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; font-src 'self' data:; connect-src 'self' https:; frame-src https://www.youtube-nocookie.com; frame-ancestors 'none'
117
117
 
118
118
  /assets/*
119
119
  Cache-Control: public, max-age=31536000, immutable
@@ -137,7 +137,7 @@ async function runBuild() {
137
137
  process.env.FLAME_BUILD_SILENT = "1";
138
138
  process.env.LOG_LEVEL = "error";
139
139
  }
140
- const { runBuildCli } = await import("./build.impl-TUVHCOIP.js");
140
+ const { runBuildCli } = await import("./build.impl-WR24HDUT.js");
141
141
  await runBuildCli();
142
142
  }
143
143
  async function writeDockerFiles() {
@@ -145,7 +145,7 @@ async function writeDockerFiles() {
145
145
  if (!existsSync(join(dockerDir, "Dockerfile"))) {
146
146
  await writeFile(
147
147
  join(dockerDir, "Dockerfile"),
148
- `FROM ghcr.io/docubook/flame-builder:${FLAME_MAJOR} AS builder
148
+ `FROM ghcr.io/docubook/flame:${FLAME_MAJOR} AS builder
149
149
  ENV NODE_ENV=production
150
150
  WORKDIR /app
151
151
  COPY . .
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  logger
3
- } from "./chunk-PFXUCD2U.js";
3
+ } from "./chunk-AJV2GEDF.js";
4
4
  import "./chunk-4IQXHHPF.js";
5
5
 
6
6
  // .docu/node/clean.ts
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  runDeploy
3
- } from "./chunk-QL6TQV3S.js";
3
+ } from "./chunk-X7KPVCDN.js";
4
4
  import "./chunk-4IQXHHPF.js";
5
5
 
6
6
  // .docu/node/deploy.deno.ts
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  runDeploy
3
- } from "./chunk-QL6TQV3S.js";
3
+ } from "./chunk-X7KPVCDN.js";
4
4
  import "./chunk-4IQXHHPF.js";
5
5
 
6
6
  // .docu/node/deploy.node.ts
@@ -1,10 +1,12 @@
1
1
  import {
2
2
  runPreview
3
- } from "./chunk-XMIEMHTQ.js";
3
+ } from "./chunk-KEWRVASF.js";
4
+ import {
5
+ denoAdapter
6
+ } from "./chunk-UISOJ4RW.js";
4
7
  import "./chunk-EOK6KATZ.js";
5
- import "./chunk-PFXUCD2U.js";
8
+ import "./chunk-AJV2GEDF.js";
6
9
  import "./chunk-4IQXHHPF.js";
7
10
 
8
11
  // .docu/node/preview.deno.ts
9
- import { denoAdapter } from "@docubook/runt";
10
12
  await runPreview(denoAdapter);
@@ -1,10 +1,12 @@
1
1
  import {
2
2
  runPreview
3
- } from "./chunk-XMIEMHTQ.js";
3
+ } from "./chunk-KEWRVASF.js";
4
+ import {
5
+ nodeAdapter
6
+ } from "./chunk-UISOJ4RW.js";
4
7
  import "./chunk-EOK6KATZ.js";
5
- import "./chunk-PFXUCD2U.js";
8
+ import "./chunk-AJV2GEDF.js";
6
9
  import "./chunk-4IQXHHPF.js";
7
10
 
8
11
  // .docu/node/preview.node.ts
9
- import { nodeAdapter } from "@docubook/runt";
10
12
  await runPreview(nodeAdapter);
@@ -1,11 +1,13 @@
1
1
  import {
2
2
  runServer
3
- } from "./chunk-QPO4DS5F.js";
4
- import "./chunk-B6LGUADD.js";
3
+ } from "./chunk-VPV7KP7K.js";
4
+ import "./chunk-QPF4DPZQ.js";
5
+ import {
6
+ denoAdapter
7
+ } from "./chunk-UISOJ4RW.js";
5
8
  import "./chunk-EOK6KATZ.js";
6
- import "./chunk-PFXUCD2U.js";
9
+ import "./chunk-AJV2GEDF.js";
7
10
  import "./chunk-4IQXHHPF.js";
8
11
 
9
12
  // .docu/node/server.deno.ts
10
- import { denoAdapter } from "@docubook/runt";
11
13
  await runServer(denoAdapter);
@@ -1,11 +1,13 @@
1
1
  import {
2
2
  runServer
3
- } from "./chunk-QPO4DS5F.js";
4
- import "./chunk-B6LGUADD.js";
3
+ } from "./chunk-VPV7KP7K.js";
4
+ import "./chunk-QPF4DPZQ.js";
5
+ import {
6
+ nodeAdapter
7
+ } from "./chunk-UISOJ4RW.js";
5
8
  import "./chunk-EOK6KATZ.js";
6
- import "./chunk-PFXUCD2U.js";
9
+ import "./chunk-AJV2GEDF.js";
7
10
  import "./chunk-4IQXHHPF.js";
8
11
 
9
12
  // .docu/node/server.node.ts
10
- import { nodeAdapter } from "@docubook/runt";
11
13
  await runServer(nodeAdapter);
@@ -12,7 +12,7 @@ import { createHash } from "node:crypto";
12
12
  import { join, dirname } from "node:path";
13
13
  import React from "react";
14
14
  import { renderToString } from "react-dom/server";
15
- import { compileMdx, getGitLastModifiedBatch } from "./mdx";
15
+ import { compileMdx, compileMdxModule, frontmatterField, getGitLastModifiedBatch } from "./mdx";
16
16
  import {
17
17
  DOCS_DIR,
18
18
  DIST_DIR,
@@ -120,8 +120,8 @@ async function renderDocsPage(
120
120
  });
121
121
  }
122
122
 
123
- const title = (typeof frontmatter.title === "string" ? frontmatter.title : "") || slug || "Docs";
124
- const description = typeof frontmatter.description === "string" ? frontmatter.description : "";
123
+ const title = frontmatterField(frontmatter, "title") || slug || "Docs";
124
+ const description = frontmatterField(frontmatter, "description");
125
125
  const slugParts = slug ? slug.split("/") : [];
126
126
 
127
127
  const page = React.createElement(
@@ -131,12 +131,15 @@ async function renderDocsPage(
131
131
  slug: slugParts,
132
132
  title,
133
133
  description,
134
- date: (frontmatter.date as string) || undefined,
135
- content: result.content,
134
+ date: frontmatterField(frontmatter, "date") || undefined,
135
+ // Render MDX content as its own root: client hydrates the island as a
136
+ // separate root, so SSR must be root-relative too or useId-based ids
137
+ // (mdx-compiler components) mismatch during hydration.
138
+ content: renderToString(result.content),
136
139
  tocs: result.tocs,
137
140
  filePath,
138
141
  repoUrl: docuConfig.repo?.url,
139
- compiledSource: result.compiledSource,
142
+ mdxSlug: slug,
140
143
  })
141
144
  );
142
145
 
@@ -149,11 +152,9 @@ async function renderDocsPage(
149
152
  const depth = slug ? slug.split("/").length : 1;
150
153
  const favicon = docuConfig.meta?.favicon || "/docs/assets/images/favicon.ico";
151
154
  const seo = buildSeoMeta(docuConfig, frontmatter, slug || "");
152
- /**
153
- * @docubook/mdx-remote uses new Function(compiledSource) for
154
- * client-side MDX hydration. Keep allowEval=true until mdx-remote drops new Function.
155
- */
156
- const csp = cspHeader(nonce, true);
155
+ // MDX content hydrates from the bundled ESM module (mdx-hydrate), not
156
+ // new Function — no 'unsafe-eval' needed in the CSP.
157
+ const csp = cspHeader(nonce);
157
158
  let html = htmlShell({
158
159
  title,
159
160
  description,
@@ -217,9 +218,66 @@ export async function runBuild(): Promise<void> {
217
218
  let built = 0;
218
219
  let skipped = 0;
219
220
 
221
+ const pluginsConfig = docuConfig.plugins ?? [];
222
+ const builder = pluginsConfig.length > 0 ? new BuildPluginBuilder(docuConfig) : null;
223
+ if (builder) {
224
+ const plugins = await loadPlugins(pluginsConfig);
225
+ for (const plugin of plugins) {
226
+ await plugin.setup(builder);
227
+ }
228
+ await builder.runOnStart();
229
+ }
230
+
231
+ // Pre-compile every page's MDX to an ESM module (program format) so the
232
+ // client bundle can hydrate the content island statically — no new Function.
233
+ // Mirrors the page loop's transform + plugin chain so SSR and client trees
234
+ // match. Runs for all files regardless of cache; the bundle is shared by
235
+ // every page, so a content change invalidates the page cache anyway.
236
+ const mdxSources: Record<string, string> = {};
237
+ const prePassTasks = mdxFiles.map(async (file) => {
238
+ let raw: string;
239
+ try {
240
+ raw = await readFile(file.absPath, "utf-8");
241
+ } catch {
242
+ return;
243
+ }
244
+ let content = raw;
245
+ if (builder) {
246
+ const relPath = file.absPath.replace(PROJECT_ROOT + "/", "");
247
+ const transformed = await builder.runOnLoad(relPath, content);
248
+ if (transformed?.contents) content = transformed.contents;
249
+ }
250
+ const remarkPlugins = builder?.collectRemarkPlugins();
251
+ const rehypePlugins = builder?.collectRehypePlugins();
252
+ mdxSources[file.path] = await compileMdxModule(content, remarkPlugins, rehypePlugins);
253
+ });
254
+ await Promise.all(prePassTasks);
255
+
256
+ // The docs root (index.mdx) renders with slug "" — mirror that key so the
257
+ // index page hydrates too. Its render has its own try/catch; skip on error.
258
+ const indexMdxPath = join(DOCS_DIR, "index.mdx");
259
+ if (existsSync(indexMdxPath)) {
260
+ try {
261
+ const indexRaw = await readFile(indexMdxPath, "utf-8");
262
+ let indexContent = indexRaw;
263
+ if (builder) {
264
+ const relPath = indexMdxPath.replace(PROJECT_ROOT + "/", "");
265
+ const transformed = await builder.runOnLoad(relPath, indexContent);
266
+ if (transformed?.contents) indexContent = transformed.contents;
267
+ }
268
+ mdxSources[""] = await compileMdxModule(
269
+ indexContent,
270
+ builder?.collectRemarkPlugins(),
271
+ builder?.collectRehypePlugins()
272
+ );
273
+ } catch {
274
+ // ignore — the index render reports its own error
275
+ }
276
+ }
277
+
220
278
  logger.bundleStart();
221
279
  let t = performance.now();
222
- assetManifest = await buildClientBundle();
280
+ assetManifest = await buildClientBundle(mdxSources);
223
281
  logger.bundleDone(Math.round(performance.now() - t));
224
282
 
225
283
  inlineThemeCss = computeInlineThemeCss();
@@ -235,16 +293,6 @@ export async function runBuild(): Promise<void> {
235
293
  };
236
294
  }
237
295
 
238
- const pluginsConfig = docuConfig.plugins ?? [];
239
- const builder = pluginsConfig.length > 0 ? new BuildPluginBuilder(docuConfig) : null;
240
- if (builder) {
241
- const plugins = await loadPlugins(pluginsConfig);
242
- for (const plugin of plugins) {
243
- await plugin.setup(builder);
244
- }
245
- await builder.runOnStart();
246
- }
247
-
248
296
  logger.spinner.start("Building pages...");
249
297
  t = performance.now();
250
298
 
@@ -362,8 +410,7 @@ export async function runBuild(): Promise<void> {
362
410
  body: renderToString(landingPage),
363
411
  favicon: landingFavicon,
364
412
  seo: landingSeo,
365
- /** unsafe-eval required by mdx-remote hydration — see above. */
366
- csp: cspHeader(landingNonce, true),
413
+ csp: cspHeader(landingNonce),
367
414
  css: assetManifest.css,
368
415
  js: assetManifest.js,
369
416
  nonce: landingNonce,
@@ -384,8 +431,7 @@ export async function runBuild(): Promise<void> {
384
431
  body: renderToString(notFoundPage),
385
432
  favicon: notFoundFavicon,
386
433
  headExtra: ['<meta name="robots" content="noindex,follow">'],
387
- /** unsafe-eval required by mdx-remote hydration — see above. */
388
- csp: cspHeader(notFoundNonce, true),
434
+ csp: cspHeader(notFoundNonce),
389
435
  css: assetManifest.css,
390
436
  js: assetManifest.js,
391
437
  nonce: notFoundNonce,