@docubook/flame 2.0.0-beta.1 → 2.0.0-beta.3

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.
Files changed (60) hide show
  1. package/.docu/lib/build.deno.js +6 -9
  2. package/.docu/lib/build.deno.js.map +1 -0
  3. package/.docu/lib/build.impl-Bz_p421t.js +424 -0
  4. package/.docu/lib/build.impl-Bz_p421t.js.map +1 -0
  5. package/.docu/lib/build.impl-CtPrlYAE.js +2 -0
  6. package/.docu/lib/build.node.js +6 -9
  7. package/.docu/lib/build.node.js.map +1 -0
  8. package/.docu/lib/clean.js +25 -25
  9. package/.docu/lib/clean.js.map +1 -0
  10. package/.docu/lib/deno-DmEaKggj.js +20 -0
  11. package/.docu/lib/deno-DmEaKggj.js.map +1 -0
  12. package/.docu/lib/deploy.deno.js +9 -9
  13. package/.docu/lib/deploy.deno.js.map +1 -0
  14. package/.docu/lib/deploy.node.js +8 -8
  15. package/.docu/lib/deploy.node.js.map +1 -0
  16. package/.docu/lib/deploy.shared-fk8eM-r4.js +295 -0
  17. package/.docu/lib/deploy.shared-fk8eM-r4.js.map +1 -0
  18. package/.docu/lib/html.shared-FwgbE1WG.js +2696 -0
  19. package/.docu/lib/html.shared-FwgbE1WG.js.map +1 -0
  20. package/.docu/lib/logger-CQyNTE6L.js +306 -0
  21. package/.docu/lib/logger-CQyNTE6L.js.map +1 -0
  22. package/.docu/lib/node-DPTySdwG.js +80 -0
  23. package/.docu/lib/node-DPTySdwG.js.map +1 -0
  24. package/.docu/lib/paths-Bl2cdp9E.js +76 -0
  25. package/.docu/lib/paths-Bl2cdp9E.js.map +1 -0
  26. package/.docu/lib/preview.deno.js +7 -11
  27. package/.docu/lib/preview.deno.js.map +1 -0
  28. package/.docu/lib/preview.impl-CnsbLmDA.js +77 -0
  29. package/.docu/lib/preview.impl-CnsbLmDA.js.map +1 -0
  30. package/.docu/lib/preview.node.js +7 -11
  31. package/.docu/lib/preview.node.js.map +1 -0
  32. package/.docu/lib/server.deno.js +7 -12
  33. package/.docu/lib/server.deno.js.map +1 -0
  34. package/.docu/lib/server.impl-CXTzYqbF.js +358 -0
  35. package/.docu/lib/server.impl-CXTzYqbF.js.map +1 -0
  36. package/.docu/lib/server.node.js +7 -12
  37. package/.docu/lib/server.node.js.map +1 -0
  38. package/.docu/lib/utils-DA17MyQG.js +167 -0
  39. package/.docu/lib/utils-DA17MyQG.js.map +1 -0
  40. package/.docu/node/build.impl.ts +76 -11
  41. package/.docu/node/build.ts +83 -9
  42. package/.docu/node/cache-key.ts +311 -0
  43. package/.docu/node/hydrate.node.ts +124 -177
  44. package/.docu/node/hydrate.ts +13 -14
  45. package/.docu/node/mdx.ts +14 -2
  46. package/.docu/node/paths.ts +27 -1
  47. package/.docu/node/types.ts +23 -5
  48. package/bin/cli.js +56 -18
  49. package/bin/compile-lib.mjs +30 -28
  50. package/package.json +10 -16
  51. package/.docu/lib/build.impl-3ZOQORN3.js +0 -12
  52. package/.docu/lib/chunk-4IQXHHPF.js +0 -62
  53. package/.docu/lib/chunk-6EOUQCRM.js +0 -301
  54. package/.docu/lib/chunk-6HFBJUMG.js +0 -434
  55. package/.docu/lib/chunk-EOK6KATZ.js +0 -191
  56. package/.docu/lib/chunk-FERG25YW.js +0 -470
  57. package/.docu/lib/chunk-MQRFIDS4.js +0 -92
  58. package/.docu/lib/chunk-U7M5SK76.js +0 -330
  59. package/.docu/lib/chunk-UISOJ4RW.js +0 -114
  60. package/.docu/lib/chunk-URBATJEC.js +0 -2720
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Client bundle builder for Node/Deno runtimes.
3
3
  *
4
- * Wraps esbuild with the plugins needed to produce a browser-ready client
4
+ * Uses Vite with Rolldown plugins to produce a browser-ready client
5
5
  * bundle (JS + CSS) from the same components Bun.build handles natively.
6
6
  * Theme helpers (getThemeConfig, buildThemeCss, computeInlineThemeCss)
7
7
  * are re-exported from `hydrate.ts` — that module's `buildClientBundle`
@@ -13,8 +13,8 @@ import { builtinModules, createRequire } from "node:module";
13
13
  import { basename, dirname, join, resolve } from "node:path";
14
14
  import { existsSync, readFileSync, readdirSync } from "node:fs";
15
15
  import { promisify } from "node:util";
16
- import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
17
- import { createHash } from "node:crypto";
16
+ import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
17
+ import { build as viteBuild } from "vite";
18
18
  import {
19
19
  ASSETS_DIR,
20
20
  FRAMEWORK_ROOT,
@@ -24,6 +24,7 @@ import {
24
24
  loadDocuConfig,
25
25
  } from "./paths";
26
26
  import { buildThemeCss, getThemeConfig } from "./hydrate";
27
+ import { atomicWriteFile, computeTailwindCacheKey, readGlobalsCss } from "./cache-key";
27
28
  import { resolveRoutes } from "./fs-scanner";
28
29
  import { normalizeImporterPath } from "./security";
29
30
  import type { DocuConfig, DocuRoute } from "./types";
@@ -58,10 +59,9 @@ function resolveTailwindBin(): string {
58
59
  return join(dirname(pkgPath), binRel);
59
60
  }
60
61
 
61
- /** Compute a cache key from globals.css + theme config content. */
62
+ /** Compute a cache key from globals.css + theme + config + toolchain. */
62
63
  function tailwindCacheKey(): string {
63
- const globalsPath = join(STYLES_DIR, "globals.css");
64
- const globalsContent = existsSync(globalsPath) ? readFileSync(globalsPath, "utf-8") : "";
64
+ const globalsContent = readGlobalsCss();
65
65
  let themeSuffix = "";
66
66
  try {
67
67
  const themeColors = getThemeConfig();
@@ -71,10 +71,7 @@ function tailwindCacheKey(): string {
71
71
  } catch {
72
72
  // theme config unavailable — proceed without it
73
73
  }
74
- return createHash("sha256")
75
- .update(globalsContent + themeSuffix)
76
- .digest("hex")
77
- .slice(0, 16);
74
+ return computeTailwindCacheKey(globalsContent, themeSuffix);
78
75
  }
79
76
 
80
77
  /**
@@ -104,7 +101,7 @@ async function buildTailwindCss(key: string): Promise<{ file: string; content: s
104
101
  }
105
102
 
106
103
  let cssContent = await readFile(tmpCss, "utf-8");
107
- await unlink(tmpCss);
104
+ await unlink(tmpCss).catch(() => {});
108
105
 
109
106
  try {
110
107
  const themeColors = getThemeConfig();
@@ -119,11 +116,12 @@ async function buildTailwindCss(key: string): Promise<{ file: string; content: s
119
116
 
120
117
  // Use the same input-derived key for lookup and output — if inputs change,
121
118
  // the key changes, cache busting works without a separate content hash.
119
+ // Atomic tmp+rename with pid suffix: parallel builds never clobber each other.
122
120
  const cssFile = `client-${key}.css`;
123
121
  const outPath = join(ASSETS_DIR, cssFile);
124
122
 
125
123
  if (!existsSync(outPath)) {
126
- await writeFile(outPath, cssContent);
124
+ await atomicWriteFile(writeFile, rename, unlink, outPath, cssContent);
127
125
  }
128
126
 
129
127
  return { file: cssFile, content: cssContent };
@@ -203,180 +201,129 @@ export async function buildClientBundle(
203
201
  await cleanOldBundles(new Set([`client-${twKey}.css`]));
204
202
 
205
203
  const nodeEnv = process.env.NODE_ENV || "development";
206
- const esbuild = await import("esbuild");
207
- const { build } = esbuild;
208
204
 
209
205
  const entryPath = join(LIB_DIR, "client.ts");
210
- const workingDir = process.cwd();
211
- let result: Awaited<ReturnType<typeof build>>;
212
- try {
213
- result = await build({
214
- entryPoints: [entryPath],
215
- bundle: true,
216
- outdir: ASSETS_DIR,
217
- entryNames: "client-[hash]",
218
- platform: "browser",
219
- format: "esm",
220
- minify: nodeEnv === "production",
221
- define: { "process.env.NODE_ENV": JSON.stringify(nodeEnv) },
222
- jsx: "automatic",
223
- jsxDev: nodeEnv !== "production",
224
- metafile: true,
225
- logLevel: "silent",
226
- plugins: [
227
- {
228
- name: "node-builtin-stub",
229
- setup(build) {
230
- build.onResolve({ filter: NODE_BUILTINS_RE }, (args) => ({
231
- path: args.path,
232
- namespace: "node-stub",
233
- }));
234
- build.onLoad({ filter: /.*/, namespace: "node-stub" }, () => ({
235
- contents: "module.exports = {};",
236
- loader: "js",
237
- }));
238
- },
206
+ const bundle = await viteBuild({
207
+ configFile: false,
208
+ publicDir: false,
209
+ define: { "process.env.NODE_ENV": JSON.stringify(nodeEnv) },
210
+ plugins: [
211
+ {
212
+ name: "node-builtin-stub",
213
+ resolveId(id) {
214
+ if (NODE_BUILTINS_RE.test(id)) return `\0node-stub:${id}`;
215
+ return null;
239
216
  },
240
- {
241
- name: "lucide-optimize",
242
- setup(build) {
243
- build.onResolve({ filter: /^lucide-react$/ }, (args) => {
244
- // Imports from within our virtual module go to the real package.
245
- if (args.namespace === "lucide-virt") {
246
- return { path: getLucideRealEntry(), namespace: "file" };
247
- }
248
- // markdown Icon.tsx uses namespace import for arbitrary
249
- // user-provided icon names in MDX — keep full barrel there.
250
- if (args.importer) {
251
- const normalized = normalizeImporterPath(args.importer);
252
- if (normalized.includes("/markdown/dist/")) {
253
- return { path: getLucideRealEntry(), namespace: "file" };
254
- }
255
- }
256
- // All other files get tree-shaken via the virtual module.
257
- // Lucide.tsx renders only config-defined icons, which are
258
- // collected by extractConfigIcons() + collectAllLucideIcons().
259
- return { path: args.path, namespace: "lucide-virt" };
260
- });
261
- build.onLoad({ filter: /.*/, namespace: "lucide-virt" }, () => {
262
- const scanned = collectAllLucideIcons();
263
- const configured = extractConfigIcons(loadDocuConfig());
264
- const allIcons = [...new Set([...scanned, ...configured])];
265
- return {
266
- contents: `export { ${allIcons.join(", ")} } from "lucide-react";`,
267
- loader: "js",
268
- };
269
- });
270
- },
217
+ load(id) {
218
+ if (!id.startsWith("\0node-stub:")) return null;
219
+ return "export default {}; export {};";
271
220
  },
272
- {
273
- name: "docu-config",
274
- setup(build) {
275
- // Components import as "../node/client-routes" (no .ts extension),
276
- // so filter matches the path tail without requiring the extension.
277
- build.onResolve({ filter: /client-routes$/ }, (args) => ({
278
- path: args.path,
279
- namespace: "client-routes",
280
- }));
281
- build.onLoad({ filter: /.*/, namespace: "client-routes" }, () => {
282
- const config = loadDocuConfig();
283
- const resolved = {
284
- ...config,
285
- routes: resolveRoutes(config.routes as DocuRoute[] | undefined),
286
- };
287
- return {
288
- contents: [
289
- `import type { DocuRoute, DocuConfig } from "./types";`,
290
- `const docuConfig = ${JSON.stringify(resolved)};`,
291
- `export const routes = docuConfig.routes || [];`,
292
- `export const config = docuConfig;`,
293
- ].join("\n"),
294
- loader: "ts",
295
- };
296
- });
297
- },
221
+ },
222
+ {
223
+ name: "lucide-optimize",
224
+ resolveId(id, importer) {
225
+ if (id !== "lucide-react") return null;
226
+ if (importer) {
227
+ const normalized = normalizeImporterPath(importer);
228
+ if (normalized.includes("/markdown/dist/")) return null;
229
+ }
230
+ return "\0lucide-virt";
298
231
  },
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
- },
232
+ load(id) {
233
+ if (id !== "\0lucide-virt") return null;
234
+ const scanned = collectAllLucideIcons();
235
+ const configured = extractConfigIcons(loadDocuConfig());
236
+ const allIcons = [...new Set([...scanned, ...configured])];
237
+ return `export { ${allIcons.join(", ")} } from ${JSON.stringify(getLucideRealEntry())};`;
361
238
  },
362
- ],
363
- });
364
- } finally {
365
- // esbuild's service child process keeps Deno's event loop alive after a
366
- // one-shot build (node-compat gap) — stop it so `flame build` exits.
367
- await esbuild.stop();
368
- }
369
-
370
- // The single entry produces one output. Match resolved source path.
371
- const { outputs } = result.metafile!;
372
- const jsOutput = Object.keys(outputs).find((p) => {
373
- const o = outputs[p];
374
- return o.entryPoint && resolve(workingDir, o.entryPoint) === entryPath;
239
+ },
240
+ {
241
+ name: "docu-config",
242
+ resolveId(id) {
243
+ if (!/client-routes$/.test(id)) return null;
244
+ return "\0client-routes";
245
+ },
246
+ load(id) {
247
+ if (id !== "\0client-routes") return null;
248
+ const config = loadDocuConfig();
249
+ const resolved = {
250
+ ...config,
251
+ routes: resolveRoutes(config.routes as DocuRoute[] | undefined),
252
+ };
253
+ return [
254
+ `const docuConfig = ${JSON.stringify(resolved)};`,
255
+ `export const routes = docuConfig.routes || [];`,
256
+ `export const config = docuConfig;`,
257
+ ].join("\n");
258
+ },
259
+ },
260
+ {
261
+ name: "mdx-hydrate",
262
+ resolveId(id) {
263
+ if (/mdx-manifest$/.test(id)) return "\0mdx-manifest";
264
+ if (id.startsWith("mdx-module:")) return `\0${id}`;
265
+ return null;
266
+ },
267
+ load(id) {
268
+ if (id === "\0mdx-manifest") {
269
+ const slugs = Object.keys(mdxSources).sort();
270
+ const imports = slugs
271
+ .map((slug, i) => {
272
+ const key = slug.replace(/["\\]/g, "");
273
+ return `import * as _mdx${i} from ${JSON.stringify(`mdx-module:${key}`)};`;
274
+ })
275
+ .join("\n");
276
+ const map = slugs.map((slug, i) => `${JSON.stringify(slug)}: _mdx${i}`).join(", ");
277
+ return `${imports}\nexport const mdxModules = { ${map} };\n`;
278
+ }
279
+ if (!id.startsWith("\0mdx-module:")) return null;
280
+ const slug = id.slice("\0mdx-module:".length);
281
+ const contents = mdxSources[slug];
282
+ if (contents == null) {
283
+ throw new Error(`unknown mdx module: ${slug}`);
284
+ }
285
+ return contents;
286
+ },
287
+ },
288
+ ],
289
+ build: {
290
+ outDir: ASSETS_DIR,
291
+ emptyOutDir: false,
292
+ sourcemap: true,
293
+ minify: nodeEnv === "production",
294
+ target: "es2020",
295
+ rollupOptions: {
296
+ input: entryPath,
297
+ output: {
298
+ format: "es",
299
+ entryFileNames: "client-[hash].js",
300
+ chunkFileNames: "chunks/[name]-[hash].js",
301
+ assetFileNames: "assets/[name]-[hash][extname]",
302
+ },
303
+ },
304
+ },
375
305
  });
376
- if (!jsOutput) {
306
+
307
+ const outputs = Array.isArray(bundle) ? bundle : [bundle];
308
+ let jsFile: string | undefined;
309
+ for (const item of outputs) {
310
+ if (!("output" in item)) continue;
311
+ for (const output of item.output) {
312
+ if (
313
+ output.type === "chunk" &&
314
+ output.isEntry &&
315
+ output.facadeModuleId &&
316
+ resolve(output.facadeModuleId) === entryPath
317
+ ) {
318
+ jsFile = basename(output.fileName);
319
+ break;
320
+ }
321
+ }
322
+ if (jsFile) break;
323
+ }
324
+ if (!jsFile) {
377
325
  throw new Error("Client bundle produced no output files");
378
326
  }
379
- const jsFile = basename(jsOutput);
380
327
 
381
328
  const { file: cssFile } = await buildTailwindCss(twKey);
382
329
 
@@ -1,9 +1,9 @@
1
1
  import { join } from "node:path";
2
- import { mkdir, unlink } from "node:fs/promises";
3
- import { existsSync, readFileSync } from "node:fs";
4
- import { createHash } from "node:crypto";
2
+ import { mkdir, unlink, rename } from "node:fs/promises";
3
+ import { existsSync } from "node:fs";
5
4
  import { resolveTheme, generateThemeCss, presetRegistry } from "@docubook/themes-colors";
6
5
  import { ASSETS_DIR, cleanOldBundles, LIB_DIR, STYLES_DIR, loadDocuConfig } from "./paths";
6
+ import { atomicWriteFile, computeTailwindCacheKey, readGlobalsCss } from "./cache-key";
7
7
  import { resolveRoutes } from "./fs-scanner";
8
8
  import type { DocuRoute } from "./types";
9
9
  import type { ThemeConfig } from "@docubook/themes-colors";
@@ -57,10 +57,9 @@ export function computeInlineThemeCss(): string | undefined {
57
57
  return undefined;
58
58
  }
59
59
 
60
- /** Compute Tailwind cache key from globals.css + theme config. */
60
+ /** Compute Tailwind cache key from globals.css + theme + config + toolchain. */
61
61
  function twCacheKey(): string {
62
- const globalsPath = join(STYLES_DIR, "globals.css");
63
- const globals = existsSync(globalsPath) ? readFileSync(globalsPath, "utf-8") : "";
62
+ const globals = readGlobalsCss();
64
63
  let themeSuffix = "";
65
64
  try {
66
65
  const themeColors = getThemeConfig();
@@ -68,10 +67,7 @@ function twCacheKey(): string {
68
67
  } catch {
69
68
  // theme config unavailable — proceed without
70
69
  }
71
- return createHash("sha256")
72
- .update(globals + themeSuffix)
73
- .digest("hex")
74
- .slice(0, 16);
70
+ return computeTailwindCacheKey(globals, themeSuffix);
75
71
  }
76
72
 
77
73
  /** Run Tailwind CLI, caching by content hash. */
@@ -105,7 +101,7 @@ async function buildTailwindCss(key: string): Promise<{ file: string; content: s
105
101
  }
106
102
 
107
103
  let cssContent = await Bun.file(tmpCss).text();
108
- await unlink(tmpCss);
104
+ await unlink(tmpCss).catch(() => {});
109
105
 
110
106
  try {
111
107
  const themeColors = getThemeConfig();
@@ -116,11 +112,14 @@ async function buildTailwindCss(key: string): Promise<{ file: string; content: s
116
112
  );
117
113
  }
118
114
 
119
- // Use the same input-derived key for lookup and output — if inputs change,
120
- // the key changes, cache busting works without a separate content hash.
121
115
  const cssFile = `client-${key}.css`;
122
116
  const outPath = join(ASSETS_DIR, cssFile);
123
- if (!existsSync(outPath)) await Bun.write(outPath, cssContent);
117
+ // Atomic tmp+rename with pid suffix: parallel builds never clobber each
118
+ // other, and a crash cannot leave a half-written CSS file behind.
119
+ if (!existsSync(outPath)) {
120
+ const { writeFile } = await import("node:fs/promises");
121
+ await atomicWriteFile(writeFile, rename, unlink, outPath, cssContent);
122
+ }
124
123
 
125
124
  return { file: cssFile, content: cssContent };
126
125
  }
package/.docu/node/mdx.ts CHANGED
@@ -174,7 +174,10 @@ async function serializeWithDocPlugins(
174
174
  // Parse-once: when the prePass already extracted the frontmatter + stripped
175
175
  // content, reuse it instead of re-parsing (the SSR phase skips extraction).
176
176
  const { strippedContent, frontmatter } =
177
- pre ?? extractFrontmatterWithContent<Frontmatter>(rawMdx, opts.frontmatterSchema);
177
+ pre ??
178
+ (opts.frontmatterSchema
179
+ ? extractFrontmatterWithContent<Frontmatter>(rawMdx, opts.frontmatterSchema)
180
+ : extractFrontmatterWithContent<Frontmatter>(rawMdx));
178
181
 
179
182
  const defaultRemark = createDefaultRemarkPlugins();
180
183
  const defaultRehype = createDefaultRehypePlugins();
@@ -221,7 +224,9 @@ export async function compileMdx(
221
224
  const tocs = extractTocsFromRawMdx(rawMdx);
222
225
  const frontmatter =
223
226
  pre?.frontmatter ??
224
- extractFrontmatterWithContent<Frontmatter>(rawMdx, frontmatterSchema).frontmatter;
227
+ (frontmatterSchema
228
+ ? extractFrontmatterWithContent<Frontmatter>(rawMdx, frontmatterSchema).frontmatter
229
+ : extractFrontmatterWithContent<Frontmatter>(rawMdx).frontmatter);
225
230
  const serialized = await serializeWithDocPlugins(
226
231
  rawMdx,
227
232
  { remarkPlugins, rehypePlugins, frontmatterSchema },
@@ -304,6 +309,13 @@ export function getPageContent(href: string): string | undefined {
304
309
  return pageContent.get(href);
305
310
  }
306
311
 
312
+ /** Drop derived page maps under OS memory pressure (Bun 1.4 `memoryPressure`). */
313
+ export function clearDerivedPageCaches(): void {
314
+ pageContent.clear();
315
+ pageStripped.clear();
316
+ pageFrontmatter.clear();
317
+ }
318
+
307
319
  export async function compileMdxModule(
308
320
  rawMdx: string,
309
321
  remarkPlugins?: Pluggable[],
@@ -10,7 +10,28 @@ import type { DocuConfig } from "./types";
10
10
 
11
11
  // .docu/node/paths.ts → package root is 2 levels up
12
12
  export const FRAMEWORK_ROOT = resolve(import.meta.dirname, "../..");
13
- export const PROJECT_ROOT = process.cwd();
13
+
14
+ /**
15
+ * Find the project root by walking up from cwd until docu.json is found.
16
+ * Falls back to cwd when not found (tests, ad-hoc scripts). Bun 1.4
17
+ * `run --parallel --filter` may change cwd per workspace task, so a bare
18
+ * process.cwd() can point at the wrong package.
19
+ */
20
+ function findProjectRoot(): string {
21
+ let dir = process.cwd();
22
+ for (let i = 0; i < 6; i++) {
23
+ try {
24
+ if (existsSync(join(dir, "docu.json"))) return dir;
25
+ } catch {
26
+ break;
27
+ }
28
+ const parent = resolve(dir, "..");
29
+ if (parent === dir) break;
30
+ dir = parent;
31
+ }
32
+ return process.cwd();
33
+ }
34
+ export const PROJECT_ROOT = findProjectRoot();
14
35
 
15
36
  // Framework paths (internal)
16
37
  export const PAGES_DIR = join(FRAMEWORK_ROOT, ".docu/pages");
@@ -29,6 +50,11 @@ export const DOCS_DIR = join(PROJECT_ROOT, "docs");
29
50
  export const DOCS_ASSETS_DIR = join(PROJECT_ROOT, "docs/assets");
30
51
  export const DOCU_CONFIG_PATH = join(PROJECT_ROOT, "docu.json");
31
52
 
53
+ /** Resolve a project-relative file against the discovered PROJECT_ROOT. */
54
+ export function resolveProjectFile(...segments: string[]): string {
55
+ return join(PROJECT_ROOT, ...segments);
56
+ }
57
+
32
58
  // Config singleton
33
59
  let _config: DocuConfig | null = null;
34
60
 
@@ -93,12 +93,30 @@ export interface DocuConfig {
93
93
  plugins?: PluginEntry[];
94
94
  }
95
95
 
96
+ export interface BuildCacheEntry {
97
+ hash: string;
98
+ mtime: number;
99
+ builtAt: number;
100
+ }
101
+
102
+ /** Version stamp stored as `__meta__` — extends entry so the index signature stays valid. */
103
+ export interface BuildCacheMeta extends BuildCacheEntry {
104
+ version: number;
105
+ runtime: string;
106
+ }
107
+
96
108
  export interface BuildCache {
97
- [path: string]: {
98
- hash: string;
99
- mtime: number;
100
- builtAt: number;
101
- };
109
+ [path: string]: BuildCacheEntry;
110
+ __meta__?: BuildCacheMeta;
111
+ __assets__?: BuildCacheEntry;
112
+ __bundle__?: BuildCacheEntry;
113
+ }
114
+
115
+ /** Narrow a cache slot to a real page/asset entry (guards `__meta__` shape). */
116
+ export function isCacheEntry(value: unknown): value is BuildCacheEntry {
117
+ if (typeof value !== "object" || value === null) return false;
118
+ const v = value as Record<string, unknown>;
119
+ return typeof v.hash === "string" && typeof v.mtime === "number" && typeof v.builtAt === "number";
102
120
  }
103
121
 
104
122
  export interface CliArgs {
package/bin/cli.js CHANGED
@@ -7,9 +7,35 @@ import { spawnSync } from "node:child_process";
7
7
 
8
8
  const __dirname = import.meta.dirname;
9
9
 
10
+ // Detect the package manager that invoked this binary. npm, pnpm, bun, and
11
+ // yarn set npm_config_user_agent when they run a bin; fall back to lockfiles
12
+ // in the target directory for direct invocations (e.g. `node bin/cli.js`).
13
+ function detectPkgManager(dir) {
14
+ const userAgent = process.env.npm_config_user_agent || "";
15
+ // pnpm/yarn UA strings also contain "npm/?" — match specific tools first.
16
+ if (/bun\//.test(userAgent)) return "bun";
17
+ if (/pnpm\//.test(userAgent)) return "pnpm";
18
+ if (/yarn\//.test(userAgent)) return "yarn";
19
+ if (/npm\//.test(userAgent)) return "npm";
20
+ if (existsSync(join(dir, "bun.lock"))) return "bun";
21
+ if (existsSync(join(dir, "pnpm-lock.yaml"))) return "pnpm";
22
+ if (existsSync(join(dir, "yarn.lock"))) return "yarn";
23
+ if (existsSync(join(dir, "package-lock.json"))) return "npm";
24
+ return "npm";
25
+ }
26
+
27
+ // Explicit `--bun` on the flame CLI itself (e.g. `flame dev --bun`).
28
+ // Distinct from `bun run --bun dev` (consumed by bun, never reaches here).
29
+ // Stripped so downstream entries don't see an unknown flag.
30
+ const bunFlagIndex = process.argv.indexOf("--bun");
31
+ const wantBun = bunFlagIndex !== -1;
32
+ if (wantBun) process.argv.splice(bunFlagIndex, 1);
33
+
10
34
  // Runtime detection — override with FLAME_RUNTIME=bun|node|deno for testing.
11
35
  // Deno's npm compat layer may expose `Bun` via globals, check execPath first.
12
- const runtime =
36
+ // The shebang (#!/usr/bin/env node) hides `typeof Bun` under `bun run`, so a
37
+ // bun PM also selects the bun runtime; the re-exec below lands on it.
38
+ let runtime =
13
39
  process.env.FLAME_RUNTIME ||
14
40
  (process.execPath.includes("deno")
15
41
  ? "deno"
@@ -18,6 +44,35 @@ const runtime =
18
44
  : typeof Deno !== "undefined"
19
45
  ? "deno"
20
46
  : "node");
47
+ if (wantBun) runtime = "bun";
48
+ else if (
49
+ !process.env.FLAME_RUNTIME &&
50
+ runtime === "node" &&
51
+ detectPkgManager(process.cwd()) === "bun"
52
+ )
53
+ runtime = "bun";
54
+
55
+ // Re-exec under `bun` when selected but this process is node. One spawn per
56
+ // command; FLAME_REEXEC guards the loop; silent node fallback when `bun` is
57
+ // missing unless explicitly requested.
58
+ if (
59
+ runtime === "bun" &&
60
+ typeof Bun === "undefined" &&
61
+ !process.execPath.includes("deno") &&
62
+ !process.env.FLAME_REEXEC
63
+ ) {
64
+ const result = spawnSync("bun", [process.argv[1], ...process.argv.slice(2)], {
65
+ stdio: "inherit",
66
+ env: { ...process.env, FLAME_REEXEC: "1" },
67
+ });
68
+ if (result.error?.code === "ENOENT") {
69
+ if (wantBun || process.env.FLAME_RUNTIME)
70
+ console.error("flame: --bun requested but `bun` not found in PATH, falling back to node.");
71
+ runtime = "node";
72
+ } else {
73
+ process.exit(result.status ?? 0);
74
+ }
75
+ }
21
76
 
22
77
  const COMMAND_MAP = {
23
78
  bun: {
@@ -50,23 +105,6 @@ if (!COMMAND_MAP) {
50
105
 
51
106
  const command = process.argv[2];
52
107
 
53
- // Detect the package manager that invoked this binary. npm, pnpm, bun, and
54
- // yarn set npm_config_user_agent when they run a bin; fall back to lockfiles
55
- // in the target directory for direct invocations (e.g. `node bin/cli.js`).
56
- function detectPkgManager(dir) {
57
- const userAgent = process.env.npm_config_user_agent || "";
58
- // pnpm/yarn UA strings also contain "npm/?" — match specific tools first.
59
- if (/bun\//.test(userAgent)) return "bun";
60
- if (/pnpm\//.test(userAgent)) return "pnpm";
61
- if (/yarn\//.test(userAgent)) return "yarn";
62
- if (/npm\//.test(userAgent)) return "npm";
63
- if (existsSync(join(dir, "bun.lock"))) return "bun";
64
- if (existsSync(join(dir, "pnpm-lock.yaml"))) return "pnpm";
65
- if (existsSync(join(dir, "yarn.lock"))) return "yarn";
66
- if (existsSync(join(dir, "package-lock.json"))) return "npm";
67
- return "npm";
68
- }
69
-
70
108
  // Parse flags
71
109
  const themeIndex = process.argv.indexOf("--theme");
72
110
  if (themeIndex !== -1 && themeIndex + 1 < process.argv.length) {