@docubook/flame 2.0.0-beta.5 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.docu/components/Theme.tsx +42 -34
- package/.docu/components/home/BackgroundBlobs.tsx +31 -0
- package/.docu/components/home/index.tsx +1 -0
- package/.docu/lib/build.deno.js +1 -1
- package/.docu/lib/{build.impl-BBlR3si3.js → build.impl-CAQr352X.js} +60 -29
- package/.docu/lib/build.impl-CAQr352X.js.map +1 -0
- package/.docu/lib/build.impl-NLhjTqTa.js +2 -0
- package/.docu/lib/build.node.js +1 -1
- 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/{deploy.shared-Dxv3lsOb.js → deploy.shared-CvgObu6_.js} +45 -17
- package/.docu/lib/deploy.shared-CvgObu6_.js.map +1 -0
- package/.docu/lib/{html.shared-DBMwv_Z6.js → html.shared-CsEFmy_Y.js} +181 -158
- package/.docu/lib/html.shared-CsEFmy_Y.js.map +1 -0
- package/.docu/lib/{logger-DYmvFF1d.js → logger-DwxYdOL7.js} +4 -3
- package/.docu/lib/logger-DwxYdOL7.js.map +1 -0
- package/.docu/lib/{paths-Bl2cdp9E.js → paths-Cbz-dj4s.js} +5 -9
- package/.docu/lib/{paths-Bl2cdp9E.js.map → paths-Cbz-dj4s.js.map} +1 -1
- package/.docu/lib/preview.deno.js +1 -1
- package/.docu/lib/{preview.impl-anVcZv2Z.js → preview.impl-BwrVLNIQ.js} +3 -3
- package/.docu/lib/{preview.impl-anVcZv2Z.js.map → preview.impl-BwrVLNIQ.js.map} +1 -1
- package/.docu/lib/preview.node.js +1 -1
- package/.docu/lib/server.deno.js +1 -1
- package/.docu/lib/{server.impl-Cvoa6Diq.js → server.impl-Cs6DEy4V.js} +43 -52
- package/.docu/lib/server.impl-Cs6DEy4V.js.map +1 -0
- package/.docu/lib/server.node.js +1 -1
- package/.docu/node/build.impl.ts +71 -34
- package/.docu/node/build.ts +68 -34
- package/.docu/node/cache-key.ts +8 -4
- package/.docu/node/client.ts +36 -27
- package/.docu/node/deploy.shared.ts +44 -18
- package/.docu/node/deploy.ts +5 -20
- package/.docu/node/home-client.ts +6 -0
- package/.docu/node/html.shared.ts +5 -4
- package/.docu/node/html.ts +4 -3
- package/.docu/node/hydrate.node.ts +87 -115
- package/.docu/node/hydrate.ts +74 -80
- package/.docu/node/mdx-manifest.d.ts +2 -2
- package/.docu/node/paths.ts +2 -9
- package/.docu/node/plugin-builder.ts +25 -13
- package/.docu/node/plugin.ts +33 -9
- package/.docu/node/server-routes.ts +59 -50
- package/.docu/node/server.impl.ts +4 -2
- package/.docu/node/server.ts +12 -16
- package/.docu/node/types.ts +11 -0
- package/.docu/pages/404.tsx +12 -9
- package/.docu/pages/index.tsx +8 -29
- package/.docu/styles/base.css +128 -0
- package/.docu/styles/globals.css +5 -137
- package/.docu/styles/site.css +7 -0
- package/package.json +6 -5
- package/.docu/lib/build.impl-BBlR3si3.js.map +0 -1
- package/.docu/lib/build.impl-DpoDUmei.js +0 -2
- package/.docu/lib/deploy.shared-Dxv3lsOb.js.map +0 -1
- package/.docu/lib/html.shared-DBMwv_Z6.js.map +0 -1
- package/.docu/lib/logger-DYmvFF1d.js.map +0 -1
- 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 {
|
|
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,
|
|
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 = (
|
|
36
|
-
if (
|
|
27
|
+
const pushIf = (value: string | undefined) => {
|
|
28
|
+
if (value) icons.push(value);
|
|
37
29
|
};
|
|
38
|
-
config.home?.hero?.actions?.forEach((
|
|
39
|
-
config.home?.features?.forEach((
|
|
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
|
|
42
|
-
pushIf(
|
|
43
|
-
if (
|
|
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((
|
|
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
|
-
|
|
63
|
-
function tailwindCacheKey(): string {
|
|
64
|
-
const globalsContent = readGlobalsCss();
|
|
65
|
-
let themeSuffix = "";
|
|
58
|
+
function themeCacheSuffix(): string {
|
|
66
59
|
try {
|
|
67
60
|
const themeColors = getThemeConfig();
|
|
68
|
-
|
|
69
|
-
themeSuffix = JSON.stringify(themeColors);
|
|
70
|
-
}
|
|
61
|
+
return themeColors ? JSON.stringify(themeColors) : "";
|
|
71
62
|
} catch {
|
|
72
|
-
|
|
63
|
+
return "";
|
|
73
64
|
}
|
|
74
|
-
return computeTailwindCacheKey(globalsContent, themeSuffix);
|
|
75
65
|
}
|
|
76
66
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
|
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, ...
|
|
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
|
|
103
|
+
`[flame] Failed to resolve theme config: ${err instanceof Error ? err.message : String(err)}`
|
|
114
104
|
);
|
|
115
105
|
}
|
|
116
106
|
|
|
117
|
-
|
|
118
|
-
|
|
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:
|
|
111
|
+
return { file: cachedFile, content: cssContent };
|
|
128
112
|
}
|
|
129
113
|
|
|
130
114
|
const NODE_BUILTINS_RE = new RegExp(
|
|
131
|
-
`^(node:.*|${builtinModules.map((
|
|
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
|
|
153
|
-
const full = join(dir,
|
|
154
|
-
if (
|
|
155
|
-
if (
|
|
156
|
-
} else if (/\.(js|ts|tsx)$/.test(
|
|
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
|
|
159
|
-
for (const
|
|
160
|
-
const name =
|
|
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
|
-
|
|
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
|
|
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<
|
|
175
|
+
): Promise<AssetManifest> {
|
|
199
176
|
await mkdir(ASSETS_DIR, { recursive: true });
|
|
200
|
-
|
|
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 (
|
|
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
|
|
270
|
-
|
|
271
|
-
.
|
|
272
|
-
|
|
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:
|
|
267
|
+
input: { client: docsEntryPath, "home-client": homeEntryPath },
|
|
297
268
|
output: {
|
|
298
269
|
format: "es",
|
|
299
|
-
entryFileNames: "
|
|
300
|
-
chunkFileNames:
|
|
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
|
|
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
|
-
|
|
314
|
-
|
|
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 (!
|
|
325
|
-
throw new Error("Client bundle produced
|
|
289
|
+
if (!docsJs || !homeJs) {
|
|
290
|
+
throw new Error("Client bundle produced incomplete entry-point outputs");
|
|
326
291
|
}
|
|
327
292
|
|
|
328
|
-
const { file:
|
|
329
|
-
|
|
330
|
-
|
|
293
|
+
const [{ file: docsCss }, { file: siteCss }] = await Promise.all([
|
|
294
|
+
buildTailwindCss("docs", "globals.css"),
|
|
295
|
+
buildTailwindCss("site", "site.css"),
|
|
296
|
+
]);
|
|
331
297
|
|
|
332
|
-
|
|
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
|
}
|
package/.docu/node/hydrate.ts
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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,
|
|
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
|
-
|
|
61
|
-
function twCacheKey(): string {
|
|
62
|
-
const globals = readGlobalsCss();
|
|
63
|
-
let themeSuffix = "";
|
|
65
|
+
function themeCacheSuffix(): string {
|
|
64
66
|
try {
|
|
65
67
|
const themeColors = getThemeConfig();
|
|
66
|
-
|
|
68
|
+
return themeColors ? JSON.stringify(themeColors) : "";
|
|
67
69
|
} catch {
|
|
68
|
-
|
|
70
|
+
return "";
|
|
69
71
|
}
|
|
70
|
-
return computeTailwindCacheKey(globals, themeSuffix);
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
116
|
+
await atomicWriteFile(writeFile, rename, unlink, cachedPath, cssContent);
|
|
122
117
|
}
|
|
123
118
|
|
|
124
|
-
return { file:
|
|
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<
|
|
125
|
+
): Promise<AssetManifest> {
|
|
131
126
|
await mkdir(ASSETS_DIR, { recursive: true });
|
|
132
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
|
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: ${
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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:
|
|
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
|
-
|
|
226
|
-
|
|
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
|
-
|
|
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 }>>;
|
package/.docu/node/paths.ts
CHANGED
|
@@ -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 (
|
|
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.
|
|
74
|
+
console.error("Failed to clean old bundles:", (err as Error).message);
|
|
82
75
|
}
|
|
83
76
|
}
|
|
84
77
|
}
|