@docubook/flame 2.0.0-beta.5 → 2.0.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.
Files changed (58) hide show
  1. package/.docu/components/Theme.tsx +42 -34
  2. package/.docu/components/home/BackgroundBlobs.tsx +31 -0
  3. package/.docu/components/home/index.tsx +1 -0
  4. package/.docu/lib/build.deno.js +1 -1
  5. package/.docu/lib/build.impl--Pm9uO30.js +2 -0
  6. package/.docu/lib/{build.impl-BBlR3si3.js → build.impl-CsP6pPug.js} +60 -29
  7. package/.docu/lib/build.impl-CsP6pPug.js.map +1 -0
  8. package/.docu/lib/build.node.js +1 -1
  9. package/.docu/lib/clean.js +1 -1
  10. package/.docu/lib/deploy.deno.js +1 -1
  11. package/.docu/lib/deploy.node.js +1 -1
  12. package/.docu/lib/{deploy.shared-Dxv3lsOb.js → deploy.shared-CsEa3L1D.js} +45 -17
  13. package/.docu/lib/deploy.shared-CsEa3L1D.js.map +1 -0
  14. package/.docu/lib/{html.shared-DBMwv_Z6.js → html.shared-CsEFmy_Y.js} +181 -158
  15. package/.docu/lib/html.shared-CsEFmy_Y.js.map +1 -0
  16. package/.docu/lib/{logger-DYmvFF1d.js → logger-DLU3A-z2.js} +4 -3
  17. package/.docu/lib/logger-DLU3A-z2.js.map +1 -0
  18. package/.docu/lib/{paths-Bl2cdp9E.js → paths-Cbz-dj4s.js} +5 -9
  19. package/.docu/lib/{paths-Bl2cdp9E.js.map → paths-Cbz-dj4s.js.map} +1 -1
  20. package/.docu/lib/preview.deno.js +1 -1
  21. package/.docu/lib/{preview.impl-anVcZv2Z.js → preview.impl-CaZTgmnB.js} +3 -3
  22. package/.docu/lib/{preview.impl-anVcZv2Z.js.map → preview.impl-CaZTgmnB.js.map} +1 -1
  23. package/.docu/lib/preview.node.js +1 -1
  24. package/.docu/lib/server.deno.js +1 -1
  25. package/.docu/lib/{server.impl-Cvoa6Diq.js → server.impl-D-CgD1bn.js} +43 -52
  26. package/.docu/lib/server.impl-D-CgD1bn.js.map +1 -0
  27. package/.docu/lib/server.node.js +1 -1
  28. package/.docu/node/build.impl.ts +71 -34
  29. package/.docu/node/build.ts +68 -34
  30. package/.docu/node/cache-key.ts +8 -4
  31. package/.docu/node/client.ts +36 -27
  32. package/.docu/node/deploy.shared.ts +44 -18
  33. package/.docu/node/deploy.ts +5 -20
  34. package/.docu/node/home-client.ts +6 -0
  35. package/.docu/node/html.shared.ts +5 -4
  36. package/.docu/node/html.ts +4 -3
  37. package/.docu/node/hydrate.node.ts +87 -115
  38. package/.docu/node/hydrate.ts +74 -80
  39. package/.docu/node/mdx-manifest.d.ts +2 -2
  40. package/.docu/node/paths.ts +2 -9
  41. package/.docu/node/plugin-builder.ts +25 -13
  42. package/.docu/node/plugin.ts +33 -9
  43. package/.docu/node/server-routes.ts +59 -50
  44. package/.docu/node/server.impl.ts +4 -2
  45. package/.docu/node/server.ts +12 -16
  46. package/.docu/node/types.ts +11 -0
  47. package/.docu/pages/404.tsx +12 -9
  48. package/.docu/pages/index.tsx +8 -29
  49. package/.docu/styles/base.css +128 -0
  50. package/.docu/styles/globals.css +5 -137
  51. package/.docu/styles/site.css +7 -0
  52. package/package.json +6 -5
  53. package/.docu/lib/build.impl-BBlR3si3.js.map +0 -1
  54. package/.docu/lib/build.impl-DpoDUmei.js +0 -2
  55. package/.docu/lib/deploy.shared-Dxv3lsOb.js.map +0 -1
  56. package/.docu/lib/html.shared-DBMwv_Z6.js.map +0 -1
  57. package/.docu/lib/logger-DYmvFF1d.js.map +0 -1
  58. package/.docu/lib/server.impl-Cvoa6Diq.js.map +0 -1
@@ -1,16 +1,8 @@
1
- /**
2
- * Client bundle builder for Node/Deno runtimes.
3
- *
4
- * Uses Vite with Rolldown plugins to produce a browser-ready client
5
- * bundle (JS + CSS) from the same components Bun.build handles natively.
6
- * Theme helpers (getThemeConfig, buildThemeCss, computeInlineThemeCss)
7
- * are re-exported from `hydrate.ts` — that module's `buildClientBundle`
8
- * is Bun-only and unused here.
9
- */
1
+ /** Client bundle builder for Node/Deno runtimes. */
10
2
 
11
3
  import { execFile } from "node:child_process";
12
4
  import { builtinModules, createRequire } from "node:module";
13
- import { basename, dirname, join, resolve } from "node:path";
5
+ import { dirname, join, resolve } from "node:path";
14
6
  import { existsSync, readFileSync, readdirSync } from "node:fs";
15
7
  import { promisify } from "node:util";
16
8
  import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
@@ -23,34 +15,38 @@ import {
23
15
  STYLES_DIR,
24
16
  loadDocuConfig,
25
17
  } from "./paths";
26
- import { buildThemeCss, getThemeConfig } from "./hydrate";
27
- import { atomicWriteFile, computeTailwindCacheKey, readGlobalsCss } from "./cache-key";
18
+ import { buildThemeCss, createMdxModuleEntries, getThemeConfig } from "./hydrate";
19
+ import { atomicWriteFile, computeTailwindCacheKey, readStyleCss } from "./cache-key";
28
20
  import { resolveRoutes } from "./fs-scanner";
29
21
  import { normalizeImporterPath } from "./security";
30
- import type { DocuConfig, DocuRoute } from "./types";
22
+ import type { AssetManifest, DocuConfig, DocuRoute } from "./types";
31
23
 
32
24
  /** Extract Lucide icon names from user docu.json configuration. */
33
25
  function extractConfigIcons(config: DocuConfig): string[] {
34
26
  const icons: string[] = [];
35
- const pushIf = (s: string | undefined) => {
36
- if (s) icons.push(s);
27
+ const pushIf = (value: string | undefined) => {
28
+ if (value) icons.push(value);
37
29
  };
38
- config.home?.hero?.actions?.forEach((a) => pushIf(a.icon));
39
- config.home?.features?.forEach((f) => pushIf(f.icon));
30
+ config.home?.hero?.actions?.forEach((action) => pushIf(action.icon));
31
+ config.home?.features?.forEach((feature) => pushIf(feature.icon));
40
32
  (function walk(routes: DocuRoute[]) {
41
- for (const r of routes) {
42
- pushIf(r.context?.icon);
43
- if (r.items) walk(r.items);
33
+ for (const route of routes) {
34
+ pushIf(route.context?.icon);
35
+ if (route.items) walk(route.items);
44
36
  }
45
37
  })(config.routes ?? []);
46
- return [...new Set(icons.filter((n) => /^[A-Z]/.test(n)))];
38
+ return [...new Set(icons.filter((name) => /^[A-Z]/.test(name)))];
47
39
  }
48
40
 
49
41
  export { buildThemeCss, computeInlineThemeCss, getThemeConfig } from "./hydrate";
50
42
 
43
+ export function viteChunkFileName(chunk: { name: string }): string {
44
+ const name = chunk.name.replace(/^_docubook-mdx-page-/, "docubook-mdx-page-");
45
+ return `chunks/${name}-[hash].js`;
46
+ }
47
+
51
48
  const execFileAsync = promisify(execFile);
52
49
 
53
- /** Resolve the @tailwindcss/cli binary path from the installed package. */
54
50
  function resolveTailwindBin(): string {
55
51
  const require = createRequire(import.meta.url);
56
52
  const pkgPath = require.resolve("@tailwindcss/cli/package.json");
@@ -59,40 +55,36 @@ function resolveTailwindBin(): string {
59
55
  return join(dirname(pkgPath), binRel);
60
56
  }
61
57
 
62
- /** Compute a cache key from globals.css + theme + config + toolchain. */
63
- function tailwindCacheKey(): string {
64
- const globalsContent = readGlobalsCss();
65
- let themeSuffix = "";
58
+ function themeCacheSuffix(): string {
66
59
  try {
67
60
  const themeColors = getThemeConfig();
68
- if (themeColors) {
69
- themeSuffix = JSON.stringify(themeColors);
70
- }
61
+ return themeColors ? JSON.stringify(themeColors) : "";
71
62
  } catch {
72
- // theme config unavailable — proceed without it
63
+ return "";
73
64
  }
74
- return computeTailwindCacheKey(globalsContent, themeSuffix);
75
65
  }
76
66
 
77
- /**
78
- * Build Tailwind CSS with content-based caching.
79
- * If a CSS file for the current input hash already exists, skip the subprocess.
80
- * Returns the filename (e.g. "client-abc123.css") and CSS content.
81
- */
82
- async function buildTailwindCss(key: string): Promise<{ file: string; content: string }> {
83
- const cachedFile = `client-${key}.css`;
67
+ function tailwindCacheKey(styleFile: string): string {
68
+ return computeTailwindCacheKey(readStyleCss(styleFile), `${styleFile}\0${themeCacheSuffix()}`);
69
+ }
70
+
71
+ async function buildTailwindCss(
72
+ name: string,
73
+ styleFile: string
74
+ ): Promise<{ file: string; content: string }> {
75
+ const key = tailwindCacheKey(styleFile);
76
+ const cachedFile = `${name}-${key}.css`;
84
77
  const cachedPath = join(ASSETS_DIR, cachedFile);
85
78
 
86
79
  if (existsSync(cachedPath)) {
87
- const content = readFileSync(cachedPath, "utf-8");
88
- return { file: cachedFile, content };
80
+ return { file: cachedFile, content: readFileSync(cachedPath, "utf-8") };
89
81
  }
90
82
 
91
- const tmpCss = join(ASSETS_DIR, `_tmp-${key}.css`);
83
+ const tmpCss = join(ASSETS_DIR, `_tmp-${name}-${key}.css`);
92
84
  const bin = resolveTailwindBin();
93
- const twArgs = ["-i", join(STYLES_DIR, "globals.css"), "-o", tmpCss, "--minify"];
85
+ const tailwindArgs = ["-i", join(STYLES_DIR, styleFile), "-o", tmpCss, "--minify"];
94
86
  const isDeno = "Deno" in globalThis;
95
- const args = isDeno ? ["run", "-A", bin, ...twArgs] : [bin, ...twArgs];
87
+ const args = isDeno ? ["run", "-A", bin, ...tailwindArgs] : [bin, ...tailwindArgs];
96
88
  try {
97
89
  await execFileAsync(process.execPath, args, { maxBuffer: 16 * 1024 * 1024 });
98
90
  } catch (err) {
@@ -105,35 +97,26 @@ async function buildTailwindCss(key: string): Promise<{ file: string; content: s
105
97
 
106
98
  try {
107
99
  const themeColors = getThemeConfig();
108
- if (themeColors) {
109
- cssContent = buildThemeCss(cssContent, themeColors);
110
- }
100
+ if (themeColors) cssContent = buildThemeCss(cssContent, themeColors);
111
101
  } catch (err) {
112
102
  console.warn(
113
- `[flame] Failed to resolve theme config, falling back to globals.css only: ${err instanceof Error ? err.message : String(err)}`
103
+ `[flame] Failed to resolve theme config: ${err instanceof Error ? err.message : String(err)}`
114
104
  );
115
105
  }
116
106
 
117
- // Use the same input-derived key for lookup and output — if inputs change,
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.
120
- const cssFile = `client-${key}.css`;
121
- const outPath = join(ASSETS_DIR, cssFile);
122
-
123
- if (!existsSync(outPath)) {
124
- await atomicWriteFile(writeFile, rename, unlink, outPath, cssContent);
107
+ if (!existsSync(cachedPath)) {
108
+ await atomicWriteFile(writeFile, rename, unlink, cachedPath, cssContent);
125
109
  }
126
110
 
127
- return { file: cssFile, content: cssContent };
111
+ return { file: cachedFile, content: cssContent };
128
112
  }
129
113
 
130
114
  const NODE_BUILTINS_RE = new RegExp(
131
- `^(node:.*|${builtinModules.map((m) => m.replace(/\//g, "\\/")).join("|")})$`
115
+ `^(node:.*|${builtinModules.map((module) => module.replace(/\//g, "\\/")).join("|")})$`
132
116
  );
133
117
 
134
118
  let lucideRealEntry: string | undefined;
135
119
 
136
- /** Resolve the real lucide-react entry path once (cached). */
137
120
  function getLucideRealEntry(): string {
138
121
  if (!lucideRealEntry) {
139
122
  lucideRealEntry = createRequire(import.meta.url).resolve("lucide-react");
@@ -144,20 +127,19 @@ function getLucideRealEntry(): string {
144
127
  const LUCIDE_IMPORT_RE = /import\s*\{([^}]+)\}\s*from\s*["']lucide-react["']/g;
145
128
  const LUCIDE_ICON_RE = /^[A-Z]/;
146
129
 
147
- /** Walk a directory scanning JS/TS/TSX files for `lucide-react` named imports. */
148
130
  function scanDirLucideIcons(dir: string, set: Set<string>): void {
149
131
  if (!existsSync(dir)) return;
150
132
  try {
151
133
  const entries = readdirSync(dir, { withFileTypes: true });
152
- for (const e of entries) {
153
- const full = join(dir, e.name);
154
- if (e.isDirectory()) {
155
- if (e.name !== "node_modules") scanDirLucideIcons(full, set);
156
- } else if (/\.(js|ts|tsx)$/.test(e.name)) {
134
+ for (const entry of entries) {
135
+ const full = join(dir, entry.name);
136
+ if (entry.isDirectory()) {
137
+ if (entry.name !== "node_modules") scanDirLucideIcons(full, set);
138
+ } else if (/\.(js|ts|tsx)$/.test(entry.name)) {
157
139
  const content = readFileSync(full, "utf-8");
158
- for (const m of content.matchAll(LUCIDE_IMPORT_RE)) {
159
- for (const s of m[1].split(",")) {
160
- const name = s
140
+ for (const match of content.matchAll(LUCIDE_IMPORT_RE)) {
141
+ for (const specifier of match[1].split(",")) {
142
+ const name = specifier
161
143
  .trim()
162
144
  .split(/\s+as\s+/)[0]
163
145
  .trim();
@@ -173,36 +155,32 @@ function scanDirLucideIcons(dir: string, set: Set<string>): void {
173
155
  }
174
156
  }
175
157
 
176
- /** Collect every lucide icon name imported across flame sources and deps. */
177
158
  function collectAllLucideIcons(): string[] {
178
159
  const icons = new Set<string>();
179
- // Scan flame's own components and pages
180
160
  scanDirLucideIcons(join(FRAMEWORK_ROOT, ".docu/components"), icons);
181
161
  scanDirLucideIcons(join(FRAMEWORK_ROOT, ".docu/pages"), icons);
182
- // Scan dependency dist directories. In development (monorepo) they live under
183
- // packages/; in production they are under node_modules/@docubook/.
184
- const depDirs = [
162
+ const dependencyDirs = [
185
163
  join(FRAMEWORK_ROOT, "..", "mdx-content", "dist"),
186
164
  join(FRAMEWORK_ROOT, "..", "ui-react", "dist"),
187
165
  join(FRAMEWORK_ROOT, "..", "core", "dist"),
188
166
  join(FRAMEWORK_ROOT, "..", "themes-colors", "dist"),
189
167
  ];
190
- for (const d of depDirs) scanDirLucideIcons(resolve(d), icons);
168
+ for (const dir of dependencyDirs) scanDirLucideIcons(resolve(dir), icons);
191
169
  return [...icons];
192
170
  }
193
171
 
194
- /** Build the client JS bundle and Tailwind CSS. */
195
172
  export async function buildClientBundle(
196
173
  /** slug → compiled MDX ESM module source (program format) for static hydration. */
197
174
  mdxSources: Record<string, string> = {}
198
- ): Promise<{ js: string; css: string }> {
175
+ ): Promise<AssetManifest> {
199
176
  await mkdir(ASSETS_DIR, { recursive: true });
200
- const twKey = tailwindCacheKey();
201
- await cleanOldBundles(new Set([`client-${twKey}.css`]));
202
-
177
+ await cleanOldBundles();
203
178
  const nodeEnv = process.env.NODE_ENV || "development";
179
+ const mdxEntries = createMdxModuleEntries(mdxSources);
180
+ const mdxEntriesById = new Map(mdxEntries.map((entry) => [entry.id, entry]));
181
+ const docsEntryPath = join(LIB_DIR, "client.ts");
182
+ const homeEntryPath = join(LIB_DIR, "home-client.ts");
204
183
 
205
- const entryPath = join(LIB_DIR, "client.ts");
206
184
  const bundle = await viteBuild({
207
185
  configFile: false,
208
186
  publicDir: false,
@@ -261,27 +239,20 @@ export async function buildClientBundle(
261
239
  name: "mdx-hydrate",
262
240
  resolveId(id) {
263
241
  if (/mdx-manifest$/.test(id)) return "\0mdx-manifest";
264
- if (id.startsWith("mdx-module:")) return `\0${id}`;
242
+ if (/^docubook-mdx-page-[a-f0-9]{16}$/.test(id)) return `\0${id}`;
265
243
  return null;
266
244
  },
267
245
  load(id) {
268
246
  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}`);
247
+ const map = mdxEntries
248
+ .map(({ slug, id }) => `${JSON.stringify(slug)}: () => import(${JSON.stringify(id)})`)
249
+ .join(", ");
250
+ return `export const mdxModules = { ${map} };\n`;
284
251
  }
252
+ if (!id.startsWith("\0docubook-mdx-page-")) return null;
253
+ const entry = mdxEntriesById.get(id.slice(1));
254
+ const contents = entry == null ? undefined : mdxSources[entry.slug];
255
+ if (contents == null) throw new Error(`unknown mdx module: ${id}`);
285
256
  return contents;
286
257
  },
287
258
  },
@@ -293,11 +264,11 @@ export async function buildClientBundle(
293
264
  minify: nodeEnv === "production",
294
265
  target: "es2020",
295
266
  rollupOptions: {
296
- input: entryPath,
267
+ input: { client: docsEntryPath, "home-client": homeEntryPath },
297
268
  output: {
298
269
  format: "es",
299
- entryFileNames: "client-[hash].js",
300
- chunkFileNames: "chunks/[name]-[hash].js",
270
+ entryFileNames: "[name]-[hash].js",
271
+ chunkFileNames: viteChunkFileName,
301
272
  assetFileNames: "assets/[name]-[hash][extname]",
302
273
  },
303
274
  },
@@ -305,29 +276,30 @@ export async function buildClientBundle(
305
276
  });
306
277
 
307
278
  const outputs = Array.isArray(bundle) ? bundle : [bundle];
308
- let jsFile: string | undefined;
279
+ let docsJs: string | undefined;
280
+ let homeJs: string | undefined;
309
281
  for (const item of outputs) {
310
282
  if (!("output" in item)) continue;
311
283
  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
- }
284
+ if (output.type !== "chunk" || !output.isEntry) continue;
285
+ if (output.name === "client") docsJs = output.fileName;
286
+ if (output.name === "home-client") homeJs = output.fileName;
321
287
  }
322
- if (jsFile) break;
323
288
  }
324
- if (!jsFile) {
325
- throw new Error("Client bundle produced no output files");
289
+ if (!docsJs || !homeJs) {
290
+ throw new Error("Client bundle produced incomplete entry-point outputs");
326
291
  }
327
292
 
328
- const { file: cssFile } = await buildTailwindCss(twKey);
329
-
330
- await writeFile(join(ASSETS_DIR, "manifest.json"), JSON.stringify({ js: jsFile, css: cssFile }));
293
+ const [{ file: docsCss }, { file: siteCss }] = await Promise.all([
294
+ buildTailwindCss("docs", "globals.css"),
295
+ buildTailwindCss("site", "site.css"),
296
+ ]);
331
297
 
332
- return { js: jsFile, css: cssFile };
298
+ const manifest: AssetManifest = {
299
+ docs: { js: docsJs, css: docsCss },
300
+ home: { js: homeJs, css: siteCss },
301
+ notFound: { css: siteCss },
302
+ };
303
+ await writeFile(join(ASSETS_DIR, "manifest.json"), JSON.stringify(manifest));
304
+ return manifest;
333
305
  }
@@ -1,15 +1,25 @@
1
- import { join } from "node:path";
2
- import { mkdir, unlink, rename } from "node:fs/promises";
1
+ import { createHash } from "node:crypto";
2
+ import { basename, join } from "node:path";
3
+ import { mkdir, rename, unlink } from "node:fs/promises";
3
4
  import { existsSync } from "node:fs";
4
5
  import { resolveTheme, generateThemeCss, presetRegistry } from "@docubook/themes-colors";
5
6
  import { ASSETS_DIR, cleanOldBundles, LIB_DIR, STYLES_DIR, loadDocuConfig } from "./paths";
6
- import { atomicWriteFile, computeTailwindCacheKey, readGlobalsCss } from "./cache-key";
7
+ import { atomicWriteFile, computeTailwindCacheKey, readStyleCss } from "./cache-key";
7
8
  import { resolveRoutes } from "./fs-scanner";
8
- import type { DocuRoute } from "./types";
9
+ import type { AssetManifest, DocuRoute } from "./types";
9
10
  import type { ThemeConfig } from "@docubook/themes-colors";
10
11
 
11
12
  const themeRegistry = presetRegistry;
12
13
 
14
+ export function createMdxModuleEntries(mdxSources: Record<string, string>) {
15
+ return Object.keys(mdxSources)
16
+ .sort()
17
+ .map((slug) => ({
18
+ slug,
19
+ id: `docubook-mdx-page-${createHash("sha256").update(slug).digest("hex").slice(0, 16)}`,
20
+ }));
21
+ }
22
+
13
23
  /**
14
24
  * Read the effective theme config with this priority:
15
25
  * 1. FLAME_THEME env var (CLI --theme flag)
@@ -23,9 +33,7 @@ export function getThemeConfig(): ThemeConfig | undefined {
23
33
  return config.themes?.colors;
24
34
  }
25
35
 
26
- /**
27
- * Append theme CSS to compiled Tailwind output based on theme config.
28
- */
36
+ /** Append theme CSS to compiled Tailwind output based on theme config. */
29
37
  export function buildThemeCss(baseCss: string, themeConfig: unknown): string {
30
38
  try {
31
39
  const resolved = resolveTheme(themeConfig as ThemeConfig | undefined | null, themeRegistry);
@@ -38,10 +46,7 @@ export function buildThemeCss(baseCss: string, themeConfig: unknown): string {
38
46
  }
39
47
  }
40
48
 
41
- /**
42
- * Compute inline theme CSS for FOUC prevention.
43
- * Returns undefined if no theme is configured or on error.
44
- */
49
+ /** Compute inline theme CSS for FOUC prevention. */
45
50
  export function computeInlineThemeCss(): string | undefined {
46
51
  try {
47
52
  const themeColors = getThemeConfig();
@@ -57,41 +62,35 @@ export function computeInlineThemeCss(): string | undefined {
57
62
  return undefined;
58
63
  }
59
64
 
60
- /** Compute Tailwind cache key from globals.css + theme + config + toolchain. */
61
- function twCacheKey(): string {
62
- const globals = readGlobalsCss();
63
- let themeSuffix = "";
65
+ function themeCacheSuffix(): string {
64
66
  try {
65
67
  const themeColors = getThemeConfig();
66
- if (themeColors) themeSuffix = JSON.stringify(themeColors);
68
+ return themeColors ? JSON.stringify(themeColors) : "";
67
69
  } catch {
68
- // theme config unavailable — proceed without
70
+ return "";
69
71
  }
70
- return computeTailwindCacheKey(globals, themeSuffix);
71
72
  }
72
73
 
73
- /** Run Tailwind CLI, caching by content hash. */
74
- async function buildTailwindCss(key: string): Promise<{ file: string; content: string }> {
75
- const cachedFile = `client-${key}.css`;
74
+ function tailwindCacheKey(styleFile: string): string {
75
+ return computeTailwindCacheKey(readStyleCss(styleFile), `${styleFile}\0${themeCacheSuffix()}`);
76
+ }
77
+
78
+ /** Run Tailwind CLI, caching each route stylesheet by content hash. */
79
+ async function buildTailwindCss(
80
+ name: string,
81
+ styleFile: string
82
+ ): Promise<{ file: string; content: string }> {
83
+ const key = tailwindCacheKey(styleFile);
84
+ const cachedFile = `${name}-${key}.css`;
76
85
  const cachedPath = join(ASSETS_DIR, cachedFile);
77
86
 
78
87
  if (existsSync(cachedPath)) {
79
- const content = await Bun.file(cachedPath).text();
80
- return { file: cachedFile, content };
88
+ return { file: cachedFile, content: await Bun.file(cachedPath).text() };
81
89
  }
82
90
 
83
- const tmpCss = join(ASSETS_DIR, `_tmp-${key}.css`);
91
+ const tmpCss = join(ASSETS_DIR, `_tmp-${name}-${key}.css`);
84
92
  const proc = Bun.spawn(
85
- [
86
- "bun",
87
- "x",
88
- "@tailwindcss/cli",
89
- "-i",
90
- join(STYLES_DIR, "globals.css"),
91
- "-o",
92
- tmpCss,
93
- "--minify",
94
- ],
93
+ ["bun", "x", "@tailwindcss/cli", "-i", join(STYLES_DIR, styleFile), "-o", tmpCss, "--minify"],
95
94
  { stdout: "ignore", stderr: "pipe" }
96
95
  );
97
96
  await proc.exited;
@@ -112,31 +111,33 @@ async function buildTailwindCss(key: string): Promise<{ file: string; content: s
112
111
  );
113
112
  }
114
113
 
115
- const cssFile = `client-${key}.css`;
116
- const outPath = join(ASSETS_DIR, cssFile);
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)) {
114
+ if (!existsSync(cachedPath)) {
120
115
  const { writeFile } = await import("node:fs/promises");
121
- await atomicWriteFile(writeFile, rename, unlink, outPath, cssContent);
116
+ await atomicWriteFile(writeFile, rename, unlink, cachedPath, cssContent);
122
117
  }
123
118
 
124
- return { file: cssFile, content: cssContent };
119
+ return { file: cachedFile, content: cssContent };
125
120
  }
126
121
 
127
122
  export async function buildClientBundle(
128
123
  /** slug → compiled MDX ESM module source (program format) for static hydration. */
129
124
  mdxSources: Record<string, string> = {}
130
- ): Promise<{ js: string; css: string }> {
125
+ ): Promise<AssetManifest> {
131
126
  await mkdir(ASSETS_DIR, { recursive: true });
132
- const twKey = twCacheKey();
133
- await cleanOldBundles(new Set([`client-${twKey}.css`]));
127
+ await cleanOldBundles();
134
128
 
135
129
  const nodeEnv = process.env.NODE_ENV || "development";
130
+ const mdxEntries = createMdxModuleEntries(mdxSources);
131
+ const mdxEntriesById = new Map(mdxEntries.map((entry) => [entry.id, entry]));
136
132
  const result = await Bun.build({
137
- entrypoints: [join(LIB_DIR, "client.ts")],
133
+ entrypoints: [join(LIB_DIR, "client.ts"), join(LIB_DIR, "home-client.ts")],
138
134
  outdir: ASSETS_DIR,
139
- naming: "client-[hash].[ext]",
135
+ splitting: true,
136
+ naming: {
137
+ entry: "[name]-[hash].[ext]",
138
+ chunk: "chunks/[name]-[hash].[ext]",
139
+ asset: "assets/[name]-[hash].[ext]",
140
+ },
140
141
  target: "browser",
141
142
  minify: nodeEnv === "production",
142
143
  define: { "process.env.NODE_ENV": JSON.stringify(nodeEnv) },
@@ -144,8 +145,6 @@ export async function buildClientBundle(
144
145
  {
145
146
  name: "docu-config",
146
147
  setup(build) {
147
- // Components import as "../node/client-routes" (no .ts extension),
148
- // so filter matches the path tail without requiring the extension.
149
148
  build.onResolve({ filter: /client-routes$/ }, (args) => ({
150
149
  path: args.path,
151
150
  namespace: "client-routes",
@@ -169,21 +168,18 @@ export async function buildClientBundle(
169
168
  },
170
169
  },
171
170
  {
172
- // Serves per-page compiled MDX (program format) as real modules so
173
- // the client hydrates the content island without `new Function`.
174
- // client.ts imports `{ mdxModules } from "./mdx-manifest"`.
175
171
  name: "mdx-hydrate",
176
172
  setup(build) {
177
- build.onResolve({ filter: /^mdx-module:/ }, (args) => ({
173
+ build.onResolve({ filter: /^docubook-mdx-page-[a-f0-9]{16}$/ }, (args) => ({
178
174
  path: args.path,
179
175
  namespace: "mdx-module",
180
176
  }));
181
177
  build.onLoad({ filter: /.*/, namespace: "mdx-module" }, (args) => {
182
- const slug = args.path.slice("mdx-module:".length);
183
- const contents = mdxSources[slug];
178
+ const entry = mdxEntriesById.get(args.path);
179
+ const contents = entry == null ? undefined : mdxSources[entry.slug];
184
180
  if (contents == null) {
185
181
  return {
186
- errors: [{ text: `unknown mdx module: ${slug}` }],
182
+ errors: [{ text: `unknown mdx module: ${args.path}` }],
187
183
  contents: "",
188
184
  loader: "js",
189
185
  };
@@ -195,20 +191,11 @@ export async function buildClientBundle(
195
191
  namespace: "mdx-manifest",
196
192
  }));
197
193
  build.onLoad({ filter: /.*/, namespace: "mdx-manifest" }, () => {
198
- // Sort keys: the prePass fills mdxSources via Promise.all, so
199
- // insertion order = resolution order (non-deterministic across
200
- // processes). Stable key order keeps the bundle hash stable so
201
- // the build cache (`assetsChanged`) actually hits.
202
- const slugs = Object.keys(mdxSources).sort();
203
- const imports = slugs
204
- .map((slug, i) => {
205
- const key = slug.replace(/["\\]/g, "");
206
- return `import * as _mdx${i} from "mdx-module:${key}";`;
207
- })
208
- .join("\n");
209
- const map = slugs.map((slug, i) => `${JSON.stringify(slug)}: _mdx${i}`).join(", ");
194
+ const map = mdxEntries
195
+ .map(({ slug, id }) => `${JSON.stringify(slug)}: () => import(${JSON.stringify(id)})`)
196
+ .join(", ");
210
197
  return {
211
- contents: `${imports}\nexport const mdxModules = { ${map} };\n`,
198
+ contents: `export const mdxModules = { ${map} };\n`,
212
199
  loader: "js",
213
200
  };
214
201
  });
@@ -222,18 +209,25 @@ export async function buildClientBundle(
222
209
  throw new Error("Client bundle failed");
223
210
  }
224
211
 
225
- if (!result.outputs[0]) {
226
- throw new Error("Client bundle produced no output files");
212
+ const entryFiles = result.outputs
213
+ .filter((output) => output.kind === "entry-point")
214
+ .map((output) => basename(output.path));
215
+ const docsJs = entryFiles.find((file) => file.startsWith("client-"));
216
+ const homeJs = entryFiles.find((file) => file.startsWith("home-client-"));
217
+ if (!docsJs || !homeJs) {
218
+ throw new Error("Client bundle produced incomplete entry-point outputs");
227
219
  }
228
- const entry = result.outputs.find((o) => o.kind === "entry-point");
229
- if (!entry) {
230
- throw new Error("Client bundle produced no entry-point output");
231
- }
232
- const jsFile = entry.path.split("/").pop()!;
233
-
234
- const { file: cssFile } = await buildTailwindCss(twKey);
235
-
236
- await Bun.write(join(ASSETS_DIR, "manifest.json"), JSON.stringify({ js: jsFile, css: cssFile }));
237
220
 
238
- return { js: jsFile, css: cssFile };
221
+ const [{ file: docsCss }, { file: siteCss }] = await Promise.all([
222
+ buildTailwindCss("docs", "globals.css"),
223
+ buildTailwindCss("site", "site.css"),
224
+ ]);
225
+
226
+ const manifest: AssetManifest = {
227
+ docs: { js: docsJs, css: docsCss },
228
+ home: { js: homeJs, css: siteCss },
229
+ notFound: { css: siteCss },
230
+ };
231
+ await Bun.write(join(ASSETS_DIR, "manifest.json"), JSON.stringify(manifest));
232
+ return manifest;
239
233
  }
@@ -1,4 +1,4 @@
1
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.
2
+ // Maps doc slug → lazy compiled MDX module. Generated at bundle time; never on disk.
3
3
  import type { ComponentType } from "react";
4
- export declare const mdxModules: Record<string, { default: ComponentType }>;
4
+ export declare const mdxModules: Record<string, () => Promise<{ default: ComponentType }>>;
@@ -64,21 +64,14 @@ export async function cleanOldBundles(preserve?: Set<string>) {
64
64
  const files = await readdir(ASSETS_DIR);
65
65
  for (const file of files) {
66
66
  if (preserve?.has(file)) continue;
67
- if (file.startsWith("client.") || file.startsWith("client-")) {
67
+ if (/^(client|home-client|docs|site)[.-]/.test(file)) {
68
68
  await unlink(join(ASSETS_DIR, file));
69
69
  }
70
70
  }
71
- } catch (err) {
72
- if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
73
- console.error("Failed to clean old bundles:", (err as Error).message);
74
- }
75
- }
76
- try {
77
71
  await rm(join(ASSETS_DIR, "chunks"), { recursive: true, force: true });
78
72
  } catch (err) {
79
- // chunks dir may not exist, or permission error — log to avoid silent failure
80
73
  if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
81
- console.warn("[flame] Failed to clean chunks dir:", (err as Error).message);
74
+ console.error("Failed to clean old bundles:", (err as Error).message);
82
75
  }
83
76
  }
84
77
  }