@docubook/flame 1.7.2 → 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.
- package/.docu/components/Context.tsx +1 -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 +51 -5
- 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-WR24HDUT.js} +3 -3
- package/.docu/lib/build.node.js +3 -3
- package/.docu/lib/{chunk-HRO7ONJQ.js → chunk-2EYRWRDP.js} +66 -26
- package/.docu/lib/{chunk-PJIJEPNR.js → chunk-AJV2GEDF.js} +5 -4
- package/.docu/lib/{chunk-KMDGSD57.js → chunk-KEWRVASF.js} +3 -3
- package/.docu/lib/{chunk-B6LGUADD.js → chunk-QPF4DPZQ.js} +294 -177
- package/.docu/lib/chunk-UISOJ4RW.js +114 -0
- package/.docu/lib/{chunk-DITXUPUV.js → chunk-VPV7KP7K.js} +56 -9
- package/.docu/lib/{chunk-SPHVBXRR.js → chunk-X7KPVCDN.js} +6 -6
- 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 +72 -26
- package/.docu/node/build.ts +67 -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/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/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 +28 -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 +29 -20
- package/README.md +7 -48
- package/bin/cli.js +25 -7
- package/package.json +7 -6
- package/template/README.md +7 -48
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { existsSync, statSync, readFileSync } from "node:fs";
|
|
7
7
|
import { readFile } from "node:fs/promises";
|
|
8
8
|
import { resolve } from "node:path";
|
|
9
|
-
import type { RuntimeAdapter, ServerHandle } from "
|
|
9
|
+
import type { RuntimeAdapter, ServerHandle } from "./runtime";
|
|
10
10
|
import { logger } from "./logger";
|
|
11
11
|
import { DIST_DIR } from "./paths";
|
|
12
12
|
import { getContentType } from "./utils";
|
|
@@ -63,7 +63,7 @@ export async function runPreview(adapter: RuntimeAdapter): Promise<ServerHandle
|
|
|
63
63
|
headers: {
|
|
64
64
|
"Content-Type": "text/html",
|
|
65
65
|
...SECURITY_HEADERS,
|
|
66
|
-
"Content-Security-Policy": cspHeader(nonce
|
|
66
|
+
"Content-Security-Policy": cspHeader(nonce),
|
|
67
67
|
},
|
|
68
68
|
});
|
|
69
69
|
}
|
|
@@ -81,7 +81,7 @@ export async function runPreview(adapter: RuntimeAdapter): Promise<ServerHandle
|
|
|
81
81
|
headers: {
|
|
82
82
|
"Content-Type": "text/html",
|
|
83
83
|
...SECURITY_HEADERS,
|
|
84
|
-
"Content-Security-Policy": cspHeader(nonce
|
|
84
|
+
"Content-Security-Policy": cspHeader(nonce),
|
|
85
85
|
},
|
|
86
86
|
});
|
|
87
87
|
}
|
package/.docu/node/preview.ts
CHANGED
|
@@ -57,7 +57,7 @@ const server = Bun.serve({
|
|
|
57
57
|
headers: {
|
|
58
58
|
"Content-Type": "text/html",
|
|
59
59
|
...SECURITY_HEADERS,
|
|
60
|
-
"Content-Security-Policy": cspHeader(nonce
|
|
60
|
+
"Content-Security-Policy": cspHeader(nonce),
|
|
61
61
|
},
|
|
62
62
|
});
|
|
63
63
|
}
|
|
@@ -75,7 +75,7 @@ const server = Bun.serve({
|
|
|
75
75
|
headers: {
|
|
76
76
|
"Content-Type": "text/html",
|
|
77
77
|
...SECURITY_HEADERS,
|
|
78
|
-
"Content-Security-Policy": cspHeader(nonce
|
|
78
|
+
"Content-Security-Policy": cspHeader(nonce),
|
|
79
79
|
},
|
|
80
80
|
});
|
|
81
81
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { FetchHandler, RuntimeAdapter, ServerOptions } from "./types";
|
|
2
|
+
|
|
3
|
+
/** Minimal ambient view of the Bun global — only what the adapter touches. */
|
|
4
|
+
declare const Bun: {
|
|
5
|
+
serve(options: { port: number; hostname?: string; idleTimeout?: number; fetch: FetchHandler }): {
|
|
6
|
+
port?: number;
|
|
7
|
+
hostname?: string;
|
|
8
|
+
stop(closeActiveConnections?: boolean): void | Promise<void>;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const bunAdapter: RuntimeAdapter = {
|
|
13
|
+
name: "bun",
|
|
14
|
+
|
|
15
|
+
serve(fetch: FetchHandler, options: ServerOptions) {
|
|
16
|
+
const server = Bun.serve({
|
|
17
|
+
port: options.port,
|
|
18
|
+
hostname: options.hostname,
|
|
19
|
+
idleTimeout: options.idleTimeout,
|
|
20
|
+
fetch,
|
|
21
|
+
});
|
|
22
|
+
return {
|
|
23
|
+
port: server.port ?? options.port,
|
|
24
|
+
hostname: server.hostname ?? options.hostname ?? "localhost",
|
|
25
|
+
stop: () => server.stop(),
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { FetchHandler, RuntimeAdapter, ServerOptions } from "./types";
|
|
2
|
+
|
|
3
|
+
/** Minimal ambient view of the Deno global — only what the adapter touches. */
|
|
4
|
+
declare const Deno: {
|
|
5
|
+
serve(
|
|
6
|
+
options: { port?: number; hostname?: string; onListen?: (addr: unknown) => void },
|
|
7
|
+
handler: (req: Request) => Response | Promise<Response>
|
|
8
|
+
): {
|
|
9
|
+
addr: { port: number; hostname: string };
|
|
10
|
+
shutdown(): Promise<void>;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const denoAdapter: RuntimeAdapter = {
|
|
15
|
+
name: "deno",
|
|
16
|
+
|
|
17
|
+
serve(fetch: FetchHandler, options: ServerOptions) {
|
|
18
|
+
const server = Deno.serve(
|
|
19
|
+
{
|
|
20
|
+
port: options.port,
|
|
21
|
+
hostname: options.hostname,
|
|
22
|
+
onListen: () => {
|
|
23
|
+
// Suppress Deno's default "Listening on ..." log; callers print their own.
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
fetch
|
|
27
|
+
);
|
|
28
|
+
return {
|
|
29
|
+
port: server.addr.port,
|
|
30
|
+
hostname: server.addr.hostname,
|
|
31
|
+
stop: () => server.shutdown(),
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -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";
|
|
@@ -45,10 +45,20 @@ function createHtmlResponse(
|
|
|
45
45
|
themeCss: state.inlineThemeCss,
|
|
46
46
|
depth,
|
|
47
47
|
});
|
|
48
|
-
/**
|
|
48
|
+
/** Dev-only: serves compiledSource → MDXRemote eval path (no CSP in production). */
|
|
49
49
|
return htmlResponse(html, nonce, status, true);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
// Dev-only memo: compileMdx per request is ~70ms; repeat navigations with
|
|
53
|
+
// an unchanged file (same mtime) reuse the compiled result. The dev server
|
|
54
|
+
// has no other per-page cache — without this every navigation re-parses MDX.
|
|
55
|
+
// ponytail: unbounded Map is fine — docs sites have a handful of pages; cap
|
|
56
|
+
// with an LRU only if a project exceeds thousands of routes.
|
|
57
|
+
const docsCache = new Map<
|
|
58
|
+
string,
|
|
59
|
+
{ mtimeMs: number; doc: NonNullable<Awaited<ReturnType<typeof getDocsForSlug>>> }
|
|
60
|
+
>();
|
|
61
|
+
|
|
52
62
|
async function getDocsForSlug(
|
|
53
63
|
slug: string,
|
|
54
64
|
state: ServerState
|
|
@@ -92,6 +102,12 @@ async function getDocsForSlug(
|
|
|
92
102
|
|
|
93
103
|
const relPath = filePath.replace(PROJECT_ROOT + "/", "");
|
|
94
104
|
|
|
105
|
+
const mtimeMs = (await stat(filePath)).mtimeMs;
|
|
106
|
+
const cached = docsCache.get(relPath);
|
|
107
|
+
if (cached && cached.mtimeMs === mtimeMs) {
|
|
108
|
+
return cached.doc;
|
|
109
|
+
}
|
|
110
|
+
|
|
95
111
|
let content = raw;
|
|
96
112
|
if (state.builder) {
|
|
97
113
|
const transformed = await state.builder.runOnLoad(relPath, content);
|
|
@@ -113,7 +129,7 @@ async function getDocsForSlug(
|
|
|
113
129
|
});
|
|
114
130
|
}
|
|
115
131
|
|
|
116
|
-
|
|
132
|
+
const doc = {
|
|
117
133
|
content: result.content,
|
|
118
134
|
compiledSource: result.compiledSource,
|
|
119
135
|
frontmatter,
|
|
@@ -121,6 +137,8 @@ async function getDocsForSlug(
|
|
|
121
137
|
filePath: relPath,
|
|
122
138
|
resolvedContent: content,
|
|
123
139
|
};
|
|
140
|
+
docsCache.set(relPath, { mtimeMs, doc });
|
|
141
|
+
return doc;
|
|
124
142
|
}
|
|
125
143
|
|
|
126
144
|
async function renderDocsServerPage(
|
|
@@ -129,12 +147,8 @@ async function renderDocsServerPage(
|
|
|
129
147
|
pathname: string,
|
|
130
148
|
state: ServerState
|
|
131
149
|
): Promise<Response> {
|
|
132
|
-
const title =
|
|
133
|
-
|
|
134
|
-
slug.join("/") ||
|
|
135
|
-
"Docs";
|
|
136
|
-
const description =
|
|
137
|
-
typeof doc.frontmatter.description === "string" ? doc.frontmatter.description : "";
|
|
150
|
+
const title = frontmatterField(doc.frontmatter, "title") || slug.join("/") || "Docs";
|
|
151
|
+
const description = frontmatterField(doc.frontmatter, "description");
|
|
138
152
|
|
|
139
153
|
const page = React.createElement(
|
|
140
154
|
DocsLayout,
|
|
@@ -143,8 +157,9 @@ async function renderDocsServerPage(
|
|
|
143
157
|
slug,
|
|
144
158
|
title,
|
|
145
159
|
description,
|
|
146
|
-
date: doc.frontmatter
|
|
147
|
-
|
|
160
|
+
date: frontmatterField(doc.frontmatter, "date") || undefined,
|
|
161
|
+
// Same root-relative SSR as build — see build.ts.
|
|
162
|
+
content: renderToString(doc.content),
|
|
148
163
|
tocs: doc.tocs,
|
|
149
164
|
filePath: doc.filePath,
|
|
150
165
|
repoUrl: state.docuConfig.repo?.url,
|
|
@@ -184,7 +199,7 @@ async function renderDocsServerPage(
|
|
|
184
199
|
depth,
|
|
185
200
|
});
|
|
186
201
|
html = await state.builder.runTransformHtmlChain(html, ctx);
|
|
187
|
-
/**
|
|
202
|
+
/** Dev-only: serves compiledSource → MDXRemote eval path (no CSP in production). */
|
|
188
203
|
return htmlResponse(html, nonce, 200, true);
|
|
189
204
|
}
|
|
190
205
|
|
|
@@ -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
|