@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
|
@@ -35,7 +35,7 @@ export const NGINX_CONF = `server {
|
|
|
35
35
|
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
|
|
36
36
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
37
37
|
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
|
38
|
-
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'
|
|
38
|
+
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; font-src 'self' data:; connect-src 'self' https:; frame-src https://www.youtube-nocookie.com; frame-ancestors 'none'" always;
|
|
39
39
|
|
|
40
40
|
location /assets/ {
|
|
41
41
|
expires 1y;
|
|
@@ -45,7 +45,7 @@ export const NGINX_CONF = `server {
|
|
|
45
45
|
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
|
|
46
46
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
47
47
|
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
|
48
|
-
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'
|
|
48
|
+
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; font-src 'self' data:; connect-src 'self' https:; frame-src https://www.youtube-nocookie.com; frame-ancestors 'none'" always;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
location /docs/assets/ {
|
|
@@ -56,7 +56,7 @@ export const NGINX_CONF = `server {
|
|
|
56
56
|
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
|
|
57
57
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
58
58
|
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
|
59
|
-
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'
|
|
59
|
+
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; font-src 'self' data:; connect-src 'self' https:; frame-src https://www.youtube-nocookie.com; frame-ancestors 'none'" always;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
location / {
|
|
@@ -135,7 +135,7 @@ export const HEADERS_FILE = `/*
|
|
|
135
135
|
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
|
|
136
136
|
Referrer-Policy: strict-origin-when-cross-origin
|
|
137
137
|
Permissions-Policy: camera=(), microphone=(), geolocation=()
|
|
138
|
-
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'
|
|
138
|
+
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; font-src 'self' data:; connect-src 'self' https:; frame-src https://www.youtube-nocookie.com; frame-ancestors 'none'
|
|
139
139
|
|
|
140
140
|
/assets/*
|
|
141
141
|
Cache-Control: public, max-age=31536000, immutable
|
|
@@ -171,7 +171,7 @@ async function writeDockerFiles() {
|
|
|
171
171
|
if (!existsSync(join(dockerDir, "Dockerfile"))) {
|
|
172
172
|
await writeFile(
|
|
173
173
|
join(dockerDir, "Dockerfile"),
|
|
174
|
-
`FROM ghcr.io/docubook/flame
|
|
174
|
+
`FROM ghcr.io/docubook/flame:${FLAME_MAJOR} AS builder
|
|
175
175
|
ENV NODE_ENV=production
|
|
176
176
|
WORKDIR /app
|
|
177
177
|
COPY . .
|
package/.docu/node/deploy.ts
CHANGED
|
@@ -49,7 +49,7 @@ async function runBuild() {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
export const DOCKERFILE_BUN = `FROM ghcr.io/docubook/flame
|
|
52
|
+
export const DOCKERFILE_BUN = `FROM ghcr.io/docubook/flame:${FLAME_MAJOR} AS builder
|
|
53
53
|
ENV NODE_ENV=production
|
|
54
54
|
WORKDIR /app
|
|
55
55
|
COPY . .
|
|
@@ -34,6 +34,9 @@ export interface HtmlShellOptions {
|
|
|
34
34
|
bodyExtra?: string[];
|
|
35
35
|
/** SEO meta tags derived from config + frontmatter */
|
|
36
36
|
seo?: SeoMeta;
|
|
37
|
+
/** Root-absolute asset URLs (`/assets/...`). Required for pages served at
|
|
38
|
+
* arbitrary paths (404 fallback) — relative depth is wrong there. */
|
|
39
|
+
absoluteAssets?: boolean;
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
export function htmlShell(opts: HtmlShellOptions): string {
|
|
@@ -51,14 +54,16 @@ export function htmlShell(opts: HtmlShellOptions): string {
|
|
|
51
54
|
depth = 0,
|
|
52
55
|
headExtra,
|
|
53
56
|
bodyExtra,
|
|
57
|
+
absoluteAssets = false,
|
|
54
58
|
} = opts;
|
|
55
59
|
const nonceAttr = nonce ? ` nonce="${escapeHtml(nonce)}"` : "";
|
|
56
60
|
const themeStyle = themeCss ? `\n <style${nonceAttr}>${escapeHtml(themeCss)}</style>` : "";
|
|
57
61
|
const headInjection = headExtra?.length ? `\n ${headExtra.join("\n ")}` : "";
|
|
58
62
|
const bodyInjection = bodyExtra?.length ? `\n ${bodyExtra.join("\n ")}` : "";
|
|
59
63
|
const depthPrefix = depth === 0 ? "" : "../".repeat(depth);
|
|
60
|
-
const assetPrefix = depthPrefix + "assets/";
|
|
61
|
-
const resolvePath = (path: string) =>
|
|
64
|
+
const assetPrefix = absoluteAssets ? "/assets/" : depthPrefix + "assets/";
|
|
65
|
+
const resolvePath = (path: string) =>
|
|
66
|
+
absoluteAssets ? path : path.startsWith("/") ? depthPrefix + path.slice(1) : path;
|
|
62
67
|
|
|
63
68
|
// Build SEO meta tags (OG, Twitter, canonical)
|
|
64
69
|
let seoTags = "";
|
package/.docu/node/html.ts
CHANGED
|
@@ -16,14 +16,16 @@ export function htmlShell(opts: HtmlShellOptions): string {
|
|
|
16
16
|
depth = 0,
|
|
17
17
|
headExtra,
|
|
18
18
|
bodyExtra,
|
|
19
|
+
absoluteAssets = false,
|
|
19
20
|
} = opts;
|
|
20
21
|
const nonceAttr = nonce ? ` nonce="${Bun.escapeHTML(nonce)}"` : "";
|
|
21
22
|
const themeStyle = themeCss ? `\n <style${nonceAttr}>${Bun.escapeHTML(themeCss)}</style>` : "";
|
|
22
23
|
const headInjection = headExtra?.length ? `\n ${headExtra.join("\n ")}` : "";
|
|
23
24
|
const bodyInjection = bodyExtra?.length ? `\n ${bodyExtra.join("\n ")}` : "";
|
|
24
25
|
const depthPrefix = depth === 0 ? "" : "../".repeat(depth);
|
|
25
|
-
const assetPrefix = depthPrefix + "assets/";
|
|
26
|
-
const resolvePath = (path: string) =>
|
|
26
|
+
const assetPrefix = absoluteAssets ? "/assets/" : depthPrefix + "assets/";
|
|
27
|
+
const resolvePath = (path: string) =>
|
|
28
|
+
absoluteAssets ? path : path.startsWith("/") ? depthPrefix + path.slice(1) : path;
|
|
27
29
|
|
|
28
30
|
// Build SEO meta tags (OG, Twitter, canonical)
|
|
29
31
|
let seoTags = "";
|
|
@@ -187,7 +187,6 @@ function collectAllLucideIcons(): string[] {
|
|
|
187
187
|
join(FRAMEWORK_ROOT, "..", "mdx-content", "dist"),
|
|
188
188
|
join(FRAMEWORK_ROOT, "..", "ui-react", "dist"),
|
|
189
189
|
join(FRAMEWORK_ROOT, "..", "core", "dist"),
|
|
190
|
-
join(FRAMEWORK_ROOT, "..", "runt", "dist"),
|
|
191
190
|
join(FRAMEWORK_ROOT, "..", "themes-colors", "dist"),
|
|
192
191
|
];
|
|
193
192
|
for (const d of depDirs) scanDirLucideIcons(resolve(d), icons);
|
|
@@ -195,7 +194,10 @@ function collectAllLucideIcons(): string[] {
|
|
|
195
194
|
}
|
|
196
195
|
|
|
197
196
|
/** Build the client JS bundle and Tailwind CSS. */
|
|
198
|
-
export async function buildClientBundle(
|
|
197
|
+
export async function buildClientBundle(
|
|
198
|
+
/** slug → compiled MDX ESM module source (program format) for static hydration. */
|
|
199
|
+
mdxSources: Record<string, string> = {}
|
|
200
|
+
): Promise<{ js: string; css: string }> {
|
|
199
201
|
await mkdir(ASSETS_DIR, { recursive: true });
|
|
200
202
|
const twKey = tailwindCacheKey();
|
|
201
203
|
await cleanOldBundles(new Set([`client-${twKey}.css`]));
|
|
@@ -243,11 +245,11 @@ export async function buildClientBundle(): Promise<{ js: string; css: string }>
|
|
|
243
245
|
if (args.namespace === "lucide-virt") {
|
|
244
246
|
return { path: getLucideRealEntry(), namespace: "file" };
|
|
245
247
|
}
|
|
246
|
-
//
|
|
248
|
+
// markdown Icon.tsx uses namespace import for arbitrary
|
|
247
249
|
// user-provided icon names in MDX — keep full barrel there.
|
|
248
250
|
if (args.importer) {
|
|
249
251
|
const normalized = normalizeImporterPath(args.importer);
|
|
250
|
-
if (normalized.includes("/
|
|
252
|
+
if (normalized.includes("/markdown/dist/")) {
|
|
251
253
|
return { path: getLucideRealEntry(), namespace: "file" };
|
|
252
254
|
}
|
|
253
255
|
}
|
|
@@ -294,6 +296,69 @@ export async function buildClientBundle(): Promise<{ js: string; css: string }>
|
|
|
294
296
|
});
|
|
295
297
|
},
|
|
296
298
|
},
|
|
299
|
+
{
|
|
300
|
+
// Serves per-page compiled MDX (program format) as real modules so
|
|
301
|
+
// the client hydrates the content island without `new Function`.
|
|
302
|
+
// client.ts imports `{ mdxModules } from "./mdx-manifest"`.
|
|
303
|
+
name: "mdx-hydrate",
|
|
304
|
+
setup(build) {
|
|
305
|
+
// Compiled MDX (program format) imports the JSX runtime and the
|
|
306
|
+
// MDX provider. Virtual modules have no real directory, so esbuild
|
|
307
|
+
// cannot resolve bare specifiers from them — map them to the
|
|
308
|
+
// installed real paths explicitly (same pattern as lucide-optimize).
|
|
309
|
+
const require = createRequire(import.meta.url);
|
|
310
|
+
const mdxModuleDeps: Record<string, string> = {
|
|
311
|
+
"react/jsx-runtime": require.resolve("react/jsx-runtime"),
|
|
312
|
+
"react/jsx-dev-runtime": require.resolve("react/jsx-dev-runtime"),
|
|
313
|
+
"@mdx-js/react": require.resolve("@mdx-js/react"),
|
|
314
|
+
};
|
|
315
|
+
build.onResolve(
|
|
316
|
+
{ filter: /^(react\/jsx-runtime|react\/jsx-dev-runtime|@mdx-js\/react)$/ },
|
|
317
|
+
(args) => {
|
|
318
|
+
if (args.namespace !== "mdx-module") return;
|
|
319
|
+
return { path: mdxModuleDeps[args.path], namespace: "file" };
|
|
320
|
+
}
|
|
321
|
+
);
|
|
322
|
+
build.onResolve({ filter: /^mdx-module:/ }, (args) => ({
|
|
323
|
+
path: args.path,
|
|
324
|
+
namespace: "mdx-module",
|
|
325
|
+
}));
|
|
326
|
+
build.onLoad({ filter: /.*/, namespace: "mdx-module" }, (args) => {
|
|
327
|
+
const slug = args.path.slice("mdx-module:".length);
|
|
328
|
+
const contents = mdxSources[slug];
|
|
329
|
+
if (contents == null) {
|
|
330
|
+
return {
|
|
331
|
+
errors: [{ text: `unknown mdx module: ${slug}` }],
|
|
332
|
+
contents: "",
|
|
333
|
+
loader: "js",
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
return { contents, loader: "js" };
|
|
337
|
+
});
|
|
338
|
+
build.onResolve({ filter: /mdx-manifest$/ }, (args) => ({
|
|
339
|
+
path: args.path,
|
|
340
|
+
namespace: "mdx-manifest",
|
|
341
|
+
}));
|
|
342
|
+
build.onLoad({ filter: /.*/, namespace: "mdx-manifest" }, () => {
|
|
343
|
+
// Sort keys: the prePass fills mdxSources via Promise.all, so
|
|
344
|
+
// insertion order = resolution order (non-deterministic across
|
|
345
|
+
// processes). Stable key order keeps the bundle hash stable so
|
|
346
|
+
// the build cache (`assetsChanged`) actually hits.
|
|
347
|
+
const slugs = Object.keys(mdxSources).sort();
|
|
348
|
+
const imports = slugs
|
|
349
|
+
.map((slug, i) => {
|
|
350
|
+
const key = slug.replace(/["\\]/g, "");
|
|
351
|
+
return `import * as _mdx${i} from "mdx-module:${key}";`;
|
|
352
|
+
})
|
|
353
|
+
.join("\n");
|
|
354
|
+
const map = slugs.map((slug, i) => `${JSON.stringify(slug)}: _mdx${i}`).join(", ");
|
|
355
|
+
return {
|
|
356
|
+
contents: `${imports}\nexport const mdxModules = { ${map} };\n`,
|
|
357
|
+
loader: "js",
|
|
358
|
+
};
|
|
359
|
+
});
|
|
360
|
+
},
|
|
361
|
+
},
|
|
297
362
|
],
|
|
298
363
|
});
|
|
299
364
|
} finally {
|
package/.docu/node/hydrate.ts
CHANGED
|
@@ -125,7 +125,10 @@ async function buildTailwindCss(key: string): Promise<{ file: string; content: s
|
|
|
125
125
|
return { file: cssFile, content: cssContent };
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
export async function buildClientBundle(
|
|
128
|
+
export async function buildClientBundle(
|
|
129
|
+
/** slug → compiled MDX ESM module source (program format) for static hydration. */
|
|
130
|
+
mdxSources: Record<string, string> = {}
|
|
131
|
+
): Promise<{ js: string; css: string }> {
|
|
129
132
|
await mkdir(ASSETS_DIR, { recursive: true });
|
|
130
133
|
const twKey = twCacheKey();
|
|
131
134
|
await cleanOldBundles(new Set([`client-${twKey}.css`]));
|
|
@@ -166,6 +169,52 @@ export async function buildClientBundle(): Promise<{ js: string; css: string }>
|
|
|
166
169
|
});
|
|
167
170
|
},
|
|
168
171
|
},
|
|
172
|
+
{
|
|
173
|
+
// Serves per-page compiled MDX (program format) as real modules so
|
|
174
|
+
// the client hydrates the content island without `new Function`.
|
|
175
|
+
// client.ts imports `{ mdxModules } from "./mdx-manifest"`.
|
|
176
|
+
name: "mdx-hydrate",
|
|
177
|
+
setup(build) {
|
|
178
|
+
build.onResolve({ filter: /^mdx-module:/ }, (args) => ({
|
|
179
|
+
path: args.path,
|
|
180
|
+
namespace: "mdx-module",
|
|
181
|
+
}));
|
|
182
|
+
build.onLoad({ filter: /.*/, namespace: "mdx-module" }, (args) => {
|
|
183
|
+
const slug = args.path.slice("mdx-module:".length);
|
|
184
|
+
const contents = mdxSources[slug];
|
|
185
|
+
if (contents == null) {
|
|
186
|
+
return {
|
|
187
|
+
errors: [{ text: `unknown mdx module: ${slug}` }],
|
|
188
|
+
contents: "",
|
|
189
|
+
loader: "js",
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
return { contents, loader: "js" };
|
|
193
|
+
});
|
|
194
|
+
build.onResolve({ filter: /mdx-manifest$/ }, (args) => ({
|
|
195
|
+
path: args.path,
|
|
196
|
+
namespace: "mdx-manifest",
|
|
197
|
+
}));
|
|
198
|
+
build.onLoad({ filter: /.*/, namespace: "mdx-manifest" }, () => {
|
|
199
|
+
// Sort keys: the prePass fills mdxSources via Promise.all, so
|
|
200
|
+
// insertion order = resolution order (non-deterministic across
|
|
201
|
+
// processes). Stable key order keeps the bundle hash stable so
|
|
202
|
+
// the build cache (`assetsChanged`) actually hits.
|
|
203
|
+
const slugs = Object.keys(mdxSources).sort();
|
|
204
|
+
const imports = slugs
|
|
205
|
+
.map((slug, i) => {
|
|
206
|
+
const key = slug.replace(/["\\]/g, "");
|
|
207
|
+
return `import * as _mdx${i} from "mdx-module:${key}";`;
|
|
208
|
+
})
|
|
209
|
+
.join("\n");
|
|
210
|
+
const map = slugs.map((slug, i) => `${JSON.stringify(slug)}: _mdx${i}`).join(", ");
|
|
211
|
+
return {
|
|
212
|
+
contents: `${imports}\nexport const mdxModules = { ${map} };\n`,
|
|
213
|
+
loader: "js",
|
|
214
|
+
};
|
|
215
|
+
});
|
|
216
|
+
},
|
|
217
|
+
},
|
|
169
218
|
],
|
|
170
219
|
});
|
|
171
220
|
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Virtual module served by the `mdx-hydrate` esbuild plugin (hydrate.node.ts).
|
|
2
|
+
// Maps doc slug → compiled MDX module. Generated at bundle time; never on disk.
|
|
3
|
+
declare module "./mdx-manifest" {
|
|
4
|
+
export const mdxModules: Record<string, { default: React.ComponentType }>;
|
|
5
|
+
}
|
package/.docu/node/mdx.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { Pluggable } from "unified";
|
|
3
|
+
import { z, type ZodType } from "zod";
|
|
3
4
|
import {
|
|
4
5
|
serialize,
|
|
5
6
|
extractTocsFromRawMdx,
|
|
@@ -8,7 +9,7 @@ import {
|
|
|
8
9
|
createDefaultRemarkPlugins,
|
|
9
10
|
MDXRemote,
|
|
10
11
|
} from "@docubook/core";
|
|
11
|
-
import { createMdxComponents } from "@docubook/
|
|
12
|
+
import { createMdxComponents } from "@docubook/markdown";
|
|
12
13
|
import { getGitLastModified, getGitLastModifiedBatch, getFilesystemMtime } from "./git";
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -106,10 +107,39 @@ export { getGitLastModifiedBatch };
|
|
|
106
107
|
export interface MdxResult {
|
|
107
108
|
content: React.ReactElement;
|
|
108
109
|
compiledSource: string;
|
|
109
|
-
frontmatter:
|
|
110
|
+
frontmatter: Frontmatter;
|
|
110
111
|
tocs: ReturnType<typeof extractTocsFromRawMdx>;
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
/**
|
|
115
|
+
* DocuBook frontmatter contract — single source of truth for frontmatter
|
|
116
|
+
* fields. Add new properties here; types and validation derive from it.
|
|
117
|
+
* YAML coerces unquoted values, so string fields use `z.coerce.*`.
|
|
118
|
+
*/
|
|
119
|
+
export const frontmatterSchema = z.object({
|
|
120
|
+
title: z.coerce.string().optional(),
|
|
121
|
+
description: z.coerce.string().optional(),
|
|
122
|
+
image: z.coerce.string().optional(),
|
|
123
|
+
date: z.coerce.string().optional(),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
export type Frontmatter = z.infer<typeof frontmatterSchema>;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Read a string field from frontmatter after the plugin transform chain
|
|
130
|
+
* (which widens the type to `Record<string, unknown>`). Returns "" when
|
|
131
|
+
* missing or not a string.
|
|
132
|
+
*/
|
|
133
|
+
export function frontmatterField(frontmatter: Record<string, unknown>, key: string): string {
|
|
134
|
+
return typeof frontmatter[key] === "string" ? (frontmatter[key] as string) : "";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Zod schema validating frontmatter after extraction.
|
|
139
|
+
* Must satisfy the frontmatter contract (defaults to `frontmatterSchema`).
|
|
140
|
+
*/
|
|
141
|
+
export type FrontmatterSchema = ZodType<Frontmatter>;
|
|
142
|
+
|
|
113
143
|
/**
|
|
114
144
|
* Compile MDX/MD content into a React element and compiled source.
|
|
115
145
|
*
|
|
@@ -118,20 +148,26 @@ export interface MdxResult {
|
|
|
118
148
|
* @param gitDates - Optional pre-fetched git last-modified map
|
|
119
149
|
* @param remarkPlugins - Additional remark plugins (merged after defaults, optional)
|
|
120
150
|
* @param rehypePlugins - Additional rehype plugins (merged after defaults, optional)
|
|
151
|
+
* @param frontmatterSchema - Custom schema overriding the default contract
|
|
121
152
|
*/
|
|
122
|
-
|
|
153
|
+
/**
|
|
154
|
+
* Shared compile core used by `compileMdx` (SSR) and `compileMdxModule`
|
|
155
|
+
* (static hydration). Strips frontmatter, merges the doc plugin chain
|
|
156
|
+
* (defaults + .html link fixes + user plugins) and runs `serialize()`.
|
|
157
|
+
*/
|
|
158
|
+
async function serializeWithDocPlugins(
|
|
123
159
|
rawMdx: string,
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
160
|
+
opts: {
|
|
161
|
+
outputFormat?: "function-body" | "program";
|
|
162
|
+
remarkPlugins?: Pluggable[];
|
|
163
|
+
rehypePlugins?: Pluggable[];
|
|
164
|
+
frontmatterSchema?: FrontmatterSchema;
|
|
165
|
+
} = {}
|
|
166
|
+
) {
|
|
167
|
+
const { strippedContent } = extractFrontmatterWithContent<Frontmatter>(
|
|
168
|
+
rawMdx,
|
|
169
|
+
opts.frontmatterSchema
|
|
170
|
+
);
|
|
135
171
|
|
|
136
172
|
const defaultRemark = createDefaultRemarkPlugins();
|
|
137
173
|
const defaultRehype = createDefaultRehypePlugins();
|
|
@@ -139,15 +175,46 @@ export async function compileMdx(
|
|
|
139
175
|
// remarkMdxJsxDocsHtmlLinks must run before user plugins so custom remark
|
|
140
176
|
// transforms see already-fixed hrefs. rehypeDocsHtmlLinks handles plain
|
|
141
177
|
// markdown [text](path) → <a> elements in the HAST phase.
|
|
142
|
-
const finalRemark = [...defaultRemark, remarkMdxJsxDocsHtmlLinks, ...(remarkPlugins ?? [])];
|
|
143
|
-
const finalRehype = [...defaultRehype, rehypeDocsHtmlLinks, ...(rehypePlugins ?? [])];
|
|
178
|
+
const finalRemark = [...defaultRemark, remarkMdxJsxDocsHtmlLinks, ...(opts.remarkPlugins ?? [])];
|
|
179
|
+
const finalRehype = [...defaultRehype, rehypeDocsHtmlLinks, ...(opts.rehypePlugins ?? [])];
|
|
144
180
|
|
|
145
|
-
|
|
181
|
+
// v2 contract: plain markdown + directives only — authored JSX tags are
|
|
182
|
+
// not parsed (dropped, content kept as text).
|
|
183
|
+
return serialize(strippedContent, {
|
|
184
|
+
outputFormat: opts.outputFormat,
|
|
185
|
+
format: "md",
|
|
146
186
|
mdxOptions: {
|
|
147
187
|
rehypePlugins: finalRehype,
|
|
148
188
|
remarkPlugins: finalRemark,
|
|
149
189
|
},
|
|
150
190
|
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Compile MDX/MD content into a React element and compiled source.
|
|
195
|
+
*
|
|
196
|
+
* @param rawMdx - Raw MDX/MD file content
|
|
197
|
+
* @param filePath - Relative file path for git date lookup
|
|
198
|
+
* @param gitDates - Optional pre-fetched git last-modified map
|
|
199
|
+
* @param remarkPlugins - Additional remark plugins (merged after defaults, optional)
|
|
200
|
+
* @param rehypePlugins - Additional rehype plugins (merged after defaults, optional)
|
|
201
|
+
* @param frontmatterSchema - Custom schema overriding the default contract
|
|
202
|
+
*/
|
|
203
|
+
export async function compileMdx(
|
|
204
|
+
rawMdx: string,
|
|
205
|
+
filePath: string,
|
|
206
|
+
gitDates?: Map<string, string>,
|
|
207
|
+
remarkPlugins?: Pluggable[],
|
|
208
|
+
rehypePlugins?: Pluggable[],
|
|
209
|
+
frontmatterSchema?: FrontmatterSchema
|
|
210
|
+
): Promise<MdxResult> {
|
|
211
|
+
const tocs = extractTocsFromRawMdx(rawMdx);
|
|
212
|
+
const { frontmatter } = extractFrontmatterWithContent<Frontmatter>(rawMdx, frontmatterSchema);
|
|
213
|
+
const serialized = await serializeWithDocPlugins(rawMdx, {
|
|
214
|
+
remarkPlugins,
|
|
215
|
+
rehypePlugins,
|
|
216
|
+
frontmatterSchema,
|
|
217
|
+
});
|
|
151
218
|
|
|
152
219
|
const components = createMdxComponents();
|
|
153
220
|
const content = React.createElement(MDXRemote, {
|
|
@@ -171,3 +238,22 @@ export async function compileMdx(
|
|
|
171
238
|
tocs,
|
|
172
239
|
};
|
|
173
240
|
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Compile MDX to a real ESM module source (program format) for static
|
|
244
|
+
* client-side hydration — the browser imports and executes it via the bundler
|
|
245
|
+
* instead of `new Function(compiledSource)`. Uses the same plugin chain as
|
|
246
|
+
* `compileMdx` so the hydrated tree matches the SSR output.
|
|
247
|
+
*/
|
|
248
|
+
export async function compileMdxModule(
|
|
249
|
+
rawMdx: string,
|
|
250
|
+
remarkPlugins?: Pluggable[],
|
|
251
|
+
rehypePlugins?: Pluggable[]
|
|
252
|
+
): Promise<string> {
|
|
253
|
+
const serialized = await serializeWithDocPlugins(rawMdx, {
|
|
254
|
+
outputFormat: "program",
|
|
255
|
+
remarkPlugins,
|
|
256
|
+
rehypePlugins,
|
|
257
|
+
});
|
|
258
|
+
return serialized.compiledSource;
|
|
259
|
+
}
|
|
@@ -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
|
}
|
package/.docu/node/route.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { loadDocuConfig } from "./paths";
|
|
2
2
|
import type { DocuRoute } from "./types";
|
|
3
3
|
import { resolveRoutes } from "./fs-scanner";
|
|
4
|
+
import { DOCS_DIR } from "./paths";
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { extractFrontmatter } from "@docubook/core";
|
|
8
|
+
import type { Frontmatter } from "./mdx";
|
|
4
9
|
|
|
5
10
|
const docuConfig = loadDocuConfig();
|
|
6
11
|
export const routes: DocuRoute[] = resolveRoutes(docuConfig.routes);
|
|
@@ -41,8 +46,53 @@ export function getRouteMap(): Map<string, string> {
|
|
|
41
46
|
return map;
|
|
42
47
|
}
|
|
43
48
|
|
|
49
|
+
/** Build-time cache of href → frontmatter description (docs content is static). */
|
|
50
|
+
const descriptionCache = new Map<string, string>();
|
|
51
|
+
|
|
52
|
+
function readDescription(href: string): string {
|
|
53
|
+
const cached = descriptionCache.get(href);
|
|
54
|
+
if (cached !== undefined) return cached;
|
|
55
|
+
|
|
56
|
+
let description = "";
|
|
57
|
+
const rel = href.replace(/^\/|$/g, "");
|
|
58
|
+
for (const ext of [".mdx", ".md"]) {
|
|
59
|
+
for (const file of [join(DOCS_DIR, `${rel}${ext}`), join(DOCS_DIR, `${rel}/index${ext}`)]) {
|
|
60
|
+
try {
|
|
61
|
+
const fm = extractFrontmatter<Frontmatter>(readFileSync(file, "utf-8"));
|
|
62
|
+
description = typeof fm.description === "string" ? fm.description : "";
|
|
63
|
+
if (description) break;
|
|
64
|
+
} catch {
|
|
65
|
+
// not this file — try the next candidate
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (description) break;
|
|
69
|
+
}
|
|
70
|
+
descriptionCache.set(href, description);
|
|
71
|
+
return description;
|
|
72
|
+
}
|
|
73
|
+
|
|
44
74
|
export function getPreviousNext(pathname: string) {
|
|
45
75
|
const normalizedPath = pathname.replace(/^\/|$/g, "");
|
|
76
|
+
|
|
77
|
+
// Docs index (/docs — DocsPage renders with pathname "" from slug []):
|
|
78
|
+
// next-only navigation into the first docs page — never read the route
|
|
79
|
+
// backward from the index, so prev stays null even if a page sits before
|
|
80
|
+
// it in the route list.
|
|
81
|
+
if (normalizedPath === "docs" || normalizedPath === "") {
|
|
82
|
+
const paths = flattenRoutes();
|
|
83
|
+
const routeMap = getRouteMap();
|
|
84
|
+
const first = paths[0];
|
|
85
|
+
if (!first) return { prev: null, next: null };
|
|
86
|
+
return {
|
|
87
|
+
prev: null,
|
|
88
|
+
next: {
|
|
89
|
+
href: first,
|
|
90
|
+
title: routeMap.get(first) || "",
|
|
91
|
+
description: readDescription(first),
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
46
96
|
const paths = flattenRoutes();
|
|
47
97
|
|
|
48
98
|
const index = paths.findIndex((href) => href === `/${normalizedPath}` || href === normalizedPath);
|
|
@@ -57,7 +107,13 @@ export function getPreviousNext(pathname: string) {
|
|
|
57
107
|
|
|
58
108
|
return {
|
|
59
109
|
prev: prevHref ? { href: prevHref, title: routeMap.get(prevHref) || "" } : null,
|
|
60
|
-
next: nextHref
|
|
110
|
+
next: nextHref
|
|
111
|
+
? {
|
|
112
|
+
href: nextHref,
|
|
113
|
+
title: routeMap.get(nextHref) || "",
|
|
114
|
+
description: readDescription(nextHref),
|
|
115
|
+
}
|
|
116
|
+
: null,
|
|
61
117
|
};
|
|
62
118
|
}
|
|
63
119
|
|
|
@@ -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
|
+
};
|