@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,107 @@
|
|
|
1
|
+
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
|
2
|
+
import type { FetchHandler, RuntimeAdapter, ServerHandle, ServerOptions } from "./types";
|
|
3
|
+
|
|
4
|
+
function toWebRequest(req: IncomingMessage, port: number, hostname: string): Request {
|
|
5
|
+
const host = req.headers.host ?? `${hostname}:${port}`;
|
|
6
|
+
const url = `http://${host}${req.url ?? "/"}`;
|
|
7
|
+
const headers = new Headers();
|
|
8
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
9
|
+
if (value === undefined) continue;
|
|
10
|
+
if (Array.isArray(value)) {
|
|
11
|
+
for (const v of value) headers.append(key, v);
|
|
12
|
+
} else {
|
|
13
|
+
headers.set(key, value);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const method = req.method ?? "GET";
|
|
17
|
+
const hasBody = method !== "GET" && method !== "HEAD";
|
|
18
|
+
return new Request(url, {
|
|
19
|
+
method,
|
|
20
|
+
headers,
|
|
21
|
+
// IncomingMessage is an async iterable of Buffer chunks; Request accepts
|
|
22
|
+
// an async iterable body when half-duplex is declared.
|
|
23
|
+
body: hasBody ? (req as unknown as BodyInit) : undefined,
|
|
24
|
+
// @ts-expect-error -- required by undici for streaming request bodies
|
|
25
|
+
duplex: hasBody ? "half" : undefined,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function writeResponse(response: Response, res: ServerResponse): Promise<void> {
|
|
30
|
+
const headers: Record<string, string | string[]> = {};
|
|
31
|
+
const setCookie = response.headers.getSetCookie?.() ?? [];
|
|
32
|
+
response.headers.forEach((value, key) => {
|
|
33
|
+
if (key === "set-cookie") return;
|
|
34
|
+
headers[key] = value;
|
|
35
|
+
});
|
|
36
|
+
if (setCookie.length > 0) headers["set-cookie"] = setCookie;
|
|
37
|
+
|
|
38
|
+
// Flush headers immediately so streaming responses (SSE) reach the client
|
|
39
|
+
// before the first body chunk.
|
|
40
|
+
res.writeHead(response.status, headers);
|
|
41
|
+
|
|
42
|
+
if (!response.body) {
|
|
43
|
+
res.end();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const reader = response.body.getReader();
|
|
48
|
+
try {
|
|
49
|
+
for (;;) {
|
|
50
|
+
const { done, value } = await reader.read();
|
|
51
|
+
if (done) break;
|
|
52
|
+
// Incremental piping — never buffer the full body (SSE streams forever).
|
|
53
|
+
const ok = res.write(value);
|
|
54
|
+
if (!ok) await new Promise<void>((resolve) => res.once("drain", resolve));
|
|
55
|
+
}
|
|
56
|
+
res.end();
|
|
57
|
+
} catch {
|
|
58
|
+
// Client disconnected mid-stream; cancel the source.
|
|
59
|
+
await reader.cancel().catch(() => {});
|
|
60
|
+
res.destroy();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const nodeAdapter: RuntimeAdapter = {
|
|
65
|
+
name: "node",
|
|
66
|
+
|
|
67
|
+
serve(fetch: FetchHandler, options: ServerOptions): Promise<ServerHandle> {
|
|
68
|
+
const hostname = options.hostname ?? "localhost";
|
|
69
|
+
|
|
70
|
+
const server = createServer((req, res) => {
|
|
71
|
+
Promise.resolve()
|
|
72
|
+
.then(() => fetch(toWebRequest(req, options.port, hostname)))
|
|
73
|
+
.then((response) => writeResponse(response, res))
|
|
74
|
+
.catch((err) => {
|
|
75
|
+
console.error(err);
|
|
76
|
+
if (!res.headersSent) {
|
|
77
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
78
|
+
}
|
|
79
|
+
res.end("Internal Server Error");
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (options.idleTimeout !== undefined) {
|
|
84
|
+
server.timeout = options.idleTimeout * 1000;
|
|
85
|
+
// keepAliveTimeout must not undercut idleTimeout or Node closes
|
|
86
|
+
// long-lived SSE connections early.
|
|
87
|
+
server.keepAliveTimeout = options.idleTimeout * 1000;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
server.once("error", reject);
|
|
92
|
+
server.listen(options.port, () => {
|
|
93
|
+
const address = server.address();
|
|
94
|
+
const port = typeof address === "object" && address ? address.port : options.port;
|
|
95
|
+
resolve({
|
|
96
|
+
port,
|
|
97
|
+
hostname,
|
|
98
|
+
stop: () =>
|
|
99
|
+
new Promise<void>((res2, rej2) => {
|
|
100
|
+
server.closeAllConnections?.();
|
|
101
|
+
server.close((err) => (err ? rej2(err) : res2()));
|
|
102
|
+
}),
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
},
|
|
107
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface ServerOptions {
|
|
2
|
+
port: number;
|
|
3
|
+
hostname?: string;
|
|
4
|
+
/** Seconds a connection may stay idle before being closed. */
|
|
5
|
+
idleTimeout?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ServerHandle {
|
|
9
|
+
port: number;
|
|
10
|
+
hostname: string;
|
|
11
|
+
stop(): void | Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type FetchHandler = (req: Request) => Response | Promise<Response>;
|
|
15
|
+
|
|
16
|
+
export interface RuntimeAdapter {
|
|
17
|
+
name: string;
|
|
18
|
+
serve(fetch: FetchHandler, options: ServerOptions): ServerHandle | Promise<ServerHandle>;
|
|
19
|
+
}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
15
15
|
import { resolve, join } from "node:path";
|
|
16
16
|
import { extractFrontmatterWithContent } from "@docubook/core";
|
|
17
|
+
import { frontmatterField } from "./mdx";
|
|
17
18
|
import { DOCS_DIR, ASSETS_DIR, loadDocuConfig } from "./paths";
|
|
18
19
|
import { scanMdxFiles, docsHtmlHref } from "./utils";
|
|
19
20
|
|
|
@@ -74,7 +75,7 @@ export function extractRecords(filePath: string, raw: string): SearchRecord[] {
|
|
|
74
75
|
const records: SearchRecord[] = [];
|
|
75
76
|
const url = docsHtmlHref(`/docs/${filePath}`);
|
|
76
77
|
const lvl0 = getSectionTitle(filePath);
|
|
77
|
-
const lvl1 = frontmatter
|
|
78
|
+
const lvl1 = frontmatterField(frontmatter, "title") || null;
|
|
78
79
|
|
|
79
80
|
const hierarchy = {
|
|
80
81
|
lvl0,
|
|
@@ -90,7 +91,7 @@ export function extractRecords(filePath: string, raw: string): SearchRecord[] {
|
|
|
90
91
|
records.push({
|
|
91
92
|
url,
|
|
92
93
|
hierarchy: { ...hierarchy },
|
|
93
|
-
content: frontmatter
|
|
94
|
+
content: frontmatterField(frontmatter, "description") || null,
|
|
94
95
|
type: "lvl1",
|
|
95
96
|
});
|
|
96
97
|
}
|
package/.docu/node/seo.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DocuConfig } from "./types";
|
|
2
|
+
import { frontmatterField } from "./mdx";
|
|
2
3
|
|
|
3
4
|
export interface SeoMeta {
|
|
4
5
|
/** Absolute canonical URL */
|
|
@@ -27,8 +28,7 @@ export function buildSeoMeta(
|
|
|
27
28
|
};
|
|
28
29
|
|
|
29
30
|
// Per-page image from frontmatter, fallback to global default from config
|
|
30
|
-
const image =
|
|
31
|
-
(typeof frontmatter.image === "string" && frontmatter.image) || config.meta?.ogImage;
|
|
31
|
+
const image = frontmatterField(frontmatter, "image") || config.meta?.ogImage;
|
|
32
32
|
if (image) {
|
|
33
33
|
// Resolve using URL constructor — handles absolute, root-relative, and relative paths
|
|
34
34
|
try {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { readFileSync, statSync } from "node:fs";
|
|
4
4
|
import React, { type ReactNode } from "react";
|
|
5
5
|
import { renderToString } from "react-dom/server";
|
|
6
|
-
import { compileMdx } from "./mdx";
|
|
6
|
+
import { compileMdx, frontmatterField } from "./mdx";
|
|
7
7
|
import { getContentType } from "./utils";
|
|
8
8
|
import { DOCS_DIR, DIST_DIR, PROJECT_ROOT } from "./paths";
|
|
9
9
|
import { BuildPluginBuilder } from "./plugin-builder";
|
|
@@ -44,11 +44,25 @@ function createHtmlResponse(
|
|
|
44
44
|
extraScripts: hmrScript(nonce),
|
|
45
45
|
themeCss: state.inlineThemeCss,
|
|
46
46
|
depth,
|
|
47
|
+
// 404 pages can be requested at arbitrary depths (e.g. a noLink section
|
|
48
|
+
// path typed in the address bar) — relative asset paths would resolve
|
|
49
|
+
// against the wrong directory and break CSS/JS.
|
|
50
|
+
absoluteAssets: status === 404,
|
|
47
51
|
});
|
|
48
|
-
/**
|
|
52
|
+
/** Dev-only: serves compiledSource → MDXRemote eval path (no CSP in production). */
|
|
49
53
|
return htmlResponse(html, nonce, status, true);
|
|
50
54
|
}
|
|
51
55
|
|
|
56
|
+
// Dev-only memo: compileMdx per request is ~70ms; repeat navigations with
|
|
57
|
+
// an unchanged file (same mtime) reuse the compiled result. The dev server
|
|
58
|
+
// has no other per-page cache — without this every navigation re-parses MDX.
|
|
59
|
+
// ponytail: unbounded Map is fine — docs sites have a handful of pages; cap
|
|
60
|
+
// with an LRU only if a project exceeds thousands of routes.
|
|
61
|
+
const docsCache = new Map<
|
|
62
|
+
string,
|
|
63
|
+
{ mtimeMs: number; doc: NonNullable<Awaited<ReturnType<typeof getDocsForSlug>>> }
|
|
64
|
+
>();
|
|
65
|
+
|
|
52
66
|
async function getDocsForSlug(
|
|
53
67
|
slug: string,
|
|
54
68
|
state: ServerState
|
|
@@ -92,6 +106,12 @@ async function getDocsForSlug(
|
|
|
92
106
|
|
|
93
107
|
const relPath = filePath.replace(PROJECT_ROOT + "/", "");
|
|
94
108
|
|
|
109
|
+
const mtimeMs = (await stat(filePath)).mtimeMs;
|
|
110
|
+
const cached = docsCache.get(relPath);
|
|
111
|
+
if (cached && cached.mtimeMs === mtimeMs) {
|
|
112
|
+
return cached.doc;
|
|
113
|
+
}
|
|
114
|
+
|
|
95
115
|
let content = raw;
|
|
96
116
|
if (state.builder) {
|
|
97
117
|
const transformed = await state.builder.runOnLoad(relPath, content);
|
|
@@ -113,7 +133,7 @@ async function getDocsForSlug(
|
|
|
113
133
|
});
|
|
114
134
|
}
|
|
115
135
|
|
|
116
|
-
|
|
136
|
+
const doc = {
|
|
117
137
|
content: result.content,
|
|
118
138
|
compiledSource: result.compiledSource,
|
|
119
139
|
frontmatter,
|
|
@@ -121,6 +141,8 @@ async function getDocsForSlug(
|
|
|
121
141
|
filePath: relPath,
|
|
122
142
|
resolvedContent: content,
|
|
123
143
|
};
|
|
144
|
+
docsCache.set(relPath, { mtimeMs, doc });
|
|
145
|
+
return doc;
|
|
124
146
|
}
|
|
125
147
|
|
|
126
148
|
async function renderDocsServerPage(
|
|
@@ -129,12 +151,8 @@ async function renderDocsServerPage(
|
|
|
129
151
|
pathname: string,
|
|
130
152
|
state: ServerState
|
|
131
153
|
): Promise<Response> {
|
|
132
|
-
const title =
|
|
133
|
-
|
|
134
|
-
slug.join("/") ||
|
|
135
|
-
"Docs";
|
|
136
|
-
const description =
|
|
137
|
-
typeof doc.frontmatter.description === "string" ? doc.frontmatter.description : "";
|
|
154
|
+
const title = frontmatterField(doc.frontmatter, "title") || slug.join("/") || "Docs";
|
|
155
|
+
const description = frontmatterField(doc.frontmatter, "description");
|
|
138
156
|
|
|
139
157
|
const page = React.createElement(
|
|
140
158
|
DocsLayout,
|
|
@@ -143,8 +161,9 @@ async function renderDocsServerPage(
|
|
|
143
161
|
slug,
|
|
144
162
|
title,
|
|
145
163
|
description,
|
|
146
|
-
date: doc.frontmatter
|
|
147
|
-
|
|
164
|
+
date: frontmatterField(doc.frontmatter, "date") || undefined,
|
|
165
|
+
// Same root-relative SSR as build — see build.ts.
|
|
166
|
+
content: renderToString(doc.content),
|
|
148
167
|
tocs: doc.tocs,
|
|
149
168
|
filePath: doc.filePath,
|
|
150
169
|
repoUrl: state.docuConfig.repo?.url,
|
|
@@ -184,7 +203,7 @@ async function renderDocsServerPage(
|
|
|
184
203
|
depth,
|
|
185
204
|
});
|
|
186
205
|
html = await state.builder.runTransformHtmlChain(html, ctx);
|
|
187
|
-
/**
|
|
206
|
+
/** Dev-only: serves compiledSource → MDXRemote eval path (no CSP in production). */
|
|
188
207
|
return htmlResponse(html, nonce, 200, true);
|
|
189
208
|
}
|
|
190
209
|
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Runtime-neutral dev server — mirror of `server.ts` (Bun-only, protected)
|
|
3
|
-
* driven by a `RuntimeAdapter`
|
|
3
|
+
* driven by a `RuntimeAdapter` (./runtime) instead of `Bun.serve`,
|
|
4
4
|
* with manual route matching instead of `Bun.FileSystemRouter`. The page set
|
|
5
5
|
* is static (`/`, `/docs/[[...slug]]`, `/404`), so a router is unnecessary.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { watch } from "node:fs";
|
|
9
|
-
import
|
|
8
|
+
import { watch, existsSync } from "node:fs";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
import { createRequire } from "node:module";
|
|
11
|
+
import type { RuntimeAdapter, ServerHandle } from "./runtime";
|
|
10
12
|
import { DOCS_DIR, loadDocuConfig } from "./paths";
|
|
11
13
|
import { loadPlugins } from "./plugin-loader";
|
|
12
14
|
import { BuildPluginBuilder } from "./plugin-builder";
|
|
@@ -72,6 +74,18 @@ export async function runServer(adapter: RuntimeAdapter): Promise<ServerHandle>
|
|
|
72
74
|
|
|
73
75
|
const hmrClients = new Set<ReadableStreamDefaultController>();
|
|
74
76
|
|
|
77
|
+
const require = createRequire(import.meta.url);
|
|
78
|
+
/** Locate the installed @docubook/markdown source dir (workspace link), if any. */
|
|
79
|
+
function resolveMdxContentSrc(): string | null {
|
|
80
|
+
try {
|
|
81
|
+
const pkgDir = dirname(require.resolve("@docubook/markdown/package.json"));
|
|
82
|
+
const src = join(pkgDir, "src");
|
|
83
|
+
return existsSync(src) ? src : null;
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
75
89
|
let hmrTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
76
90
|
const watcher = watch(DOCS_DIR, { recursive: true }, (_event, filename) => {
|
|
77
91
|
if (!filename || (!filename.endsWith(".mdx") && !filename.endsWith(".md"))) return;
|
|
@@ -87,12 +101,44 @@ export async function runServer(adapter: RuntimeAdapter): Promise<ServerHandle>
|
|
|
87
101
|
}, 300);
|
|
88
102
|
});
|
|
89
103
|
|
|
104
|
+
// Rebuild the client bundle when component sources change (e.g. the
|
|
105
|
+
// @docubook/markdown workspace package) so interactive islands pick up
|
|
106
|
+
// edits, then reload connected clients. MDX edits are handled by the watcher
|
|
107
|
+
// above — no rebuild needed, content recompiles per request.
|
|
108
|
+
let srcHmrTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
109
|
+
const mdxContentSrc = resolveMdxContentSrc();
|
|
110
|
+
const srcWatcher = mdxContentSrc
|
|
111
|
+
? watch(mdxContentSrc, { recursive: true }, (_event, filename) => {
|
|
112
|
+
if (!filename || !/\.(ts|tsx|js|jsx|css)$/.test(filename)) return;
|
|
113
|
+
if (srcHmrTimeout) clearTimeout(srcHmrTimeout);
|
|
114
|
+
srcHmrTimeout = setTimeout(async () => {
|
|
115
|
+
try {
|
|
116
|
+
state.assetManifest = await buildClientBundle();
|
|
117
|
+
logger.warn("[hmr] client bundle rebuilt (component change)");
|
|
118
|
+
} catch (e) {
|
|
119
|
+
logger.warn(
|
|
120
|
+
`[hmr] bundle rebuild failed: ${e instanceof Error ? e.message : String(e)}`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
for (const client of [...hmrClients]) {
|
|
124
|
+
try {
|
|
125
|
+
client.enqueue(new TextEncoder().encode("data: reload\n\n"));
|
|
126
|
+
} catch {
|
|
127
|
+
hmrClients.delete(client);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}, 300);
|
|
131
|
+
})
|
|
132
|
+
: null;
|
|
133
|
+
|
|
90
134
|
process.on("SIGINT", () => {
|
|
91
135
|
watcher.close();
|
|
136
|
+
srcWatcher?.close();
|
|
92
137
|
process.exit(0);
|
|
93
138
|
});
|
|
94
139
|
process.on("SIGTERM", () => {
|
|
95
140
|
watcher.close();
|
|
141
|
+
srcWatcher?.close();
|
|
96
142
|
process.exit(0);
|
|
97
143
|
});
|
|
98
144
|
|
package/.docu/node/server.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { watch } from "node:fs";
|
|
1
|
+
import { watch, existsSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
2
4
|
import { DOCS_DIR, PAGES_DIR, loadDocuConfig } from "./paths";
|
|
3
5
|
import { loadPlugins } from "./plugin-loader";
|
|
4
6
|
import { BuildPluginBuilder } from "./plugin-builder";
|
|
@@ -73,6 +75,18 @@ try {
|
|
|
73
75
|
|
|
74
76
|
const hmrClients = new Set<ReadableStreamDefaultController>();
|
|
75
77
|
|
|
78
|
+
const require = createRequire(import.meta.url);
|
|
79
|
+
/** Locate the installed @docubook/markdown source dir (workspace link), if any. */
|
|
80
|
+
function resolveMdxContentSrc(): string | null {
|
|
81
|
+
try {
|
|
82
|
+
const pkgDir = dirname(require.resolve("@docubook/markdown/package.json"));
|
|
83
|
+
const src = join(pkgDir, "src");
|
|
84
|
+
return existsSync(src) ? src : null;
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
76
90
|
let hmrTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
77
91
|
const watcher = watch(DOCS_DIR, { recursive: true }, (_event, filename) => {
|
|
78
92
|
if (!filename || (!filename.endsWith(".mdx") && !filename.endsWith(".md"))) return;
|
|
@@ -88,12 +102,42 @@ const watcher = watch(DOCS_DIR, { recursive: true }, (_event, filename) => {
|
|
|
88
102
|
}, 300);
|
|
89
103
|
});
|
|
90
104
|
|
|
105
|
+
// Rebuild the client bundle when component sources change (e.g. the
|
|
106
|
+
// @docubook/markdown workspace package) so interactive islands pick up
|
|
107
|
+
// edits, then reload connected clients. MDX edits are handled by the watcher
|
|
108
|
+
// above — no rebuild needed, content recompiles per request.
|
|
109
|
+
let srcHmrTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
110
|
+
const mdxContentSrc = resolveMdxContentSrc();
|
|
111
|
+
const srcWatcher = mdxContentSrc
|
|
112
|
+
? watch(mdxContentSrc, { recursive: true }, (_event, filename) => {
|
|
113
|
+
if (!filename || !/\.(ts|tsx|js|jsx|css)$/.test(filename)) return;
|
|
114
|
+
if (srcHmrTimeout) clearTimeout(srcHmrTimeout);
|
|
115
|
+
srcHmrTimeout = setTimeout(async () => {
|
|
116
|
+
try {
|
|
117
|
+
state.assetManifest = await buildClientBundle();
|
|
118
|
+
logger.warn("[hmr] client bundle rebuilt (component change)");
|
|
119
|
+
} catch (e) {
|
|
120
|
+
logger.warn(`[hmr] bundle rebuild failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
121
|
+
}
|
|
122
|
+
for (const client of [...hmrClients]) {
|
|
123
|
+
try {
|
|
124
|
+
client.enqueue(new TextEncoder().encode("data: reload\n\n"));
|
|
125
|
+
} catch {
|
|
126
|
+
hmrClients.delete(client);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}, 300);
|
|
130
|
+
})
|
|
131
|
+
: null;
|
|
132
|
+
|
|
91
133
|
process.on("SIGINT", () => {
|
|
92
134
|
watcher.close();
|
|
135
|
+
srcWatcher?.close();
|
|
93
136
|
process.exit(0);
|
|
94
137
|
});
|
|
95
138
|
process.on("SIGTERM", () => {
|
|
96
139
|
watcher.close();
|
|
140
|
+
srcWatcher?.close();
|
|
97
141
|
process.exit(0);
|
|
98
142
|
});
|
|
99
143
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { ReactNode } from "react";
|
|
2
1
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
3
2
|
import DocsBreadcrumb from "../../components/Breadcrumb";
|
|
4
3
|
import Pagination from "../../components/Pagination";
|
|
@@ -14,10 +13,15 @@ interface DocsPageProps {
|
|
|
14
13
|
title: string;
|
|
15
14
|
description?: string;
|
|
16
15
|
date?: string;
|
|
17
|
-
content
|
|
16
|
+
/** SSR'd MDX content HTML — rendered as its own root so client hydration
|
|
17
|
+
* (separate island root) matches useId-based ids. */
|
|
18
|
+
content: string;
|
|
18
19
|
tocs: TocItem[];
|
|
19
20
|
filePath: string;
|
|
20
21
|
repoUrl?: string;
|
|
22
|
+
/** Build-time slug keying into the bundled mdxModules manifest (client.ts). */
|
|
23
|
+
mdxSlug?: string;
|
|
24
|
+
/** Dev-only pre-compiled source for the legacy MDXRemote eval path. */
|
|
21
25
|
compiledSource?: string;
|
|
22
26
|
}
|
|
23
27
|
|
|
@@ -30,6 +34,7 @@ export default function DocsPage({
|
|
|
30
34
|
tocs,
|
|
31
35
|
filePath,
|
|
32
36
|
repoUrl,
|
|
37
|
+
mdxSlug,
|
|
33
38
|
compiledSource,
|
|
34
39
|
}: DocsPageProps) {
|
|
35
40
|
const pathname = slug.join("/");
|
|
@@ -39,7 +44,7 @@ export default function DocsPage({
|
|
|
39
44
|
<div className="flex w-full flex-1 px-0 pb-4 lg:h-[calc(100vh-4rem)] lg:px-8 lg:pb-8">
|
|
40
45
|
<div
|
|
41
46
|
id="scroll-container"
|
|
42
|
-
className="bg-base-100 border-base-300
|
|
47
|
+
className="bg-base-100 border-base-300 relative flex w-full flex-col items-start rounded-b-3xl border shadow-md max-lg:scroll-p-54 lg:h-full lg:flex-row lg:overflow-y-auto lg:rounded-xl"
|
|
43
48
|
>
|
|
44
49
|
{/* Mobile bar - island */}
|
|
45
50
|
<div
|
|
@@ -60,7 +65,11 @@ export default function DocsPage({
|
|
|
60
65
|
{description && (
|
|
61
66
|
<p className="text-muted-foreground -mt-4 text-[16.5px]">{description}</p>
|
|
62
67
|
)}
|
|
63
|
-
<div
|
|
68
|
+
<div
|
|
69
|
+
id="mdx-content-island"
|
|
70
|
+
data-mdx-slug={mdxSlug}
|
|
71
|
+
dangerouslySetInnerHTML={{ __html: content }}
|
|
72
|
+
/>
|
|
64
73
|
{compiledSource && (
|
|
65
74
|
<script
|
|
66
75
|
id="mdx-compiled-source"
|
package/.docu/pages/index.tsx
CHANGED
|
@@ -61,7 +61,7 @@ export default function IndexPage() {
|
|
|
61
61
|
|
|
62
62
|
return (
|
|
63
63
|
<div className="bg-base-100 relative isolate min-h-screen overflow-hidden">
|
|
64
|
-
<div className="absolute
|
|
64
|
+
<div className="absolute top-4 right-4 z-10" id="theme-island" />
|
|
65
65
|
|
|
66
66
|
{/* Background gradient blobs */}
|
|
67
67
|
<div
|
package/.docu/styles/globals.css
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
@import "tailwindcss";
|
|
2
|
-
@import "@docubook/
|
|
2
|
+
@import "@docubook/markdown/styles.css";
|
|
3
3
|
|
|
4
4
|
@plugin "daisyui" {
|
|
5
5
|
themes:
|
|
@@ -9,16 +9,31 @@
|
|
|
9
9
|
@plugin "@tailwindcss/typography";
|
|
10
10
|
@source "../../.docu/components";
|
|
11
11
|
@source "../../.docu/pages";
|
|
12
|
-
@source
|
|
12
|
+
@source "../../../ui-react/src";
|
|
13
|
+
|
|
14
|
+
/* daisyUI breadcrumbs underline every li child on hover (including plain
|
|
15
|
+
spans) — crumbs are not links, so strip the underline (and pointer cursor)
|
|
16
|
+
for non-anchor items; real anchors keep daisyUI's hover feedback. */
|
|
17
|
+
.breadcrumbs > ul > li > :not(a):hover {
|
|
18
|
+
text-decoration-line: none;
|
|
19
|
+
cursor: default;
|
|
20
|
+
}
|
|
21
|
+
@source inline(
|
|
22
|
+
"breadcrumbs collapse collapse-open collapse-close collapse-arrow collapse-plus collapse-title collapse-content"
|
|
23
|
+
);
|
|
13
24
|
@source inline("modal modal-top modal-middle modal-bottom modal-box modal-backdrop");
|
|
14
25
|
@source inline("drawer drawer-end drawer-toggle drawer-side drawer-overlay drawer-content");
|
|
15
26
|
@source inline("sm:drawer-open md:drawer-open lg:drawer-open xl:drawer-open");
|
|
16
27
|
@source inline("navbar menu menu-horizontal");
|
|
17
28
|
@source inline("kbd kbd-xs kbd-sm kbd-md kbd-lg kbd-xl");
|
|
18
29
|
@source inline("toggle theme-controller toggle-xs toggle-sm toggle-md toggle-lg");
|
|
19
|
-
@source inline(
|
|
30
|
+
@source inline(
|
|
31
|
+
"toggle-primary toggle-secondary toggle-accent toggle-neutral toggle-success toggle-warning toggle-info toggle-error"
|
|
32
|
+
);
|
|
20
33
|
@source inline("input input-ghost input-xs input-sm input-md input-lg input-xl");
|
|
21
|
-
@source inline(
|
|
34
|
+
@source inline(
|
|
35
|
+
"input-primary input-secondary input-accent input-neutral input-success input-warning input-info input-error"
|
|
36
|
+
);
|
|
22
37
|
@source inline("label");
|
|
23
38
|
|
|
24
39
|
@custom-variant dark (&:is(.dark *));
|
|
@@ -43,6 +58,9 @@
|
|
|
43
58
|
--color-muted-foreground: hsl(var(--muted-foreground));
|
|
44
59
|
--color-accent: hsl(var(--accent));
|
|
45
60
|
--color-accent-foreground: hsl(var(--accent-foreground));
|
|
61
|
+
--color-primary-content: hsl(var(--primary-foreground));
|
|
62
|
+
--color-secondary-content: hsl(var(--secondary-foreground));
|
|
63
|
+
--color-accent-content: hsl(var(--accent-foreground));
|
|
46
64
|
--color-popover: hsl(var(--popover));
|
|
47
65
|
--color-popover-foreground: hsl(var(--popover-foreground));
|
|
48
66
|
--color-card: hsl(var(--card));
|
|
@@ -76,27 +94,27 @@
|
|
|
76
94
|
@layer base {
|
|
77
95
|
:root {
|
|
78
96
|
--base-100: 100% 0 0;
|
|
79
|
-
--base-200:
|
|
80
|
-
--base-300:
|
|
81
|
-
--base-content:
|
|
82
|
-
--background:
|
|
97
|
+
--base-200: 97% 0.002 260;
|
|
98
|
+
--base-300: 92% 0.004 260;
|
|
99
|
+
--base-content: 22% 0.03 260;
|
|
100
|
+
--background: 0 0% 100%;
|
|
83
101
|
--foreground: 220 30% 15%;
|
|
84
102
|
--card: 0 0% 100%;
|
|
85
103
|
--card-foreground: 220 30% 15%;
|
|
86
104
|
--popover: 0 0% 100%;
|
|
87
105
|
--popover-foreground: 220 30% 15%;
|
|
88
106
|
--primary: 210 81% 56%;
|
|
89
|
-
--primary-foreground:
|
|
90
|
-
--secondary: 210
|
|
107
|
+
--primary-foreground: 210 40% 98%;
|
|
108
|
+
--secondary: 210 20% 94%;
|
|
91
109
|
--secondary-foreground: 220 30% 15%;
|
|
92
|
-
--muted: 210 20%
|
|
93
|
-
--muted-foreground: 220 15%
|
|
110
|
+
--muted: 210 20% 94%;
|
|
111
|
+
--muted-foreground: 220 15% 38%;
|
|
94
112
|
--accent: 200 100% 40%;
|
|
95
|
-
--accent-foreground:
|
|
113
|
+
--accent-foreground: 200 40% 98%;
|
|
96
114
|
--destructive: 0 85% 60%;
|
|
97
|
-
--destructive-foreground:
|
|
98
|
-
--border-color: 210 20%
|
|
99
|
-
--input: 210 20%
|
|
115
|
+
--destructive-foreground: 210 40% 98%;
|
|
116
|
+
--border-color: 210 20% 88%;
|
|
117
|
+
--input: 210 20% 88%;
|
|
100
118
|
--ring: 210 81% 56%;
|
|
101
119
|
--radius: 0.5rem;
|
|
102
120
|
}
|
|
@@ -113,15 +131,15 @@
|
|
|
113
131
|
--popover: 225 18% 14%;
|
|
114
132
|
--popover-foreground: 210 20% 93%;
|
|
115
133
|
--primary: 210 100% 67%;
|
|
116
|
-
--primary-foreground:
|
|
134
|
+
--primary-foreground: 210 0% 10%;
|
|
117
135
|
--secondary: 220 15% 18%;
|
|
118
136
|
--secondary-foreground: 210 20% 90%;
|
|
119
137
|
--muted: 220 12% 20%;
|
|
120
138
|
--muted-foreground: 215 12% 58%;
|
|
121
139
|
--accent: 200 100% 62%;
|
|
122
|
-
--accent-foreground:
|
|
140
|
+
--accent-foreground: 200 0% 10%;
|
|
123
141
|
--destructive: 0 80% 65%;
|
|
124
|
-
--destructive-foreground: 0 0%
|
|
142
|
+
--destructive-foreground: 0 0% 10%;
|
|
125
143
|
--border-color: 220 10% 28%;
|
|
126
144
|
--input: 220 10% 28%;
|
|
127
145
|
--ring: 210 100% 67%;
|
|
@@ -195,7 +213,7 @@
|
|
|
195
213
|
}
|
|
196
214
|
|
|
197
215
|
.rehype-code-title {
|
|
198
|
-
@apply -mb-8
|
|
216
|
+
@apply mt-5 -mb-8 w-full px-2 pb-5 text-sm font-medium;
|
|
199
217
|
}
|
|
200
218
|
|
|
201
219
|
.highlight-comp > code {
|