@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.
- package/.docu/components/Context.tsx +1 -1
- package/.docu/components/Pagination.tsx +9 -1
- package/.docu/components/Search.tsx +6 -6
- package/.docu/components/Sidebar.tsx +37 -8
- package/.docu/components/Theme.tsx +2 -2
- package/.docu/components/Toc.tsx +52 -6
- package/.docu/components/Typography.tsx +1 -1
- package/.docu/components/home/Hero.tsx +4 -4
- package/.docu/components/registry.ts +1 -1
- package/.docu/lib/build.deno.js +3 -3
- package/.docu/lib/{build.impl-TSIF3F7O.js → build.impl-VXB4KL4D.js} +3 -3
- package/.docu/lib/build.node.js +3 -3
- package/.docu/lib/{chunk-SPHVBXRR.js → chunk-7PRQ3RQB.js} +6 -6
- package/.docu/lib/{chunk-DITXUPUV.js → chunk-A6FIEG3H.js} +61 -10
- package/.docu/lib/{chunk-PJIJEPNR.js → chunk-JRERMREW.js} +5 -4
- package/.docu/lib/{chunk-KMDGSD57.js → chunk-LZDEWK25.js} +3 -3
- package/.docu/lib/{chunk-HRO7ONJQ.js → chunk-MQWWCO6O.js} +70 -27
- package/.docu/lib/{chunk-B6LGUADD.js → chunk-TRT6WQZG.js} +346 -183
- package/.docu/lib/chunk-UISOJ4RW.js +114 -0
- package/.docu/lib/clean.js +1 -1
- package/.docu/lib/deploy.deno.js +1 -1
- package/.docu/lib/deploy.node.js +1 -1
- package/.docu/lib/preview.deno.js +5 -3
- package/.docu/lib/preview.node.js +5 -3
- package/.docu/lib/server.deno.js +6 -4
- package/.docu/lib/server.node.js +6 -4
- package/.docu/node/build.impl.ts +75 -26
- package/.docu/node/build.ts +70 -17
- package/.docu/node/client.ts +64 -27
- package/.docu/node/deploy.shared.ts +5 -5
- package/.docu/node/deploy.ts +1 -1
- package/.docu/node/html.shared.ts +7 -2
- package/.docu/node/html.ts +4 -2
- package/.docu/node/hydrate.node.ts +69 -4
- package/.docu/node/hydrate.ts +50 -1
- package/.docu/node/mdx-manifest.d.ts +5 -0
- package/.docu/node/mdx.ts +103 -17
- package/.docu/node/preview.deno.ts +1 -1
- package/.docu/node/preview.impl.ts +3 -3
- package/.docu/node/preview.node.ts +1 -1
- package/.docu/node/preview.ts +2 -2
- package/.docu/node/route.ts +57 -1
- package/.docu/node/runtime/bun.ts +28 -0
- package/.docu/node/runtime/deno.ts +34 -0
- package/.docu/node/runtime/index.ts +4 -0
- package/.docu/node/runtime/node.ts +107 -0
- package/.docu/node/runtime/types.ts +19 -0
- package/.docu/node/search-indexer.ts +3 -2
- package/.docu/node/seo.ts +2 -2
- package/.docu/node/server-routes.ts +32 -13
- package/.docu/node/server.deno.ts +1 -1
- package/.docu/node/server.impl.ts +49 -3
- package/.docu/node/server.node.ts +1 -1
- package/.docu/node/server.ts +45 -1
- package/.docu/pages/docs/[[...slug]].tsx +13 -4
- package/.docu/pages/index.tsx +1 -1
- package/.docu/styles/globals.css +38 -20
- package/README.md +7 -48
- package/bin/cli.js +25 -7
- package/package.json +7 -6
- package/template/README.md +7 -48
- package/template/docs/getting-started/configuration.mdx +16 -4
- package/template/docs/getting-started/overview.mdx +34 -42
- package/template/docs/guide/components.mdx +46 -228
- package/template/docs/guide/routing.mdx +8 -6
- package/template/docs/index.mdx +9 -8
- 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
|
+
};
|
package/.docu/lib/clean.js
CHANGED
package/.docu/lib/deploy.deno.js
CHANGED
package/.docu/lib/deploy.node.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
runPreview
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-LZDEWK25.js";
|
|
4
|
+
import {
|
|
5
|
+
denoAdapter
|
|
6
|
+
} from "./chunk-UISOJ4RW.js";
|
|
4
7
|
import "./chunk-EOK6KATZ.js";
|
|
5
|
-
import "./chunk-
|
|
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-
|
|
3
|
+
} from "./chunk-LZDEWK25.js";
|
|
4
|
+
import {
|
|
5
|
+
nodeAdapter
|
|
6
|
+
} from "./chunk-UISOJ4RW.js";
|
|
4
7
|
import "./chunk-EOK6KATZ.js";
|
|
5
|
-
import "./chunk-
|
|
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);
|
package/.docu/lib/server.deno.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
runServer
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
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-
|
|
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);
|
package/.docu/lib/server.node.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
runServer
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
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-
|
|
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);
|
package/.docu/node/build.impl.ts
CHANGED
|
@@ -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 = (
|
|
124
|
-
const 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
|
|
135
|
-
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
|
-
|
|
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
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
package/.docu/node/build.ts
CHANGED
|
@@ -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 = (
|
|
117
|
-
const 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
|
|
128
|
-
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
|
-
|
|
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
|
|
package/.docu/node/client.ts
CHANGED
|
@@ -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/
|
|
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
|
-
|
|
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 (
|
|
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
|
-
//
|
|
29
|
-
//
|
|
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
|
-
|
|
56
|
+
"create"
|
|
41
57
|
);
|
|
42
58
|
|
|
43
|
-
//
|
|
44
|
-
//
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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") {
|