@docubook/flame 1.2.0 → 1.3.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 +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/preview.ts +27 -16
- package/.docu/node/search-indexer.ts +2 -24
- package/.docu/node/security.ts +75 -4
- package/.docu/node/server-routes.ts +266 -0
- package/.docu/node/server.ts +53 -225
- 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 +5 -5
|
@@ -16,7 +16,8 @@ function getContextRoutes() {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
function getFirstItemHref(route: { href: string; items?: { href: string }[] }): string {
|
|
19
|
-
|
|
19
|
+
if (!route.items?.length) return route.href;
|
|
20
|
+
return `${route.href}${getFirstItemHref(route.items[0])}`;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
function getActiveContextRoute(path: string) {
|
package/.docu/node/build.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFile, writeFile, mkdir, readdir, copyFile
|
|
1
|
+
import { readFile, writeFile, mkdir, readdir, copyFile } from "node:fs/promises";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { join, dirname } from "node:path";
|
|
@@ -14,12 +14,17 @@ import {
|
|
|
14
14
|
PROJECT_ROOT,
|
|
15
15
|
loadDocuConfig,
|
|
16
16
|
} from "./paths";
|
|
17
|
-
import { htmlShell
|
|
17
|
+
import { htmlShell } from "./html";
|
|
18
18
|
import { generateSearchIndex } from "./search-indexer";
|
|
19
19
|
import { buildClientBundle, computeInlineThemeCss } from "./hydrate";
|
|
20
20
|
import { logger } from "./logger";
|
|
21
21
|
import { initSentry, captureException } from "./sentry";
|
|
22
|
+
import { loadPlugins } from "./plugin-loader";
|
|
23
|
+
import { BuildPluginBuilder } from "./plugin-builder";
|
|
24
|
+
import { scanMdxFiles } from "./utils";
|
|
22
25
|
import type { BuildCache, CliArgs } from "./types";
|
|
26
|
+
import { generateNonce } from "./security";
|
|
27
|
+
import type { PageMeta, PageContext } from "./plugin";
|
|
23
28
|
import DocsPage from "../pages/docs/[[...slug]]";
|
|
24
29
|
import IndexPage from "../pages/index";
|
|
25
30
|
import NotFoundPage from "../pages/404";
|
|
@@ -55,77 +60,60 @@ async function writeCache(cache: BuildCache): Promise<void> {
|
|
|
55
60
|
await writeFile(CACHE_FILE, JSON.stringify(cache, null, 2));
|
|
56
61
|
}
|
|
57
62
|
|
|
58
|
-
async function findMdxFiles(dir: string, baseDir = ""): Promise<{ path: string; mtime: number }[]> {
|
|
59
|
-
const files: { path: string; mtime: number }[] = [];
|
|
60
|
-
try {
|
|
61
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
62
|
-
for (const entry of entries) {
|
|
63
|
-
const fullPath = join(dir, entry.name);
|
|
64
|
-
const relativePath = baseDir ? `${baseDir}/${entry.name}` : entry.name;
|
|
65
|
-
|
|
66
|
-
if (entry.isDirectory()) {
|
|
67
|
-
if (entry.name === "assets" || entry.name.startsWith(".")) continue;
|
|
68
|
-
files.push(...(await findMdxFiles(fullPath, relativePath)));
|
|
69
|
-
} else if (entry.name.endsWith(".mdx") || entry.name.endsWith(".md")) {
|
|
70
|
-
if (entry.name === "index.mdx" && !baseDir) continue;
|
|
71
|
-
const stats = await stat(fullPath);
|
|
72
|
-
let path = relativePath.replace(/\.(mdx|md)$/, "");
|
|
73
|
-
|
|
74
|
-
if (/\/index$/.test(path)) {
|
|
75
|
-
path = path.replace(/\/index$/, "");
|
|
76
|
-
}
|
|
77
|
-
files.push({ path, mtime: stats.mtimeMs });
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
} catch (err) {
|
|
81
|
-
console.error("Failed to scan docs directory:", (err as Error).message);
|
|
82
|
-
}
|
|
83
|
-
return files;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
63
|
export function parseConcurrency(): number {
|
|
87
|
-
return Math.max(1, parseInt(process.env.BUILD_CONCURRENCY || "
|
|
64
|
+
return Math.max(1, parseInt(process.env.BUILD_CONCURRENCY || "4", 10) || 4);
|
|
88
65
|
}
|
|
89
66
|
|
|
90
|
-
|
|
67
|
+
type RebuildDecision = "yes" | "hash_check" | "no";
|
|
68
|
+
|
|
69
|
+
export function shouldRebuild(path: string, mtime: number, cache: BuildCache): RebuildDecision {
|
|
91
70
|
const cached = cache[path];
|
|
92
|
-
if (!cached) return
|
|
93
|
-
|
|
71
|
+
if (!cached) return "yes";
|
|
72
|
+
if (mtime > cached.builtAt) return "hash_check";
|
|
73
|
+
return "no";
|
|
94
74
|
}
|
|
95
75
|
|
|
96
76
|
let assetManifest = { js: "client.js", css: "client.css" };
|
|
97
77
|
|
|
98
78
|
let inlineThemeCss: string | undefined;
|
|
99
79
|
|
|
100
|
-
function htmlShell(title: string, description: string, body: string): string {
|
|
101
|
-
const favicon = docuConfig.meta?.favicon || "/favicon.ico";
|
|
102
|
-
return createHtmlShell({
|
|
103
|
-
title,
|
|
104
|
-
description,
|
|
105
|
-
body,
|
|
106
|
-
favicon,
|
|
107
|
-
css: assetManifest.css,
|
|
108
|
-
js: assetManifest.js,
|
|
109
|
-
themeCss: inlineThemeCss,
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
|
|
113
80
|
async function renderDocsPage(
|
|
114
81
|
slug: string,
|
|
115
82
|
rawMdx: string,
|
|
116
83
|
filePath: string,
|
|
117
|
-
gitDates?: Map<string, string
|
|
84
|
+
gitDates?: Map<string, string>,
|
|
85
|
+
builder?: BuildPluginBuilder,
|
|
86
|
+
nonce?: string
|
|
118
87
|
): Promise<string> {
|
|
88
|
+
let content = rawMdx;
|
|
89
|
+
if (builder) {
|
|
90
|
+
const transformed = await builder.runOnLoad(filePath, content);
|
|
91
|
+
if (transformed?.contents) {
|
|
92
|
+
content = transformed.contents;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
119
96
|
let result;
|
|
120
97
|
try {
|
|
121
|
-
|
|
98
|
+
const remarkPlugins = builder?.collectRemarkPlugins();
|
|
99
|
+
const rehypePlugins = builder?.collectRehypePlugins();
|
|
100
|
+
result = await compileMdx(content, filePath, gitDates, remarkPlugins, rehypePlugins);
|
|
122
101
|
} catch (err) {
|
|
123
102
|
const msg = err instanceof Error ? err.message : "Unknown MDX error";
|
|
124
103
|
throw new Error(`MDX Error in: docs/${slug}.mdx\n${msg}`, { cause: err });
|
|
125
104
|
}
|
|
126
105
|
|
|
127
|
-
|
|
128
|
-
|
|
106
|
+
let frontmatter = result.frontmatter as Record<string, unknown>;
|
|
107
|
+
if (builder) {
|
|
108
|
+
frontmatter = await builder.runTransformFrontmatterChain(frontmatter, {
|
|
109
|
+
slug,
|
|
110
|
+
filePath,
|
|
111
|
+
content,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const title = (frontmatter.title as string) || slug || "Docs";
|
|
116
|
+
const description = (frontmatter.description as string) || "";
|
|
129
117
|
const slugParts = slug ? slug.split("/") : [];
|
|
130
118
|
|
|
131
119
|
const page = React.createElement(
|
|
@@ -135,7 +123,7 @@ async function renderDocsPage(
|
|
|
135
123
|
slug: slugParts,
|
|
136
124
|
title,
|
|
137
125
|
description,
|
|
138
|
-
date:
|
|
126
|
+
date: (frontmatter.date as string) || undefined,
|
|
139
127
|
content: result.content,
|
|
140
128
|
tocs: result.tocs,
|
|
141
129
|
filePath,
|
|
@@ -145,7 +133,30 @@ async function renderDocsPage(
|
|
|
145
133
|
);
|
|
146
134
|
|
|
147
135
|
const body = renderToString(page);
|
|
148
|
-
|
|
136
|
+
|
|
137
|
+
const ctx: PageContext = { slug, filePath, frontmatter, content, config: docuConfig };
|
|
138
|
+
const headExtra = builder?.collectHead(ctx);
|
|
139
|
+
const bodyExtra = builder?.collectBody(ctx);
|
|
140
|
+
|
|
141
|
+
const favicon = docuConfig.meta?.favicon || "/favicon.ico";
|
|
142
|
+
let html = htmlShell({
|
|
143
|
+
title,
|
|
144
|
+
description,
|
|
145
|
+
body,
|
|
146
|
+
favicon,
|
|
147
|
+
css: assetManifest.css,
|
|
148
|
+
js: assetManifest.js,
|
|
149
|
+
nonce,
|
|
150
|
+
themeCss: inlineThemeCss,
|
|
151
|
+
headExtra,
|
|
152
|
+
bodyExtra,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (builder) {
|
|
156
|
+
html = await builder.runTransformHtmlChain(html, ctx);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return html;
|
|
149
160
|
}
|
|
150
161
|
|
|
151
162
|
async function copyDirectoryRecursive(src: string, dest: string): Promise<void> {
|
|
@@ -182,7 +193,7 @@ async function build() {
|
|
|
182
193
|
|
|
183
194
|
await copyDirectoryRecursive(DOCS_ASSETS_DIR, join(DIST_DIR, "docs", "assets"));
|
|
184
195
|
|
|
185
|
-
const mdxFiles = await
|
|
196
|
+
const mdxFiles = await scanMdxFiles(DOCS_DIR);
|
|
186
197
|
const cache = args.force ? {} : await readCache();
|
|
187
198
|
let built = 0;
|
|
188
199
|
let skipped = 0;
|
|
@@ -205,20 +216,25 @@ async function build() {
|
|
|
205
216
|
};
|
|
206
217
|
}
|
|
207
218
|
|
|
219
|
+
const hasPlugins = !!docuConfig.plugins?.length;
|
|
220
|
+
const builder = hasPlugins ? new BuildPluginBuilder(docuConfig) : null;
|
|
221
|
+
if (hasPlugins && builder) {
|
|
222
|
+
const plugins = await loadPlugins(docuConfig.plugins!);
|
|
223
|
+
for (const plugin of plugins) {
|
|
224
|
+
await plugin.setup(builder);
|
|
225
|
+
}
|
|
226
|
+
await builder.runOnStart();
|
|
227
|
+
}
|
|
228
|
+
|
|
208
229
|
logger.spinner.start("Building pages...");
|
|
209
230
|
t = performance.now();
|
|
210
231
|
|
|
211
|
-
const allRelPaths = mdxFiles
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (existsSync(p)) return p.replace(PROJECT_ROOT + "/", "");
|
|
218
|
-
}
|
|
219
|
-
return null;
|
|
220
|
-
})
|
|
221
|
-
.filter((p): p is string => p !== null);
|
|
232
|
+
const allRelPaths = mdxFiles.map((f) => f.absPath.replace(PROJECT_ROOT + "/", ""));
|
|
233
|
+
|
|
234
|
+
const indexMdxFull = join(DOCS_DIR, "index.mdx");
|
|
235
|
+
if (existsSync(indexMdxFull)) {
|
|
236
|
+
allRelPaths.push(indexMdxFull.replace(PROJECT_ROOT + "/", ""));
|
|
237
|
+
}
|
|
222
238
|
const gitDates = await getGitLastModifiedBatch(allRelPaths);
|
|
223
239
|
|
|
224
240
|
const CONCURRENCY = parseConcurrency();
|
|
@@ -226,40 +242,54 @@ async function build() {
|
|
|
226
242
|
const errors: string[] = [];
|
|
227
243
|
|
|
228
244
|
for (const file of mdxFiles) {
|
|
229
|
-
const
|
|
230
|
-
const mdxPath2 = join(DOCS_DIR, `${file.path}.mdx`);
|
|
231
|
-
const mdxPath3 = join(DOCS_DIR, `${file.path}.md`);
|
|
245
|
+
const rebuildDecision = shouldRebuild(file.path, file.mtime, cache);
|
|
232
246
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
absPath = p;
|
|
239
|
-
break;
|
|
240
|
-
} catch (err) {
|
|
241
|
-
if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
|
|
247
|
+
if (rebuildDecision === "no") {
|
|
248
|
+
const outputPath = join(DIST_DIR, "docs", `${file.path}.html`);
|
|
249
|
+
if (existsSync(outputPath) && !assetsChanged) {
|
|
250
|
+
skipped++;
|
|
251
|
+
continue;
|
|
242
252
|
}
|
|
243
253
|
}
|
|
244
|
-
if (!rawMdx) continue;
|
|
245
254
|
|
|
246
|
-
let
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if (!needRebuild) {
|
|
252
|
-
skipped++;
|
|
255
|
+
let rawMdx: string;
|
|
256
|
+
try {
|
|
257
|
+
rawMdx = await readFile(file.absPath, "utf-8");
|
|
258
|
+
} catch (err) {
|
|
259
|
+
if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
|
|
253
260
|
continue;
|
|
254
261
|
}
|
|
255
262
|
|
|
256
|
-
|
|
263
|
+
if (rebuildDecision === "hash_check") {
|
|
264
|
+
const contentHash = hashContent(rawMdx);
|
|
265
|
+
const cached = cache[file.path];
|
|
266
|
+
if (cached && cached.hash === contentHash) {
|
|
267
|
+
if (!assetsChanged) {
|
|
268
|
+
const outputPath = join(DIST_DIR, "docs", `${file.path}.html`);
|
|
269
|
+
if (existsSync(outputPath)) {
|
|
270
|
+
cache[file.path] = { ...cached, mtime: file.mtime, builtAt: Date.now() };
|
|
271
|
+
skipped++;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const relPath = file.absPath.replace(PROJECT_ROOT + "/", "");
|
|
257
279
|
const capturedRawMdx = rawMdx;
|
|
258
280
|
const capturedFile = file;
|
|
259
281
|
|
|
260
282
|
buildTasks.push(async () => {
|
|
261
283
|
try {
|
|
262
|
-
const
|
|
284
|
+
const pageNonce = generateNonce();
|
|
285
|
+
const html = await renderDocsPage(
|
|
286
|
+
capturedFile.path,
|
|
287
|
+
capturedRawMdx,
|
|
288
|
+
relPath,
|
|
289
|
+
gitDates,
|
|
290
|
+
builder,
|
|
291
|
+
pageNonce
|
|
292
|
+
);
|
|
263
293
|
const outputPath = join(DIST_DIR, "docs", `${capturedFile.path}.html`);
|
|
264
294
|
await mkdir(dirname(outputPath), { recursive: true });
|
|
265
295
|
await writeFile(outputPath, html);
|
|
@@ -285,7 +315,14 @@ async function build() {
|
|
|
285
315
|
const indexMdxPath = join(DOCS_DIR, "index.mdx");
|
|
286
316
|
const indexRaw = await readFile(indexMdxPath, "utf-8");
|
|
287
317
|
const indexRelPath = indexMdxPath.replace(PROJECT_ROOT + "/", "");
|
|
288
|
-
const indexHtml = await renderDocsPage(
|
|
318
|
+
const indexHtml = await renderDocsPage(
|
|
319
|
+
"",
|
|
320
|
+
indexRaw,
|
|
321
|
+
indexRelPath,
|
|
322
|
+
gitDates,
|
|
323
|
+
builder,
|
|
324
|
+
generateNonce()
|
|
325
|
+
);
|
|
289
326
|
await mkdir(join(DIST_DIR, "docs"), { recursive: true });
|
|
290
327
|
await writeFile(join(DIST_DIR, "docs", "index.html"), indexHtml);
|
|
291
328
|
} catch (err) {
|
|
@@ -295,11 +332,17 @@ async function build() {
|
|
|
295
332
|
}
|
|
296
333
|
|
|
297
334
|
const landingPage = React.createElement(IndexPage);
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
docuConfig.meta?.
|
|
301
|
-
|
|
302
|
-
|
|
335
|
+
const landingFavicon = docuConfig.meta?.favicon || "/favicon.ico";
|
|
336
|
+
const landingHtml = htmlShell({
|
|
337
|
+
title: docuConfig.meta?.title || "DocuBook",
|
|
338
|
+
description: docuConfig.meta?.description || "",
|
|
339
|
+
body: renderToString(landingPage),
|
|
340
|
+
favicon: landingFavicon,
|
|
341
|
+
css: assetManifest.css,
|
|
342
|
+
js: assetManifest.js,
|
|
343
|
+
nonce: generateNonce(),
|
|
344
|
+
themeCss: inlineThemeCss,
|
|
345
|
+
});
|
|
303
346
|
await writeFile(join(DIST_DIR, "index.html"), landingHtml);
|
|
304
347
|
|
|
305
348
|
const notFoundPage = React.createElement(
|
|
@@ -307,13 +350,33 @@ async function build() {
|
|
|
307
350
|
{ repoUrl: docuConfig.repo?.url },
|
|
308
351
|
React.createElement(NotFoundPage)
|
|
309
352
|
);
|
|
310
|
-
const
|
|
353
|
+
const notFoundFavicon = docuConfig.meta?.favicon || "/favicon.ico";
|
|
354
|
+
const notFoundHtml = htmlShell({
|
|
355
|
+
title: "404 - Not Found",
|
|
356
|
+
description: "",
|
|
357
|
+
body: renderToString(notFoundPage),
|
|
358
|
+
favicon: notFoundFavicon,
|
|
359
|
+
css: assetManifest.css,
|
|
360
|
+
js: assetManifest.js,
|
|
361
|
+
nonce: generateNonce(),
|
|
362
|
+
themeCss: inlineThemeCss,
|
|
363
|
+
});
|
|
311
364
|
await writeFile(join(DIST_DIR, "404.html"), notFoundHtml);
|
|
312
365
|
|
|
313
366
|
logger.spinner.stop(
|
|
314
367
|
`Built ${built} pages (${skipped} cached) \x1b[90m(${Math.round(performance.now() - t)}ms)\x1b[0m`
|
|
315
368
|
);
|
|
316
369
|
|
|
370
|
+
if (builder) {
|
|
371
|
+
const pages: PageMeta[] = mdxFiles.map((f) => ({
|
|
372
|
+
slug: f.path,
|
|
373
|
+
title: f.path.split("/").pop() || f.path,
|
|
374
|
+
filePath: join(DOCS_DIR, f.path),
|
|
375
|
+
outputPath: join(DIST_DIR, "docs", `${f.path}.html`),
|
|
376
|
+
}));
|
|
377
|
+
await builder.runOnEnd(pages);
|
|
378
|
+
}
|
|
379
|
+
|
|
317
380
|
logger.indexStart();
|
|
318
381
|
t = performance.now();
|
|
319
382
|
const indexCount = await generateSearchIndex();
|
|
@@ -330,10 +393,12 @@ async function build() {
|
|
|
330
393
|
}
|
|
331
394
|
}
|
|
332
395
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
396
|
+
if (!process.env.VITEST) {
|
|
397
|
+
initSentry()
|
|
398
|
+
.then(() => build())
|
|
399
|
+
.catch((err) => {
|
|
400
|
+
captureException(err);
|
|
401
|
+
console.error("Build failed:", err);
|
|
402
|
+
process.exit(1);
|
|
403
|
+
});
|
|
404
|
+
}
|
package/.docu/node/html.ts
CHANGED
|
@@ -6,14 +6,39 @@ export interface HtmlShellOptions {
|
|
|
6
6
|
css: string;
|
|
7
7
|
js: string;
|
|
8
8
|
nonce?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Content-Security-Policy value (from `cspHeader()` in security.ts).
|
|
11
|
+
* When provided, injects `<meta http-equiv="Content-Security-Policy">` in `<head>`.
|
|
12
|
+
* Essential for static deployment where HTTP headers cannot be set.
|
|
13
|
+
*/
|
|
14
|
+
csp?: string;
|
|
9
15
|
extraScripts?: string;
|
|
10
16
|
themeCss?: string;
|
|
17
|
+
/** HTML strings to inject before `</head>` (from plugin `injectHead` hooks). */
|
|
18
|
+
headExtra?: string[];
|
|
19
|
+
/** HTML strings to inject before `</body>`, after the main script (from plugin `injectBody` hooks). */
|
|
20
|
+
bodyExtra?: string[];
|
|
11
21
|
}
|
|
12
22
|
|
|
13
23
|
export function htmlShell(opts: HtmlShellOptions): string {
|
|
14
|
-
const {
|
|
24
|
+
const {
|
|
25
|
+
title,
|
|
26
|
+
description,
|
|
27
|
+
body,
|
|
28
|
+
favicon,
|
|
29
|
+
css,
|
|
30
|
+
js,
|
|
31
|
+
nonce,
|
|
32
|
+
csp,
|
|
33
|
+
extraScripts,
|
|
34
|
+
themeCss,
|
|
35
|
+
headExtra,
|
|
36
|
+
bodyExtra,
|
|
37
|
+
} = opts;
|
|
15
38
|
const nonceAttr = nonce ? ` nonce="${Bun.escapeHTML(nonce)}"` : "";
|
|
16
39
|
const themeStyle = themeCss ? `\n <style${nonceAttr}>${Bun.escapeHTML(themeCss)}</style>` : "";
|
|
40
|
+
const headInjection = headExtra?.length ? `\n ${headExtra.join("\n ")}` : "";
|
|
41
|
+
const bodyInjection = bodyExtra?.length ? `\n ${bodyExtra.join("\n ")}` : "";
|
|
17
42
|
return `<!DOCTYPE html>
|
|
18
43
|
<html lang="en">
|
|
19
44
|
<head>
|
|
@@ -23,15 +48,39 @@ export function htmlShell(opts: HtmlShellOptions): string {
|
|
|
23
48
|
<meta name="description" content="${Bun.escapeHTML(description)}">
|
|
24
49
|
<link rel="icon" type="image/x-icon" href="${Bun.escapeHTML(favicon)}">${themeStyle}
|
|
25
50
|
<link rel="stylesheet" href="/assets/${Bun.escapeHTML(css)}">
|
|
26
|
-
|
|
51
|
+
${csp ? `<meta http-equiv="Content-Security-Policy" content="${Bun.escapeHTML(csp)}">` : ""}
|
|
52
|
+
<script${nonceAttr}>try{if(localStorage.getItem("theme")==="dark")document.documentElement.classList.add("dark")}catch(e){}</script>${headInjection}
|
|
27
53
|
</head>
|
|
28
54
|
<body>
|
|
29
55
|
<div id="root">${body}</div>
|
|
30
|
-
<script${nonceAttr} src="/assets/${Bun.escapeHTML(js)}"></script>${extraScripts ? `\n ${extraScripts}` : ""}
|
|
56
|
+
<script${nonceAttr} src="/assets/${Bun.escapeHTML(js)}"></script>${extraScripts ? `\n ${extraScripts}` : ""}${bodyInjection}
|
|
31
57
|
</body>
|
|
32
58
|
</html>`;
|
|
33
59
|
}
|
|
34
60
|
|
|
61
|
+
export function errorHtml(message: string, stack?: string): string {
|
|
62
|
+
const msg = Bun.escapeHTML(message || "Unknown error");
|
|
63
|
+
const st = Bun.escapeHTML(stack || "");
|
|
64
|
+
return `<!DOCTYPE html>
|
|
65
|
+
<html lang="en">
|
|
66
|
+
<head>
|
|
67
|
+
<meta charset="utf-8">
|
|
68
|
+
<title>Server Error</title>
|
|
69
|
+
<style>
|
|
70
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
71
|
+
body{padding:2rem;font-family:ui-monospace,monospace;background:#1a1a2e;color:#e0e0e0}
|
|
72
|
+
h1{color:#ff6b6b;font-size:1.5rem;margin-bottom:1rem}
|
|
73
|
+
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}
|
|
74
|
+
.msg{color:#ff6b6b;font-weight:bold}
|
|
75
|
+
</style>
|
|
76
|
+
</head>
|
|
77
|
+
<body>
|
|
78
|
+
<h1>🔥 Server Error</h1>
|
|
79
|
+
<pre><span class="msg">${msg}</span>${st ? `\n\n${st}` : ""}</pre>
|
|
80
|
+
</body>
|
|
81
|
+
</html>`;
|
|
82
|
+
}
|
|
83
|
+
|
|
35
84
|
export function hmrScript(nonce: string): string {
|
|
36
85
|
return `<script nonce="${Bun.escapeHTML(nonce)}">
|
|
37
86
|
(function(){
|
package/.docu/node/mdx.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import type { Pluggable } from "unified";
|
|
2
3
|
import {
|
|
3
4
|
serialize,
|
|
4
5
|
extractTocsFromRawMdx,
|
|
@@ -19,10 +20,21 @@ export interface MdxResult {
|
|
|
19
20
|
tocs: ReturnType<typeof extractTocsFromRawMdx>;
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Compile MDX/MD content into a React element and compiled source.
|
|
25
|
+
*
|
|
26
|
+
* @param rawMdx - Raw MDX/MD file content
|
|
27
|
+
* @param filePath - Relative file path for git date lookup
|
|
28
|
+
* @param gitDates - Optional pre-fetched git last-modified map
|
|
29
|
+
* @param remarkPlugins - Additional remark plugins (merged after defaults, optional)
|
|
30
|
+
* @param rehypePlugins - Additional rehype plugins (merged after defaults, optional)
|
|
31
|
+
*/
|
|
22
32
|
export async function compileMdx(
|
|
23
33
|
rawMdx: string,
|
|
24
34
|
filePath: string,
|
|
25
|
-
gitDates?: Map<string, string
|
|
35
|
+
gitDates?: Map<string, string>,
|
|
36
|
+
remarkPlugins?: Pluggable[],
|
|
37
|
+
rehypePlugins?: Pluggable[]
|
|
26
38
|
): Promise<MdxResult> {
|
|
27
39
|
const tocs = extractTocsFromRawMdx(rawMdx);
|
|
28
40
|
const { frontmatter, strippedContent } = extractFrontmatterWithContent<{
|
|
@@ -31,10 +43,16 @@ export async function compileMdx(
|
|
|
31
43
|
date?: string;
|
|
32
44
|
}>(rawMdx);
|
|
33
45
|
|
|
46
|
+
const defaultRemark = createDefaultRemarkPlugins();
|
|
47
|
+
const defaultRehype = createDefaultRehypePlugins();
|
|
48
|
+
|
|
49
|
+
const finalRemark = remarkPlugins?.length ? [...defaultRemark, ...remarkPlugins] : defaultRemark;
|
|
50
|
+
const finalRehype = rehypePlugins?.length ? [...defaultRehype, ...rehypePlugins] : defaultRehype;
|
|
51
|
+
|
|
34
52
|
const serialized = await serialize(strippedContent, {
|
|
35
53
|
mdxOptions: {
|
|
36
|
-
rehypePlugins:
|
|
37
|
-
remarkPlugins:
|
|
54
|
+
rehypePlugins: finalRehype,
|
|
55
|
+
remarkPlugins: finalRemark,
|
|
38
56
|
},
|
|
39
57
|
});
|
|
40
58
|
|