@docubook/flame 2.0.0-alpha.1 → 2.0.0-beta.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/node/mdx.ts CHANGED
@@ -115,13 +115,19 @@ export interface MdxResult {
115
115
  * DocuBook frontmatter contract — single source of truth for frontmatter
116
116
  * fields. Add new properties here; types and validation derive from it.
117
117
  * YAML coerces unquoted values, so string fields use `z.coerce.*`.
118
+ * `.passthrough()` keeps unknown fields (e.g. `author: wildan`, `tags`)
119
+ * in the parsed output — arbitrary frontmatter metadata stays available
120
+ * via `frontmatterField(frontmatter, key)` instead of being silently
121
+ * stripped by Zod's default object parsing.
118
122
  */
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
- });
123
+ export const frontmatterSchema = z
124
+ .object({
125
+ title: z.coerce.string().optional(),
126
+ description: z.coerce.string().optional(),
127
+ image: z.coerce.string().optional(),
128
+ date: z.coerce.string().optional(),
129
+ })
130
+ .passthrough();
125
131
 
126
132
  export type Frontmatter = z.infer<typeof frontmatterSchema>;
127
133
 
@@ -162,12 +168,13 @@ async function serializeWithDocPlugins(
162
168
  remarkPlugins?: Pluggable[];
163
169
  rehypePlugins?: Pluggable[];
164
170
  frontmatterSchema?: FrontmatterSchema;
165
- } = {}
171
+ } = {},
172
+ pre?: { frontmatter: Frontmatter; strippedContent: string }
166
173
  ) {
167
- const { strippedContent } = extractFrontmatterWithContent<Frontmatter>(
168
- rawMdx,
169
- opts.frontmatterSchema
170
- );
174
+ // Parse-once: when the prePass already extracted the frontmatter + stripped
175
+ // content, reuse it instead of re-parsing (the SSR phase skips extraction).
176
+ const { strippedContent, frontmatter } =
177
+ pre ?? extractFrontmatterWithContent<Frontmatter>(rawMdx, opts.frontmatterSchema);
171
178
 
172
179
  const defaultRemark = createDefaultRemarkPlugins();
173
180
  const defaultRehype = createDefaultRehypePlugins();
@@ -179,7 +186,8 @@ async function serializeWithDocPlugins(
179
186
  const finalRehype = [...defaultRehype, rehypeDocsHtmlLinks, ...(opts.rehypePlugins ?? [])];
180
187
 
181
188
  // v2 contract: plain markdown + directives only — authored JSX tags are
182
- // not parsed (dropped, content kept as text).
189
+ // not parsed (dropped, content kept as text). Return the frontmatter parsed
190
+ // above — `serialize()` only parses it when `parseFrontmatter` is set.
183
191
  return serialize(strippedContent, {
184
192
  outputFormat: opts.outputFormat,
185
193
  format: "md",
@@ -187,7 +195,7 @@ async function serializeWithDocPlugins(
187
195
  rehypePlugins: finalRehype,
188
196
  remarkPlugins: finalRemark,
189
197
  },
190
- });
198
+ }).then((serialized) => ({ ...serialized, frontmatter, strippedContent }));
191
199
  }
192
200
 
193
201
  /**
@@ -206,15 +214,19 @@ export async function compileMdx(
206
214
  gitDates?: Map<string, string>,
207
215
  remarkPlugins?: Pluggable[],
208
216
  rehypePlugins?: Pluggable[],
209
- frontmatterSchema?: FrontmatterSchema
217
+ frontmatterSchema?: FrontmatterSchema,
218
+ /** Pre-pass extracted data — avoids re-parsing frontmatter in the SSR phase. */
219
+ pre?: { frontmatter: Frontmatter; strippedContent: string }
210
220
  ): Promise<MdxResult> {
211
221
  const tocs = extractTocsFromRawMdx(rawMdx);
212
- const { frontmatter } = extractFrontmatterWithContent<Frontmatter>(rawMdx, frontmatterSchema);
213
- const serialized = await serializeWithDocPlugins(rawMdx, {
214
- remarkPlugins,
215
- rehypePlugins,
216
- frontmatterSchema,
217
- });
222
+ const frontmatter =
223
+ pre?.frontmatter ??
224
+ extractFrontmatterWithContent<Frontmatter>(rawMdx, frontmatterSchema).frontmatter;
225
+ const serialized = await serializeWithDocPlugins(
226
+ rawMdx,
227
+ { remarkPlugins, rehypePlugins, frontmatterSchema },
228
+ pre
229
+ );
218
230
 
219
231
  const components = createMdxComponents();
220
232
  const content = React.createElement(MDXRemote, {
@@ -245,15 +257,69 @@ export async function compileMdx(
245
257
  * instead of `new Function(compiledSource)`. Uses the same plugin chain as
246
258
  * `compileMdx` so the hydrated tree matches the SSR output.
247
259
  */
260
+ /**
261
+ * Frontmatter records collected once during compilation — pagination title /
262
+ * description and other metadata consumers read from here instead of
263
+ * re-reading + re-parsing files (parse-once contract: the frontmatter is
264
+ * already parsed by `serializeWithDocPlugins`).
265
+ */
266
+ const pageFrontmatter = new Map<string, Frontmatter>();
267
+
268
+ /** Register a page's frontmatter, keyed by its href. */
269
+ export function registerPageFrontmatter(href: string, frontmatter: Frontmatter): void {
270
+ pageFrontmatter.set(href, frontmatter);
271
+ }
272
+
273
+ /** Look up a page's frontmatter (undefined when not yet compiled). */
274
+ export function getPageFrontmatter(href: string): Frontmatter | undefined {
275
+ return pageFrontmatter.get(href);
276
+ }
277
+
278
+ /**
279
+ * Frontmatter-stripped content from the prePass — lets the SSR compile
280
+ * phase skip its own `extractFrontmatterWithContent` (parse-once across
281
+ * both compile phases).
282
+ */
283
+ const pageStripped = new Map<string, string>();
284
+
285
+ export function registerPageStripped(href: string, stripped: string): void {
286
+ pageStripped.set(href, stripped);
287
+ }
288
+
289
+ export function getPageStripped(href: string): string | undefined {
290
+ return pageStripped.get(href);
291
+ }
292
+
293
+ /**
294
+ * Original (pre-transform) file content, cached during the prePass so the
295
+ * page loop does not re-read the file from disk — one read per file.
296
+ */
297
+ const pageContent = new Map<string, string>();
298
+
299
+ export function registerPageContent(href: string, raw: string): void {
300
+ pageContent.set(href, raw);
301
+ }
302
+
303
+ export function getPageContent(href: string): string | undefined {
304
+ return pageContent.get(href);
305
+ }
306
+
248
307
  export async function compileMdxModule(
249
308
  rawMdx: string,
250
309
  remarkPlugins?: Pluggable[],
251
- rehypePlugins?: Pluggable[]
310
+ rehypePlugins?: Pluggable[],
311
+ /** Page href — registers the frontmatter (title/description/image/date)
312
+ * once for pagination and metadata consumers. */
313
+ href?: string
252
314
  ): Promise<string> {
253
315
  const serialized = await serializeWithDocPlugins(rawMdx, {
254
316
  outputFormat: "program",
255
317
  remarkPlugins,
256
318
  rehypePlugins,
257
319
  });
320
+ if (href) {
321
+ registerPageFrontmatter(href, serialized.frontmatter as Frontmatter);
322
+ registerPageStripped(href, serialized.strippedContent);
323
+ }
258
324
  return serialized.compiledSource;
259
325
  }
@@ -5,6 +5,7 @@ import { DOCS_DIR } from "./paths";
5
5
  import { readFileSync } from "node:fs";
6
6
  import { join } from "node:path";
7
7
  import { extractFrontmatter } from "@docubook/core";
8
+ import { getPageFrontmatter, registerPageFrontmatter } from "./mdx";
8
9
  import type { Frontmatter } from "./mdx";
9
10
 
10
11
  const docuConfig = loadDocuConfig();
@@ -46,29 +47,31 @@ export function getRouteMap(): Map<string, string> {
46
47
  return map;
47
48
  }
48
49
 
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 = "";
50
+ /**
51
+ * Frontmatter for a page — read from the parse-once registry (populated
52
+ * during compilation) instead of re-reading + re-parsing the file. Falls
53
+ * back to a direct read only for pages the dev server has not compiled yet,
54
+ * and caches the result back into the registry.
55
+ */
56
+ function readPageFrontmatter(href: string): Frontmatter {
57
+ const registered = getPageFrontmatter(href);
58
+ if (registered) return registered;
59
+
60
+ let fm: Frontmatter = {};
57
61
  const rel = href.replace(/^\/|$/g, "");
58
62
  for (const ext of [".mdx", ".md"]) {
59
63
  for (const file of [join(DOCS_DIR, `${rel}${ext}`), join(DOCS_DIR, `${rel}/index${ext}`)]) {
60
64
  try {
61
- const fm = extractFrontmatter<Frontmatter>(readFileSync(file, "utf-8"));
62
- description = typeof fm.description === "string" ? fm.description : "";
63
- if (description) break;
65
+ fm = extractFrontmatter<Frontmatter>(readFileSync(file, "utf-8"));
66
+ break;
64
67
  } catch {
65
68
  // not this file — try the next candidate
66
69
  }
67
70
  }
68
- if (description) break;
71
+ if (Object.keys(fm).length) break;
69
72
  }
70
- descriptionCache.set(href, description);
71
- return description;
73
+ registerPageFrontmatter(href, fm);
74
+ return fm;
72
75
  }
73
76
 
74
77
  export function getPreviousNext(pathname: string) {
@@ -83,12 +86,13 @@ export function getPreviousNext(pathname: string) {
83
86
  const routeMap = getRouteMap();
84
87
  const first = paths[0];
85
88
  if (!first) return { prev: null, next: null };
89
+ const fm = readPageFrontmatter(first);
86
90
  return {
87
91
  prev: null,
88
92
  next: {
89
93
  href: first,
90
- title: routeMap.get(first) || "",
91
- description: readDescription(first),
94
+ title: fm.title || routeMap.get(first) || "",
95
+ description: fm.description || "",
92
96
  },
93
97
  };
94
98
  }
@@ -105,13 +109,17 @@ export function getPreviousNext(pathname: string) {
105
109
  const prevHref = index > 0 ? paths[index - 1] : null;
106
110
  const nextHref = index < paths.length - 1 ? paths[index + 1] : null;
107
111
 
112
+ const prevFm = prevHref ? readPageFrontmatter(prevHref) : null;
113
+ const nextFm = nextHref ? readPageFrontmatter(nextHref) : null;
108
114
  return {
109
- prev: prevHref ? { href: prevHref, title: routeMap.get(prevHref) || "" } : null,
115
+ prev: prevHref
116
+ ? { href: prevHref, title: prevFm?.title || routeMap.get(prevHref) || "" }
117
+ : null,
110
118
  next: nextHref
111
119
  ? {
112
120
  href: nextHref,
113
- title: routeMap.get(nextHref) || "",
114
- description: readDescription(nextHref),
121
+ title: nextFm?.title || routeMap.get(nextHref) || "",
122
+ description: nextFm?.description || "",
115
123
  }
116
124
  : null,
117
125
  };
@@ -13,6 +13,7 @@
13
13
 
14
14
  import { readFile, writeFile, mkdir } from "node:fs/promises";
15
15
  import { resolve, join } from "node:path";
16
+ import { getPageContent } from "./mdx";
16
17
  import { extractFrontmatterWithContent } from "@docubook/core";
17
18
  import { frontmatterField } from "./mdx";
18
19
  import { DOCS_DIR, ASSETS_DIR, loadDocuConfig } from "./paths";
@@ -193,7 +194,9 @@ export async function generateSearchIndex(docsDir?: string, outputDir?: string):
193
194
  const mdxFiles = await scanMdxFiles(docs);
194
195
  const results = await Promise.all(
195
196
  mdxFiles.map(async (file) => {
196
- const raw = await readFile(file.absPath, "utf-8");
197
+ // Parse-once: reuse the prePass-cached raw content when available —
198
+ // no second disk read of every file.
199
+ const raw = getPageContent(`/${file.path}`) ?? (await readFile(file.absPath, "utf-8"));
197
200
  return extractRecords(file.path, raw);
198
201
  })
199
202
  );
@@ -9,7 +9,7 @@
9
9
  @plugin "@tailwindcss/typography";
10
10
  @source "../../.docu/components";
11
11
  @source "../../.docu/pages";
12
- @source "../../../ui-react/src";
12
+ @import "@docubook/ui-react/styles.css";
13
13
 
14
14
  /* daisyUI breadcrumbs underline every li child on hover (including plain
15
15
  spans) — crumbs are not links, so strip the underline (and pointer cursor)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docubook/flame",
3
- "version": "2.0.0-alpha.1",
3
+ "version": "2.0.0-beta.0",
4
4
  "description": "A blazing-fast React + MDX framework powered by Bun, built for modern documentation experiences.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -56,10 +56,10 @@
56
56
  "react-dom": "^19.2.7",
57
57
  "unified": "^11.0.0",
58
58
  "zod": "^4.4.3",
59
- "@docubook/core": "^2.0.0-alpha.1",
60
- "@docubook/markdown": "^2.0.0-alpha.1",
61
- "@docubook/themes-colors": "^2.0.0-alpha.1",
62
- "@docubook/ui-react": "^2.0.0-alpha.1"
59
+ "@docubook/core": "^2.0.0-beta.0",
60
+ "@docubook/markdown": "^2.0.0-beta.0",
61
+ "@docubook/themes-colors": "^2.0.0-beta.0",
62
+ "@docubook/ui-react": "^2.0.0-beta.0"
63
63
  },
64
64
  "peerDependencies": {
65
65
  "@sentry/bun": "^10.0.0"