@docubook/flame 1.7.2 → 2.0.0-alpha.1

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 (67) hide show
  1. package/.docu/components/Context.tsx +1 -1
  2. package/.docu/components/Pagination.tsx +9 -1
  3. package/.docu/components/Search.tsx +6 -6
  4. package/.docu/components/Sidebar.tsx +37 -8
  5. package/.docu/components/Theme.tsx +2 -2
  6. package/.docu/components/Toc.tsx +52 -6
  7. package/.docu/components/Typography.tsx +1 -1
  8. package/.docu/components/home/Hero.tsx +4 -4
  9. package/.docu/components/registry.ts +1 -1
  10. package/.docu/lib/build.deno.js +3 -3
  11. package/.docu/lib/{build.impl-TSIF3F7O.js → build.impl-VXB4KL4D.js} +3 -3
  12. package/.docu/lib/build.node.js +3 -3
  13. package/.docu/lib/{chunk-SPHVBXRR.js → chunk-7PRQ3RQB.js} +6 -6
  14. package/.docu/lib/{chunk-DITXUPUV.js → chunk-A6FIEG3H.js} +61 -10
  15. package/.docu/lib/{chunk-PJIJEPNR.js → chunk-JRERMREW.js} +5 -4
  16. package/.docu/lib/{chunk-KMDGSD57.js → chunk-LZDEWK25.js} +3 -3
  17. package/.docu/lib/{chunk-HRO7ONJQ.js → chunk-MQWWCO6O.js} +70 -27
  18. package/.docu/lib/{chunk-B6LGUADD.js → chunk-TRT6WQZG.js} +346 -183
  19. package/.docu/lib/chunk-UISOJ4RW.js +114 -0
  20. package/.docu/lib/clean.js +1 -1
  21. package/.docu/lib/deploy.deno.js +1 -1
  22. package/.docu/lib/deploy.node.js +1 -1
  23. package/.docu/lib/preview.deno.js +5 -3
  24. package/.docu/lib/preview.node.js +5 -3
  25. package/.docu/lib/server.deno.js +6 -4
  26. package/.docu/lib/server.node.js +6 -4
  27. package/.docu/node/build.impl.ts +75 -26
  28. package/.docu/node/build.ts +70 -17
  29. package/.docu/node/client.ts +64 -27
  30. package/.docu/node/deploy.shared.ts +5 -5
  31. package/.docu/node/deploy.ts +1 -1
  32. package/.docu/node/html.shared.ts +7 -2
  33. package/.docu/node/html.ts +4 -2
  34. package/.docu/node/hydrate.node.ts +69 -4
  35. package/.docu/node/hydrate.ts +50 -1
  36. package/.docu/node/mdx-manifest.d.ts +5 -0
  37. package/.docu/node/mdx.ts +103 -17
  38. package/.docu/node/preview.deno.ts +1 -1
  39. package/.docu/node/preview.impl.ts +3 -3
  40. package/.docu/node/preview.node.ts +1 -1
  41. package/.docu/node/preview.ts +2 -2
  42. package/.docu/node/route.ts +57 -1
  43. package/.docu/node/runtime/bun.ts +28 -0
  44. package/.docu/node/runtime/deno.ts +34 -0
  45. package/.docu/node/runtime/index.ts +4 -0
  46. package/.docu/node/runtime/node.ts +107 -0
  47. package/.docu/node/runtime/types.ts +19 -0
  48. package/.docu/node/search-indexer.ts +3 -2
  49. package/.docu/node/seo.ts +2 -2
  50. package/.docu/node/server-routes.ts +32 -13
  51. package/.docu/node/server.deno.ts +1 -1
  52. package/.docu/node/server.impl.ts +49 -3
  53. package/.docu/node/server.node.ts +1 -1
  54. package/.docu/node/server.ts +45 -1
  55. package/.docu/pages/docs/[[...slug]].tsx +13 -4
  56. package/.docu/pages/index.tsx +1 -1
  57. package/.docu/styles/globals.css +38 -20
  58. package/README.md +7 -48
  59. package/bin/cli.js +25 -7
  60. package/package.json +7 -6
  61. package/template/README.md +7 -48
  62. package/template/docs/getting-started/configuration.mdx +16 -4
  63. package/template/docs/getting-started/overview.mdx +34 -42
  64. package/template/docs/guide/components.mdx +46 -228
  65. package/template/docs/guide/routing.mdx +8 -6
  66. package/template/docs/index.mdx +9 -8
  67. package/template/docu.json +4 -4
@@ -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
+ };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  logger
3
- } from "./chunk-PJIJEPNR.js";
3
+ } from "./chunk-JRERMREW.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-SPHVBXRR.js";
3
+ } from "./chunk-7PRQ3RQB.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-SPHVBXRR.js";
3
+ } from "./chunk-7PRQ3RQB.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-KMDGSD57.js";
3
+ } from "./chunk-LZDEWK25.js";
4
+ import {
5
+ denoAdapter
6
+ } from "./chunk-UISOJ4RW.js";
4
7
  import "./chunk-EOK6KATZ.js";
5
- import "./chunk-PJIJEPNR.js";
8
+ import "./chunk-JRERMREW.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-KMDGSD57.js";
3
+ } from "./chunk-LZDEWK25.js";
4
+ import {
5
+ nodeAdapter
6
+ } from "./chunk-UISOJ4RW.js";
4
7
  import "./chunk-EOK6KATZ.js";
5
- import "./chunk-PJIJEPNR.js";
8
+ import "./chunk-JRERMREW.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-DITXUPUV.js";
4
- import "./chunk-B6LGUADD.js";
3
+ } from "./chunk-A6FIEG3H.js";
4
+ import "./chunk-TRT6WQZG.js";
5
+ import {
6
+ denoAdapter
7
+ } from "./chunk-UISOJ4RW.js";
5
8
  import "./chunk-EOK6KATZ.js";
6
- import "./chunk-PJIJEPNR.js";
9
+ import "./chunk-JRERMREW.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-DITXUPUV.js";
4
- import "./chunk-B6LGUADD.js";
3
+ } from "./chunk-A6FIEG3H.js";
4
+ import "./chunk-TRT6WQZG.js";
5
+ import {
6
+ nodeAdapter
7
+ } from "./chunk-UISOJ4RW.js";
5
8
  import "./chunk-EOK6KATZ.js";
6
- import "./chunk-PJIJEPNR.js";
9
+ import "./chunk-JRERMREW.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,12 +431,14 @@ 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,
392
438
  themeCss: inlineThemeCss,
439
+ // Served as the static-host fallback at ANY requested path — relative
440
+ // depth can never be right there, so use root-absolute asset URLs.
441
+ absoluteAssets: true,
393
442
  });
394
443
  await writeFile(join(DIST_DIR, "404.html"), notFoundHtml);
395
444
 
@@ -4,7 +4,7 @@ import { createHash } from "node:crypto";
4
4
  import { join, dirname } from "node:path";
5
5
  import React from "react";
6
6
  import { renderToString } from "react-dom/server";
7
- import { compileMdx, getGitLastModifiedBatch } from "./mdx";
7
+ import { compileMdx, compileMdxModule, frontmatterField, getGitLastModifiedBatch } from "./mdx";
8
8
  import {
9
9
  DOCS_DIR,
10
10
  DIST_DIR,
@@ -113,8 +113,8 @@ async function renderDocsPage(
113
113
  });
114
114
  }
115
115
 
116
- const title = (typeof frontmatter.title === "string" ? frontmatter.title : "") || slug || "Docs";
117
- const description = typeof frontmatter.description === "string" ? frontmatter.description : "";
116
+ const title = frontmatterField(frontmatter, "title") || slug || "Docs";
117
+ const description = frontmatterField(frontmatter, "description");
118
118
  const slugParts = slug ? slug.split("/") : [];
119
119
 
120
120
  const page = React.createElement(
@@ -124,12 +124,15 @@ async function renderDocsPage(
124
124
  slug: slugParts,
125
125
  title,
126
126
  description,
127
- date: (frontmatter.date as string) || undefined,
128
- content: result.content,
127
+ date: frontmatterField(frontmatter, "date") || undefined,
128
+ // Render MDX content as its own root: client hydrates the island as a
129
+ // separate root, so SSR must be root-relative too or useId-based ids
130
+ // (mdx-compiler components) mismatch during hydration.
131
+ content: renderToString(result.content),
129
132
  tocs: result.tocs,
130
133
  filePath,
131
134
  repoUrl: docuConfig.repo?.url,
132
- compiledSource: result.compiledSource,
135
+ mdxSlug: slug,
133
136
  })
134
137
  );
135
138
 
@@ -203,9 +206,66 @@ async function build() {
203
206
  let built = 0;
204
207
  let skipped = 0;
205
208
 
209
+ const pluginsConfig = docuConfig.plugins ?? [];
210
+ const builder = pluginsConfig.length > 0 ? new BuildPluginBuilder(docuConfig) : null;
211
+ if (builder) {
212
+ const plugins = await loadPlugins(pluginsConfig);
213
+ for (const plugin of plugins) {
214
+ await plugin.setup(builder);
215
+ }
216
+ await builder.runOnStart();
217
+ }
218
+
219
+ // Pre-compile every page's MDX to an ESM module (program format) so the
220
+ // client bundle can hydrate the content island statically — no new Function.
221
+ // Mirrors the page loop's transform + plugin chain so SSR and client trees
222
+ // match. Runs for all files regardless of cache; the bundle is shared by
223
+ // every page, so a content change invalidates the page cache anyway.
224
+ const mdxSources: Record<string, string> = {};
225
+ const prePassTasks = mdxFiles.map(async (file) => {
226
+ let raw: string;
227
+ try {
228
+ raw = await readFile(file.absPath, "utf-8");
229
+ } catch {
230
+ return;
231
+ }
232
+ let content = raw;
233
+ if (builder) {
234
+ const relPath = file.absPath.replace(PROJECT_ROOT + "/", "");
235
+ const transformed = await builder.runOnLoad(relPath, content);
236
+ if (transformed?.contents) content = transformed.contents;
237
+ }
238
+ const remarkPlugins = builder?.collectRemarkPlugins();
239
+ const rehypePlugins = builder?.collectRehypePlugins();
240
+ mdxSources[file.path] = await compileMdxModule(content, remarkPlugins, rehypePlugins);
241
+ });
242
+ await Promise.all(prePassTasks);
243
+
244
+ // The docs root (index.mdx) renders with slug "" — mirror that key so the
245
+ // index page hydrates too. Its render has its own try/catch; skip on error.
246
+ const indexMdxPath = join(DOCS_DIR, "index.mdx");
247
+ if (existsSync(indexMdxPath)) {
248
+ try {
249
+ const indexRaw = await readFile(indexMdxPath, "utf-8");
250
+ let indexContent = indexRaw;
251
+ if (builder) {
252
+ const relPath = indexMdxPath.replace(PROJECT_ROOT + "/", "");
253
+ const transformed = await builder.runOnLoad(relPath, indexContent);
254
+ if (transformed?.contents) indexContent = transformed.contents;
255
+ }
256
+ mdxSources[""] = await compileMdxModule(
257
+ indexContent,
258
+ builder?.collectRemarkPlugins(),
259
+ builder?.collectRehypePlugins()
260
+ );
261
+ } catch {
262
+ // ignore — the index render reports its own error
263
+ }
264
+ }
265
+
206
266
  logger.bundleStart();
207
267
  let t = performance.now();
208
- assetManifest = await buildClientBundle();
268
+ assetManifest = await buildClientBundle(mdxSources);
209
269
  logger.bundleDone(Math.round(performance.now() - t));
210
270
 
211
271
  inlineThemeCss = computeInlineThemeCss();
@@ -221,16 +281,6 @@ async function build() {
221
281
  };
222
282
  }
223
283
 
224
- const pluginsConfig = docuConfig.plugins ?? [];
225
- const builder = pluginsConfig.length > 0 ? new BuildPluginBuilder(docuConfig) : null;
226
- if (builder) {
227
- const plugins = await loadPlugins(pluginsConfig);
228
- for (const plugin of plugins) {
229
- await plugin.setup(builder);
230
- }
231
- await builder.runOnStart();
232
- }
233
-
234
284
  logger.spinner.start("Building pages...");
235
285
  t = performance.now();
236
286
 
@@ -368,6 +418,9 @@ async function build() {
368
418
  js: assetManifest.js,
369
419
  nonce: generateNonce(),
370
420
  themeCss: inlineThemeCss,
421
+ // Served as the static-host fallback at ANY requested path — relative
422
+ // depth can never be right there, so use root-absolute asset URLs.
423
+ absoluteAssets: true,
371
424
  });
372
425
  await writeFile(join(DIST_DIR, "404.html"), notFoundHtml);
373
426
 
@@ -1,22 +1,37 @@
1
1
  import { createRoot, hydrateRoot } from "react-dom/client";
2
2
  import React from "react";
3
+ import { MDXProvider } from "@mdx-js/react";
3
4
  import { MDXRemote } from "@docubook/core";
4
- import { createMdxComponents } from "@docubook/mdx-content";
5
+ import { createMdxComponents } from "@docubook/markdown";
6
+ import { mdxModules } from "./mdx-manifest";
5
7
  import Sidebar, { MobileBar } from "../components/Sidebar";
6
8
  import Toc from "../components/Toc";
7
9
  import { ThemeToggle } from "../components/Theme";
8
10
  import { safeParseTocs } from "./parse-tocs";
9
11
  import type { TocItem } from "./types";
10
12
 
13
+ /**
14
+ * Island mount mode — deliberate trade-off per island API:
15
+ *
16
+ * - `hydrate`: SSR HTML exists and the client renders the identical tree →
17
+ * attach React in place (no flash, SSR content preserved).
18
+ * - `create`: client-only render — the client tree deliberately differs from
19
+ * SSR (or SSR output is absent) → full render, discards SSR markup.
20
+ * - `auto`: hydrate when the SSR container has children, else create.
21
+ */
22
+ type MountMode = "auto" | "hydrate" | "create";
23
+
11
24
  function mountIsland(
12
25
  id: string,
13
- render: (el: HTMLElement) => React.ReactElement,
14
- forceCreate = false
26
+ render: (el: HTMLElement) => React.ReactElement | null,
27
+ mode: MountMode = "auto"
15
28
  ) {
16
29
  const el = document.getElementById(id);
17
30
  if (!el) return;
18
31
  const node = render(el);
19
- if (!forceCreate && el.childElementCount > 0) {
32
+ if (node === null) return; // island stays as-is (SSR HTML preserved)
33
+ const hydrate = mode === "hydrate" || (mode === "auto" && el.childElementCount > 0);
34
+ if (hydrate) {
20
35
  hydrateRoot(el, node);
21
36
  } else {
22
37
  el.innerHTML = "";
@@ -25,8 +40,9 @@ function mountIsland(
25
40
  }
26
41
 
27
42
  function mountIslands() {
28
- // forceCreate: SSR sidebar renders <Menu> only; client renders full <Sidebar>
29
- // (DesktopSidebar + MobileBar) — structure mismatch forces full createRoot.
43
+ // SSR renders <Menu> only; client renders full <Sidebar> (DesktopSidebar +
44
+ // MobileBar) — structural mismatch makes hydration impossible, so always
45
+ // createRoot and discard the SSR <Menu> markup.
30
46
  mountIsland(
31
47
  "sidebar-island",
32
48
  (el) => {
@@ -37,11 +53,11 @@ function mountIslands() {
37
53
  repoUrl: el.dataset.repo || "",
38
54
  });
39
55
  },
40
- true
56
+ "create"
41
57
  );
42
58
 
43
- // mobile-bar-island SSR div is empty (data attributes only),
44
- // so hydrateRoot child check falls through to createRoot automatically.
59
+ // SSR div is empty (data attributes only) — childElementCount is 0, so
60
+ // auto falls through to createRoot.
45
61
  mountIsland("mobile-bar-island", (el) => {
46
62
  const tocs: TocItem[] = safeParseTocs(el.dataset.tocs);
47
63
  return React.createElement(MobileBar, {
@@ -58,24 +74,45 @@ function mountIslands() {
58
74
 
59
75
  mountIsland("theme-island", () => React.createElement(ThemeToggle));
60
76
 
61
- hydrateMdxContent();
62
- }
63
-
64
- function hydrateMdxContent() {
65
- const island = document.getElementById("mdx-content-island");
66
- const sourceEl = document.getElementById("mdx-compiled-source");
67
- if (!island || !sourceEl) return;
68
-
69
- try {
70
- const compiledSource = JSON.parse(sourceEl.textContent || "");
71
- const components = createMdxComponents();
72
- hydrateRoot(
73
- island,
74
- React.createElement(MDXRemote, { compiledSource, scope: {}, frontmatter: {}, components })
75
- );
76
- } catch (e) {
77
- console.error("[mdx-hydrate]", e);
78
- }
77
+ // MDX content: SSR renders the full content HTML; the client rebuilds the
78
+ // identical tree. Two sources, same tree shape:
79
+ // - static build: per-slug compiled ESM module bundled via ./mdx-manifest
80
+ // (no new Function) — module and SSR output come from the same plugin
81
+ // chain, so hydration matches;
82
+ // - dev: legacy per-page compiledSource script → MDXRemote eval.
83
+ // Hydrate when SSR markup exists, create only when the container is empty.
84
+ mountIsland(
85
+ "mdx-content-island",
86
+ (el) => {
87
+ const sourceEl = document.getElementById("mdx-compiled-source");
88
+ if (sourceEl) {
89
+ try {
90
+ const compiledSource = JSON.parse(sourceEl.textContent || "");
91
+ return React.createElement(MDXRemote, {
92
+ compiledSource,
93
+ scope: {},
94
+ frontmatter: {},
95
+ components: createMdxComponents(),
96
+ });
97
+ } catch (e) {
98
+ console.error("[mdx-hydrate]", e);
99
+ return null;
100
+ }
101
+ }
102
+ const slug = el.dataset.mdxSlug;
103
+ // The docs root (index.mdx) renders with an empty slug — `mdxSlug != null`
104
+ // keeps "" addressable (its module is stored under key ""), while a
105
+ // missing marker stays undefined and skips hydration.
106
+ const mod = slug != null ? mdxModules[slug] : undefined;
107
+ if (!mod) return null;
108
+ return React.createElement(
109
+ MDXProvider,
110
+ { components: createMdxComponents() },
111
+ React.createElement(mod.default, null)
112
+ );
113
+ },
114
+ "auto"
115
+ );
79
116
  }
80
117
 
81
118
  if (document.readyState === "loading") {