@docubook/flame 1.2.1 → 1.3.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 +2 -1
- package/.docu/node/build.ts +166 -101
- package/.docu/node/html.ts +52 -3
- package/.docu/node/mdx.ts +21 -3
- package/.docu/node/plugin-builder.ts +465 -0
- package/.docu/node/plugin-loader.ts +119 -0
- package/.docu/node/plugin.ts +265 -0
- package/.docu/node/search-indexer.ts +2 -24
- package/.docu/node/security.ts +44 -3
- package/.docu/node/server-routes.ts +266 -0
- package/.docu/node/server.ts +53 -226
- package/.docu/node/types.ts +3 -0
- package/.docu/node/utils.ts +58 -1
- package/.docu/pages/docs/[[...slug]].tsx +3 -1
- package/README.md +48 -0
- package/docu.schema.json +28 -0
- package/package.json +7 -7
package/.docu/node/server.ts
CHANGED
|
@@ -1,22 +1,20 @@
|
|
|
1
|
-
import { statSync } from "node:fs";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
-
import { resolve, join } from "node:path";
|
|
4
1
|
import { watch } from "node:fs";
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { compileMdx } from "./mdx";
|
|
9
|
-
import { DOCS_DIR, DIST_DIR, PAGES_DIR, PROJECT_ROOT, loadDocuConfig } from "./paths";
|
|
10
|
-
import DocsPage from "../pages/docs/[[...slug]]";
|
|
11
|
-
import NotFoundPage from "../pages/404";
|
|
12
|
-
import IndexPage from "../pages/index";
|
|
13
|
-
import { DocsLayout } from "../components/DocsLayout";
|
|
2
|
+
import { DOCS_DIR, PAGES_DIR, loadDocuConfig } from "./paths";
|
|
3
|
+
import { loadPlugins } from "./plugin-loader";
|
|
4
|
+
import { BuildPluginBuilder } from "./plugin-builder";
|
|
14
5
|
import { buildClientBundle, computeInlineThemeCss } from "./hydrate";
|
|
15
6
|
import { generateSearchIndex } from "./search-indexer";
|
|
16
7
|
import { logger } from "./logger";
|
|
17
8
|
import { initSentry, captureException } from "./sentry";
|
|
18
|
-
import {
|
|
19
|
-
|
|
9
|
+
import {
|
|
10
|
+
serveStatic,
|
|
11
|
+
handleDocsIndex,
|
|
12
|
+
handleDocsRoute,
|
|
13
|
+
handleIndex,
|
|
14
|
+
handleNotFound,
|
|
15
|
+
serverErrorResponse,
|
|
16
|
+
type ServerState,
|
|
17
|
+
} from "./server-routes";
|
|
20
18
|
|
|
21
19
|
const docuConfig = loadDocuConfig();
|
|
22
20
|
|
|
@@ -40,6 +38,23 @@ logger.indexDone(records, Math.round(performance.now() - t));
|
|
|
40
38
|
|
|
41
39
|
logger.routes();
|
|
42
40
|
|
|
41
|
+
// Plugin setup — all hooks active (onLoad, remark/rehype, frontmatter, head/body, html transform, handleRequest)
|
|
42
|
+
const hasPlugins = !!docuConfig.plugins?.length;
|
|
43
|
+
const builder = hasPlugins ? new BuildPluginBuilder(docuConfig) : null;
|
|
44
|
+
if (hasPlugins && builder) {
|
|
45
|
+
const plugins = await loadPlugins(docuConfig.plugins!);
|
|
46
|
+
for (const plugin of plugins) {
|
|
47
|
+
await plugin.setup(builder);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const state: ServerState = {
|
|
52
|
+
docuConfig,
|
|
53
|
+
assetManifest,
|
|
54
|
+
inlineThemeCss,
|
|
55
|
+
builder,
|
|
56
|
+
};
|
|
57
|
+
|
|
43
58
|
let router: InstanceType<typeof Bun.FileSystemRouter> | null = null;
|
|
44
59
|
try {
|
|
45
60
|
router = new Bun.FileSystemRouter({
|
|
@@ -76,180 +91,6 @@ process.on("SIGTERM", () => {
|
|
|
76
91
|
process.exit(0);
|
|
77
92
|
});
|
|
78
93
|
|
|
79
|
-
function createHtmlResponse(
|
|
80
|
-
title: string,
|
|
81
|
-
description: string,
|
|
82
|
-
body: string,
|
|
83
|
-
status = 200
|
|
84
|
-
): Response {
|
|
85
|
-
const nonce = generateNonce();
|
|
86
|
-
const favicon = docuConfig.meta?.favicon || "/favicon.ico";
|
|
87
|
-
const html = createHtmlShell({
|
|
88
|
-
title,
|
|
89
|
-
description,
|
|
90
|
-
body,
|
|
91
|
-
favicon,
|
|
92
|
-
css: assetManifest.css,
|
|
93
|
-
js: assetManifest.js,
|
|
94
|
-
nonce,
|
|
95
|
-
extraScripts: hmrScript(nonce),
|
|
96
|
-
themeCss: inlineThemeCss,
|
|
97
|
-
});
|
|
98
|
-
return htmlResponse(html, nonce, status, process.env.NODE_ENV !== "production");
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async function getDocsForSlug(slug: string) {
|
|
102
|
-
if (!isSlugSafe(slug, DOCS_DIR)) return null;
|
|
103
|
-
|
|
104
|
-
const paths = [
|
|
105
|
-
join(DOCS_DIR, slug, "index.mdx"),
|
|
106
|
-
join(DOCS_DIR, `${slug}.mdx`),
|
|
107
|
-
join(DOCS_DIR, slug, "index.md"),
|
|
108
|
-
join(DOCS_DIR, `${slug}.md`),
|
|
109
|
-
];
|
|
110
|
-
|
|
111
|
-
let filePath: string | null = null;
|
|
112
|
-
let raw: string | null = null;
|
|
113
|
-
for (const p of paths) {
|
|
114
|
-
if (!p.startsWith(DOCS_DIR)) continue;
|
|
115
|
-
try {
|
|
116
|
-
raw = await readFile(p, "utf-8");
|
|
117
|
-
filePath = p;
|
|
118
|
-
break;
|
|
119
|
-
} catch (err) {
|
|
120
|
-
if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
if (!filePath || !raw) return null;
|
|
124
|
-
|
|
125
|
-
const relPath = filePath.replace(PROJECT_ROOT + "/", "");
|
|
126
|
-
const result = await compileMdx(raw, relPath);
|
|
127
|
-
|
|
128
|
-
return {
|
|
129
|
-
content: result.content,
|
|
130
|
-
compiledSource: result.compiledSource,
|
|
131
|
-
frontmatter: result.frontmatter,
|
|
132
|
-
tocs: result.tocs,
|
|
133
|
-
filePath: relPath,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
async function handleDocsIndex(): Promise<Response> {
|
|
138
|
-
const doc = await getDocsForSlug("");
|
|
139
|
-
if (!doc) return renderPage(NotFoundPage, "404 - Not Found", "", 404);
|
|
140
|
-
|
|
141
|
-
const title = doc.frontmatter.title || "Docs";
|
|
142
|
-
const description = doc.frontmatter.description || "";
|
|
143
|
-
|
|
144
|
-
const page = React.createElement(
|
|
145
|
-
DocsLayout,
|
|
146
|
-
{ repoUrl: docuConfig.repo?.url, pathname: "/docs" },
|
|
147
|
-
React.createElement(DocsPage, {
|
|
148
|
-
slug: [],
|
|
149
|
-
title,
|
|
150
|
-
description,
|
|
151
|
-
date: doc.frontmatter.date,
|
|
152
|
-
content: doc.content,
|
|
153
|
-
tocs: doc.tocs,
|
|
154
|
-
filePath: doc.filePath,
|
|
155
|
-
repoUrl: docuConfig.repo?.url,
|
|
156
|
-
compiledSource: doc.compiledSource,
|
|
157
|
-
})
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
const body = renderToString(page);
|
|
161
|
-
return createHtmlResponse(title, description, body);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
async function handleDocsRoute(slug: string[]): Promise<Response> {
|
|
165
|
-
const path = slug.join("/");
|
|
166
|
-
|
|
167
|
-
const doc = await getDocsForSlug(path);
|
|
168
|
-
if (!doc) return renderPage(NotFoundPage, "404 - Not Found", "", 404);
|
|
169
|
-
|
|
170
|
-
const title = doc.frontmatter.title || path;
|
|
171
|
-
const description = doc.frontmatter.description || "";
|
|
172
|
-
|
|
173
|
-
const page = React.createElement(
|
|
174
|
-
DocsLayout,
|
|
175
|
-
{ repoUrl: docuConfig.repo?.url, pathname: `/docs/${path}` },
|
|
176
|
-
React.createElement(DocsPage, {
|
|
177
|
-
slug,
|
|
178
|
-
title,
|
|
179
|
-
description,
|
|
180
|
-
date: doc.frontmatter.date,
|
|
181
|
-
content: doc.content,
|
|
182
|
-
tocs: doc.tocs,
|
|
183
|
-
filePath: doc.filePath,
|
|
184
|
-
repoUrl: docuConfig.repo?.url,
|
|
185
|
-
compiledSource: doc.compiledSource,
|
|
186
|
-
})
|
|
187
|
-
);
|
|
188
|
-
|
|
189
|
-
const body = renderToString(page);
|
|
190
|
-
return createHtmlResponse(title, description, body);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function renderPage(
|
|
194
|
-
Component: React.ComponentType<Record<string, unknown>>,
|
|
195
|
-
title: string,
|
|
196
|
-
description: string,
|
|
197
|
-
status: number,
|
|
198
|
-
props: Record<string, unknown> = {}
|
|
199
|
-
): Response {
|
|
200
|
-
const page = React.createElement(
|
|
201
|
-
DocsLayout,
|
|
202
|
-
{ repoUrl: docuConfig.repo?.url, pathname: "/docs" },
|
|
203
|
-
React.createElement(Component, props)
|
|
204
|
-
);
|
|
205
|
-
const body = renderToString(page);
|
|
206
|
-
return createHtmlResponse(title, description, body, status);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function handleIndex(): Response {
|
|
210
|
-
const page = React.createElement(IndexPage);
|
|
211
|
-
const body = renderToString(page);
|
|
212
|
-
return createHtmlResponse(
|
|
213
|
-
docuConfig.meta?.title || "DocuBook",
|
|
214
|
-
docuConfig.meta?.description || "",
|
|
215
|
-
body
|
|
216
|
-
);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function serveStatic(pathname: string): Response | null {
|
|
220
|
-
if (!isPathSafe(pathname, DIST_DIR)) return null;
|
|
221
|
-
const decoded = decodeURIComponent(pathname);
|
|
222
|
-
const assetPath = resolve(DIST_DIR, decoded.slice(1));
|
|
223
|
-
try {
|
|
224
|
-
const s = statSync(assetPath);
|
|
225
|
-
if (s.isFile()) {
|
|
226
|
-
return new Response(Bun.file(assetPath), {
|
|
227
|
-
headers: { "Content-Type": getContentType(pathname) },
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
} catch (err) {
|
|
231
|
-
if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
if (decoded.startsWith("/docs/assets/")) {
|
|
235
|
-
const docsAsset = resolve(DOCS_DIR, "assets", decoded.replace("/docs/assets/", ""));
|
|
236
|
-
const docsAssetsDir = resolve(DOCS_DIR, "assets");
|
|
237
|
-
const docsAssetsDirSlash = docsAssetsDir.endsWith("/") ? docsAssetsDir : docsAssetsDir + "/";
|
|
238
|
-
if (docsAsset !== docsAssetsDir && !docsAsset.startsWith(docsAssetsDirSlash)) return null;
|
|
239
|
-
try {
|
|
240
|
-
const s = statSync(docsAsset);
|
|
241
|
-
if (s.isFile()) {
|
|
242
|
-
return new Response(Bun.file(docsAsset), {
|
|
243
|
-
headers: { "Content-Type": getContentType(pathname) },
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
} catch (err) {
|
|
247
|
-
if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
return null;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
94
|
const server = Bun.serve({
|
|
254
95
|
port: PORT,
|
|
255
96
|
development: true,
|
|
@@ -260,6 +101,22 @@ const server = Bun.serve({
|
|
|
260
101
|
const pathname = url.pathname;
|
|
261
102
|
const startTime = performance.now();
|
|
262
103
|
|
|
104
|
+
if (builder) {
|
|
105
|
+
const pluginResponse = await builder.runHandleRequest(req, {
|
|
106
|
+
port: server.port,
|
|
107
|
+
hostname: server.hostname,
|
|
108
|
+
});
|
|
109
|
+
if (pluginResponse) {
|
|
110
|
+
logger.request(
|
|
111
|
+
req.method,
|
|
112
|
+
pathname,
|
|
113
|
+
pluginResponse.status,
|
|
114
|
+
Math.round(performance.now() - startTime)
|
|
115
|
+
);
|
|
116
|
+
return pluginResponse;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
263
120
|
try {
|
|
264
121
|
if (pathname === "/__hmr") {
|
|
265
122
|
const stream = new ReadableStream({
|
|
@@ -296,19 +153,19 @@ const server = Bun.serve({
|
|
|
296
153
|
const slug = slugParam ? slugParam.split("/") : [];
|
|
297
154
|
|
|
298
155
|
if (slug.length === 0) {
|
|
299
|
-
response = await handleDocsIndex();
|
|
156
|
+
response = await handleDocsIndex(state);
|
|
300
157
|
} else {
|
|
301
|
-
response = await handleDocsRoute(slug);
|
|
158
|
+
response = await handleDocsRoute(slug, state);
|
|
302
159
|
}
|
|
303
160
|
} else if (routeName === "/404") {
|
|
304
|
-
response =
|
|
161
|
+
response = handleNotFound(state);
|
|
305
162
|
} else if (routeName === "/") {
|
|
306
|
-
response = handleIndex();
|
|
163
|
+
response = handleIndex(state);
|
|
307
164
|
} else {
|
|
308
|
-
response =
|
|
165
|
+
response = handleNotFound(state);
|
|
309
166
|
}
|
|
310
167
|
} else {
|
|
311
|
-
response =
|
|
168
|
+
response = handleNotFound(state);
|
|
312
169
|
}
|
|
313
170
|
|
|
314
171
|
logger.request(
|
|
@@ -320,45 +177,15 @@ const server = Bun.serve({
|
|
|
320
177
|
return response;
|
|
321
178
|
} catch (err) {
|
|
322
179
|
captureException(err, { method: req.method, pathname });
|
|
323
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
324
|
-
const stack = err instanceof Error ? err.stack || "" : "";
|
|
325
180
|
logger.request(req.method, pathname, 500, Math.round(performance.now() - startTime));
|
|
326
|
-
|
|
327
|
-
<style>body{margin:0;padding:2rem;font-family:ui-monospace,monospace;background:#1a1a2e;color:#e0e0e0}
|
|
328
|
-
h1{color:#ff6b6b}pre{background:#0d0d1a;border:1px solid #333;border-radius:8px;padding:1.5rem;overflow-x:auto;font-size:14px;line-height:1.6;white-space:pre-wrap;word-break:break-word}
|
|
329
|
-
.msg{color:#ff6b6b;font-weight:bold}</style></head><body>
|
|
330
|
-
<h1>🔥 Server Error</h1>
|
|
331
|
-
<pre><span class="msg">${Bun.escapeHTML(message)}</span>\n\n${Bun.escapeHTML(stack)}</pre></body></html>`;
|
|
332
|
-
return new Response(html, {
|
|
333
|
-
status: 500,
|
|
334
|
-
headers: {
|
|
335
|
-
"Content-Type": "text/html",
|
|
336
|
-
...SECURITY_HEADERS,
|
|
337
|
-
"Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'",
|
|
338
|
-
},
|
|
339
|
-
});
|
|
181
|
+
return serverErrorResponse(err);
|
|
340
182
|
}
|
|
341
183
|
},
|
|
342
184
|
|
|
343
185
|
error(error) {
|
|
344
186
|
console.error(error);
|
|
345
187
|
captureException(error);
|
|
346
|
-
|
|
347
|
-
const stack = Bun.escapeHTML(error?.stack || "");
|
|
348
|
-
const html = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Error</title>
|
|
349
|
-
<style>body{margin:0;padding:2rem;font-family:ui-monospace,monospace;background:#1a1a2e;color:#e0e0e0}
|
|
350
|
-
h1{color:#ff6b6b}pre{background:#0d0d1a;border:1px solid #333;border-radius:8px;padding:1.5rem;overflow-x:auto;font-size:14px;line-height:1.6;white-space:pre-wrap;word-break:break-word}
|
|
351
|
-
.msg{color:#ff6b6b;font-weight:bold}</style></head><body>
|
|
352
|
-
<h1>🔥 Server Error</h1>
|
|
353
|
-
<pre><span class="msg">${msg}</span>\n\n${stack}</pre></body></html>`;
|
|
354
|
-
return new Response(html, {
|
|
355
|
-
status: 500,
|
|
356
|
-
headers: {
|
|
357
|
-
"Content-Type": "text/html",
|
|
358
|
-
...SECURITY_HEADERS,
|
|
359
|
-
"Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'",
|
|
360
|
-
},
|
|
361
|
-
});
|
|
188
|
+
return serverErrorResponse(error);
|
|
362
189
|
},
|
|
363
190
|
});
|
|
364
191
|
|
package/.docu/node/types.ts
CHANGED
|
@@ -67,6 +67,7 @@ export interface HomeConfig {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
import type { ThemeConfig } from "@docubook/themes-colors";
|
|
70
|
+
import type { PluginEntry } from "./plugin";
|
|
70
71
|
|
|
71
72
|
export interface DocuConfig {
|
|
72
73
|
meta: DocuMeta;
|
|
@@ -78,6 +79,8 @@ export interface DocuConfig {
|
|
|
78
79
|
themes?: {
|
|
79
80
|
colors: ThemeConfig;
|
|
80
81
|
};
|
|
82
|
+
/** List of DocuBook plugins to load. Empty by default — no-op when absent. */
|
|
83
|
+
plugins?: PluginEntry[];
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
export interface BuildCache {
|
package/.docu/node/utils.ts
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
export { cn, parseDate, formatDate, formatDate2 } from "@docubook/core";
|
|
2
2
|
|
|
3
|
+
import { readdir, stat } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
export interface ScannedMdxFile {
|
|
7
|
+
path: string;
|
|
8
|
+
absPath: string;
|
|
9
|
+
mtime: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Scan a directory recursively for MDX/MD files.
|
|
14
|
+
* Skips "assets" directories, hidden directories (dot-prefixed), and root-level index.mdx.
|
|
15
|
+
* Shared between build.ts and search-indexer.ts.
|
|
16
|
+
*/
|
|
17
|
+
export async function scanMdxFiles(dir: string, baseDir = ""): Promise<ScannedMdxFile[]> {
|
|
18
|
+
const files: ScannedMdxFile[] = [];
|
|
19
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
20
|
+
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
const fullPath = join(dir, entry.name);
|
|
23
|
+
const relativePath = baseDir ? `${baseDir}/${entry.name}` : entry.name;
|
|
24
|
+
|
|
25
|
+
if (entry.isDirectory()) {
|
|
26
|
+
if (entry.name === "assets" || entry.name.startsWith(".")) continue;
|
|
27
|
+
files.push(...(await scanMdxFiles(fullPath, relativePath)));
|
|
28
|
+
} else if (entry.name.endsWith(".mdx") || entry.name.endsWith(".md")) {
|
|
29
|
+
if (entry.name === "index.mdx" && !baseDir) continue;
|
|
30
|
+
const stats = await stat(fullPath);
|
|
31
|
+
let path = relativePath.replace(/\.(mdx|md)$/, "");
|
|
32
|
+
|
|
33
|
+
if (/\/index$/.test(path)) {
|
|
34
|
+
path = path.replace(/\/index$/, "");
|
|
35
|
+
}
|
|
36
|
+
files.push({ path, absPath: fullPath, mtime: stats.mtimeMs });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return files;
|
|
41
|
+
}
|
|
42
|
+
|
|
3
43
|
export function isExternalUrl(url: string): boolean {
|
|
4
44
|
return /^(https?:\/\/|\/\/)/.test(url);
|
|
5
45
|
}
|
|
@@ -40,9 +80,26 @@ export async function getGitLastModifiedBatch(filePaths: string[]): Promise<Map<
|
|
|
40
80
|
const result = new Map<string, string>();
|
|
41
81
|
if (filePaths.length === 0) return result;
|
|
42
82
|
|
|
83
|
+
// Filter and validate paths — same guard as getGitLastModified
|
|
84
|
+
const safePaths: string[] = [];
|
|
85
|
+
for (const fp of filePaths) {
|
|
86
|
+
const cleanPath = fp.replace(/^\//, "");
|
|
87
|
+
if (
|
|
88
|
+
!cleanPath ||
|
|
89
|
+
!/^[a-zA-Z0-9\-_/.\s]+$/.test(cleanPath) ||
|
|
90
|
+
/(^|\/)\.\.($|\/)/.test(cleanPath)
|
|
91
|
+
) {
|
|
92
|
+
console.warn(`[utils] getGitLastModifiedBatch: skipping invalid path "${fp}"`);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
safePaths.push(cleanPath);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (safePaths.length === 0) return result;
|
|
99
|
+
|
|
43
100
|
try {
|
|
44
101
|
const proc = Bun.spawn(
|
|
45
|
-
["git", "log", "--format=%cI", "--name-only", "--diff-filter=ACMR", ...
|
|
102
|
+
["git", "log", "--format=%cI", "--name-only", "--diff-filter=ACMR", ...safePaths],
|
|
46
103
|
{ stderr: "ignore" }
|
|
47
104
|
);
|
|
48
105
|
const text = await new Response(proc.stdout).text();
|
|
@@ -62,7 +62,9 @@ export default function DocsPage({
|
|
|
62
62
|
<script
|
|
63
63
|
id="mdx-compiled-source"
|
|
64
64
|
type="application/json"
|
|
65
|
-
dangerouslySetInnerHTML={{
|
|
65
|
+
dangerouslySetInnerHTML={{
|
|
66
|
+
__html: JSON.stringify(compiledSource).replace(/<\//g, "\\u003C/"),
|
|
67
|
+
}}
|
|
66
68
|
/>
|
|
67
69
|
)}
|
|
68
70
|
<div className="border-base-300 my-8 flex items-center border-b-2 border-dashed">
|
package/README.md
CHANGED
|
@@ -282,6 +282,54 @@ Reference in MDX:
|
|
|
282
282
|
|
|
283
283
|
---
|
|
284
284
|
|
|
285
|
+
## Plugins
|
|
286
|
+
|
|
287
|
+
Flame supports a plugin system to extend the build pipeline and dev server. Plugins can inject head/body HTML, transform content, add remark/rehype plugins, intercept API requests, and more.
|
|
288
|
+
|
|
289
|
+
### Usage in `docu.json`
|
|
290
|
+
|
|
291
|
+
```json
|
|
292
|
+
{
|
|
293
|
+
"plugins": [
|
|
294
|
+
"@docubook/plugin-sitemap",
|
|
295
|
+
["./plugins/analytics", { "id": "G-XXXXXXX" }]
|
|
296
|
+
]
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
String = npm package or relative path. Tuple `["name", opts]` = factory with options.
|
|
301
|
+
|
|
302
|
+
### Available Hooks
|
|
303
|
+
|
|
304
|
+
| Hook | Purpose |
|
|
305
|
+
| --------------------------------- | ------------------------------------------------- |
|
|
306
|
+
| `onStart` | Validate config before build |
|
|
307
|
+
| `onEnd` | Generate files after build (sitemap, RSS) |
|
|
308
|
+
| `onLoad` | Transform raw file content before MDX compilation |
|
|
309
|
+
| `transformFrontmatter` | Mutate frontmatter (reading time, SEO) |
|
|
310
|
+
| `transformHtml` | Modify final HTML before writing to disk |
|
|
311
|
+
| `injectHead` / `injectBody` | Inject HTML into `<head>` or before `</body>` |
|
|
312
|
+
| `remarkPlugins` / `rehypePlugins` | Add remark/rehype plugins to the MDX pipeline |
|
|
313
|
+
| `handleRequest` | Intercept dev server requests (custom API) |
|
|
314
|
+
|
|
315
|
+
### Quick Example
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
import type { DocuBookPlugin } from "@docubook/flame";
|
|
319
|
+
|
|
320
|
+
export default {
|
|
321
|
+
name: "reading-time",
|
|
322
|
+
setup(build) {
|
|
323
|
+
build.transformFrontmatter((fm, ctx) => ({
|
|
324
|
+
...fm,
|
|
325
|
+
readingTime: `${Math.ceil((ctx.content ?? "").split(/\s+/).length / 200)} min read`,
|
|
326
|
+
}));
|
|
327
|
+
},
|
|
328
|
+
} satisfies DocuBookPlugin;
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
See the [full plugin guide](docs/getting-started/plugins.mdx) for step-by-step instructions.
|
|
332
|
+
|
|
285
333
|
## Architecture
|
|
286
334
|
|
|
287
335
|
- **Bun** — runtime, bundler, file watcher
|
package/docu.schema.json
CHANGED
|
@@ -118,6 +118,34 @@
|
|
|
118
118
|
"description": "Documentation navigation routes. Leave empty for auto-detection from docs/ folder.",
|
|
119
119
|
"items": { "$ref": "#/$defs/route" }
|
|
120
120
|
},
|
|
121
|
+
"plugins": {
|
|
122
|
+
"type": "array",
|
|
123
|
+
"description": "List of DocuBook plugins to load",
|
|
124
|
+
"items": {
|
|
125
|
+
"oneOf": [
|
|
126
|
+
{
|
|
127
|
+
"type": "string",
|
|
128
|
+
"description": "Plugin package name or relative path (e.g. @docubook/plugin-sitemap)"
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"type": "array",
|
|
132
|
+
"description": "Plugin package name with factory options",
|
|
133
|
+
"items": [
|
|
134
|
+
{
|
|
135
|
+
"type": "string",
|
|
136
|
+
"description": "Plugin package name"
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"type": "object",
|
|
140
|
+
"description": "Plugin factory options"
|
|
141
|
+
}
|
|
142
|
+
],
|
|
143
|
+
"minItems": 2,
|
|
144
|
+
"maxItems": 2
|
|
145
|
+
}
|
|
146
|
+
]
|
|
147
|
+
}
|
|
148
|
+
},
|
|
121
149
|
"themes": {
|
|
122
150
|
"type": "object",
|
|
123
151
|
"description": "Color themes configuration. Use preset name or custom hex values.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@docubook/flame",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "A blazing-fast React + MDX framework powered by Bun, built for modern documentation experiences.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"react",
|
|
35
35
|
"react-dom"
|
|
36
36
|
],
|
|
37
|
-
"homepage": "https://
|
|
37
|
+
"homepage": "https://github.com/DocuBook/docubook/tree/main/packages/flame",
|
|
38
38
|
"repository": {
|
|
39
39
|
"type": "git",
|
|
40
40
|
"url": "git+https://github.com/DocuBook/docubook.git",
|
|
@@ -51,13 +51,13 @@
|
|
|
51
51
|
"lucide-react": "^1.14.0",
|
|
52
52
|
"react": "^19.0.0",
|
|
53
53
|
"react-dom": "^19.0.0",
|
|
54
|
-
"@docubook/mdx-content": "^3.2.
|
|
55
|
-
"@docubook/core": "^1.7.
|
|
56
|
-
"@docubook/themes-colors": "^0.10.
|
|
57
|
-
"@docubook/ui-react": "^0.1.
|
|
54
|
+
"@docubook/mdx-content": "^3.2.2",
|
|
55
|
+
"@docubook/core": "^1.7.2",
|
|
56
|
+
"@docubook/themes-colors": "^0.10.2",
|
|
57
|
+
"@docubook/ui-react": "^0.1.4"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
|
-
"@sentry/bun": "^
|
|
60
|
+
"@sentry/bun": "^10.0.0"
|
|
61
61
|
},
|
|
62
62
|
"peerDependenciesMeta": {
|
|
63
63
|
"@sentry/bun": {
|