@docubook/flame 1.5.3 → 1.6.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/lib/build.deno.js +4 -4
- package/.docu/lib/build.impl-N2MBT3CM.js +12 -0
- package/.docu/lib/build.node.js +4 -4
- package/.docu/lib/{chunk-J5NMYSBJ.js → chunk-4IQXHHPF.js} +5 -2
- package/.docu/lib/{chunk-YI76L7LB.js → chunk-IJFMQFWW.js} +69 -25
- package/.docu/lib/{chunk-YENO4SVR.js → chunk-JTH4JBBS.js} +38 -5
- package/.docu/lib/chunk-TI6M4643.js +204 -0
- package/.docu/lib/{chunk-EIKBL7SD.js → chunk-UUS45SUC.js} +2 -2
- package/.docu/lib/{chunk-WCMRAAWH.js → chunk-Y2TAEAUQ.js} +2 -2
- package/.docu/lib/{chunk-VX4HLVHE.js → chunk-ZA5QB5UK.js} +4 -4
- package/.docu/lib/clean.js +2 -2
- package/.docu/lib/deploy.deno.js +2 -2
- package/.docu/lib/deploy.node.js +2 -2
- package/.docu/lib/preview.deno.js +3 -3
- package/.docu/lib/preview.node.js +3 -3
- package/.docu/lib/server.deno.js +4 -4
- package/.docu/lib/server.node.js +4 -4
- package/.docu/node/build.impl.ts +15 -3
- package/.docu/node/build.ts +6 -0
- package/.docu/node/client.ts +38 -13
- package/.docu/node/deploy.shared.ts +147 -12
- package/.docu/node/deploy.ts +141 -30
- package/.docu/node/html.shared.ts +19 -0
- package/.docu/node/html.ts +17 -23
- package/.docu/node/hydrate.node.ts +74 -39
- package/.docu/node/hydrate.ts +74 -48
- package/.docu/node/paths.ts +7 -5
- package/.docu/node/seo.ts +42 -0
- package/.docu/node/server-routes.ts +2 -1
- package/.docu/node/types.ts +2 -0
- package/bin/cli.js +19 -6
- package/docu.schema.json +5 -1
- package/package.json +4 -4
- package/template/docs/assets/images/og.png +0 -0
- package/template/docu.json +2 -1
- package/.docu/lib/build.impl-O3Z5XIGM.js +0 -12
- package/.docu/lib/chunk-7VXEOH7X.js +0 -79
package/.docu/node/html.ts
CHANGED
|
@@ -1,26 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
description: string;
|
|
4
|
-
body: string;
|
|
5
|
-
favicon: string;
|
|
6
|
-
css: string;
|
|
7
|
-
js: string;
|
|
8
|
-
nonce?: string;
|
|
9
|
-
/**
|
|
10
|
-
* Content-Security-Policy value (from `cspHeader()` in security.ts).
|
|
11
|
-
* When provided, injects `<meta http-equiv="Content-Security-Policy">` in `<head>`.
|
|
12
|
-
* Essential for static deployment where HTTP headers cannot be set.
|
|
13
|
-
*/
|
|
14
|
-
csp?: string;
|
|
15
|
-
extraScripts?: string;
|
|
16
|
-
themeCss?: string;
|
|
17
|
-
/** Depth from document root (0=root, 1=subdir, 2=sub/subdir). Used for relative asset paths. */
|
|
18
|
-
depth?: number;
|
|
19
|
-
/** HTML strings to inject before `</head>` (from plugin `injectHead` hooks). */
|
|
20
|
-
headExtra?: string[];
|
|
21
|
-
/** HTML strings to inject before `</body>`, after the main script (from plugin `injectBody` hooks). */
|
|
22
|
-
bodyExtra?: string[];
|
|
23
|
-
}
|
|
1
|
+
import type { HtmlShellOptions } from "./html.shared";
|
|
2
|
+
export type { HtmlShellOptions };
|
|
24
3
|
|
|
25
4
|
export function htmlShell(opts: HtmlShellOptions): string {
|
|
26
5
|
const {
|
|
@@ -45,6 +24,18 @@ export function htmlShell(opts: HtmlShellOptions): string {
|
|
|
45
24
|
const depthPrefix = depth === 0 ? "" : "../".repeat(depth);
|
|
46
25
|
const assetPrefix = depthPrefix + "assets/";
|
|
47
26
|
const resolvePath = (path: string) => (path.startsWith("/") ? depthPrefix + path.slice(1) : path);
|
|
27
|
+
|
|
28
|
+
// Build SEO meta tags (OG, Twitter, canonical)
|
|
29
|
+
let seoTags = "";
|
|
30
|
+
if (opts.seo) {
|
|
31
|
+
const s = opts.seo;
|
|
32
|
+
const e = Bun.escapeHTML;
|
|
33
|
+
seoTags = `\n <meta property="og:title" content="${e(title)}" />\n <meta property="og:description" content="${e(description)}" />\n <meta property="og:url" content="${e(s.url)}" />\n <meta property="og:type" content="website" />\n <meta property="og:site_name" content="${e(s.siteName)}" />\n <meta name="twitter:card" content="summary_large_image" />\n <link rel="canonical" href="${e(s.url)}" />`;
|
|
34
|
+
if (s.image) {
|
|
35
|
+
seoTags += `\n <meta property="og:image" content="${e(s.image)}" />`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
48
39
|
return `<!DOCTYPE html>
|
|
49
40
|
<html lang="en">
|
|
50
41
|
<head>
|
|
@@ -53,12 +44,15 @@ export function htmlShell(opts: HtmlShellOptions): string {
|
|
|
53
44
|
<title>${Bun.escapeHTML(title)}</title>
|
|
54
45
|
<meta name="description" content="${Bun.escapeHTML(description)}">
|
|
55
46
|
${favicon ? `<link rel="icon" type="image/x-icon" href="${Bun.escapeHTML(resolvePath(favicon))}">` : ""}${themeStyle}
|
|
47
|
+
<link rel="preload" href="${Bun.escapeHTML(assetPrefix + css)}" as="style">
|
|
56
48
|
<link rel="stylesheet" href="${Bun.escapeHTML(assetPrefix + css)}">
|
|
57
49
|
${csp ? `<meta http-equiv="Content-Security-Policy" content="${Bun.escapeHTML(csp)}">` : ""}
|
|
50
|
+
${seoTags}
|
|
58
51
|
<script${nonceAttr}>try{if(localStorage.getItem("theme")==="dark")document.documentElement.classList.add("dark")}catch(e){}</script>${headInjection}
|
|
59
52
|
</head>
|
|
60
53
|
<body>
|
|
61
54
|
<div id="root">${body}</div>
|
|
55
|
+
<link rel="modulepreload" href="${Bun.escapeHTML(assetPrefix + js)}">
|
|
62
56
|
<script type="module"${nonceAttr} src="${Bun.escapeHTML(assetPrefix + js)}"></script>${extraScripts ? `\n ${extraScripts}` : ""}${bodyInjection}
|
|
63
57
|
</body>
|
|
64
58
|
</html>`;
|
|
@@ -58,10 +58,42 @@ function resolveTailwindBin(): string {
|
|
|
58
58
|
return join(dirname(pkgPath), binRel);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
/**
|
|
62
|
-
|
|
61
|
+
/** Compute a cache key from globals.css + theme config content. */
|
|
62
|
+
function tailwindCacheKey(): string {
|
|
63
|
+
const globalsPath = join(STYLES_DIR, "globals.css");
|
|
64
|
+
const globalsContent = existsSync(globalsPath) ? readFileSync(globalsPath, "utf-8") : "";
|
|
65
|
+
let themeSuffix = "";
|
|
66
|
+
try {
|
|
67
|
+
const themeColors = getThemeConfig();
|
|
68
|
+
if (themeColors) {
|
|
69
|
+
themeSuffix = JSON.stringify(themeColors);
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
// theme config unavailable — proceed without it
|
|
73
|
+
}
|
|
74
|
+
return createHash("md5")
|
|
75
|
+
.update(globalsContent + themeSuffix)
|
|
76
|
+
.digest("hex")
|
|
77
|
+
.slice(0, 16);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Build Tailwind CSS with content-based caching.
|
|
82
|
+
* If a CSS file for the current input hash already exists, skip the subprocess.
|
|
83
|
+
* Returns the filename (e.g. "client-abc123.css") and CSS content.
|
|
84
|
+
*/
|
|
85
|
+
async function buildTailwindCss(key: string): Promise<{ file: string; content: string }> {
|
|
86
|
+
const cachedFile = `client-${key}.css`;
|
|
87
|
+
const cachedPath = join(ASSETS_DIR, cachedFile);
|
|
88
|
+
|
|
89
|
+
if (existsSync(cachedPath)) {
|
|
90
|
+
const content = readFileSync(cachedPath, "utf-8");
|
|
91
|
+
return { file: cachedFile, content };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const tmpCss = join(ASSETS_DIR, `_tmp-${key}.css`);
|
|
63
95
|
const bin = resolveTailwindBin();
|
|
64
|
-
const twArgs = ["-i", join(STYLES_DIR, "globals.css"), "-o",
|
|
96
|
+
const twArgs = ["-i", join(STYLES_DIR, "globals.css"), "-o", tmpCss, "--minify"];
|
|
65
97
|
const isDeno = "Deno" in globalThis;
|
|
66
98
|
const args = isDeno ? ["run", "-A", bin, ...twArgs] : [bin, ...twArgs];
|
|
67
99
|
try {
|
|
@@ -70,6 +102,31 @@ async function runTailwind(outputCss: string): Promise<void> {
|
|
|
70
102
|
const stderr = (err as { stderr?: string }).stderr ?? String(err);
|
|
71
103
|
throw new Error(`Tailwind CSS build failed:\n${stderr}`, { cause: err });
|
|
72
104
|
}
|
|
105
|
+
|
|
106
|
+
let cssContent = await readFile(tmpCss, "utf-8");
|
|
107
|
+
await unlink(tmpCss);
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const themeColors = getThemeConfig();
|
|
111
|
+
if (themeColors) {
|
|
112
|
+
cssContent = buildThemeCss(cssContent, themeColors);
|
|
113
|
+
}
|
|
114
|
+
} catch (err) {
|
|
115
|
+
console.warn(
|
|
116
|
+
`[flame] Failed to resolve theme config, falling back to globals.css only: ${err instanceof Error ? err.message : String(err)}`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Use the same input-derived key for lookup and output — if inputs change,
|
|
121
|
+
// the key changes, cache busting works without a separate content hash.
|
|
122
|
+
const cssFile = `client-${key}.css`;
|
|
123
|
+
const outPath = join(ASSETS_DIR, cssFile);
|
|
124
|
+
|
|
125
|
+
if (!existsSync(outPath)) {
|
|
126
|
+
await writeFile(outPath, cssContent);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { file: cssFile, content: cssContent };
|
|
73
130
|
}
|
|
74
131
|
|
|
75
132
|
const NODE_BUILTINS_RE = new RegExp(
|
|
@@ -111,8 +168,10 @@ function scanDirLucideIcons(dir: string, set: Set<string>): void {
|
|
|
111
168
|
}
|
|
112
169
|
}
|
|
113
170
|
}
|
|
114
|
-
} catch {
|
|
115
|
-
|
|
171
|
+
} catch (err) {
|
|
172
|
+
console.warn(
|
|
173
|
+
`[flame] Failed to scan lucide icons: ${err instanceof Error ? err.message : String(err)}`
|
|
174
|
+
);
|
|
116
175
|
}
|
|
117
176
|
}
|
|
118
177
|
|
|
@@ -138,7 +197,8 @@ function collectAllLucideIcons(): string[] {
|
|
|
138
197
|
/** Build the client JS bundle and Tailwind CSS. */
|
|
139
198
|
export async function buildClientBundle(): Promise<{ js: string; css: string }> {
|
|
140
199
|
await mkdir(ASSETS_DIR, { recursive: true });
|
|
141
|
-
|
|
200
|
+
const twKey = tailwindCacheKey();
|
|
201
|
+
await cleanOldBundles(new Set([`client-${twKey}.css`]));
|
|
142
202
|
|
|
143
203
|
const nodeEnv = process.env.NODE_ENV || "development";
|
|
144
204
|
const esbuild = await import("esbuild");
|
|
@@ -153,10 +213,8 @@ export async function buildClientBundle(): Promise<{ js: string; css: string }>
|
|
|
153
213
|
bundle: true,
|
|
154
214
|
outdir: ASSETS_DIR,
|
|
155
215
|
entryNames: "client-[hash]",
|
|
156
|
-
chunkNames: "chunks/[name]-[hash]",
|
|
157
216
|
platform: "browser",
|
|
158
217
|
format: "esm",
|
|
159
|
-
splitting: true,
|
|
160
218
|
minify: nodeEnv === "production",
|
|
161
219
|
define: { "process.env.NODE_ENV": JSON.stringify(nodeEnv) },
|
|
162
220
|
jsx: "automatic",
|
|
@@ -185,17 +243,17 @@ export async function buildClientBundle(): Promise<{ js: string; css: string }>
|
|
|
185
243
|
if (args.namespace === "lucide-virt") {
|
|
186
244
|
return { path: getLucideRealEntry(), namespace: "file" };
|
|
187
245
|
}
|
|
188
|
-
//
|
|
189
|
-
//
|
|
246
|
+
// mdx-content Icon.tsx uses namespace import for arbitrary
|
|
247
|
+
// user-provided icon names in MDX — keep full barrel there.
|
|
190
248
|
if (args.importer) {
|
|
191
249
|
const normalized = normalizeImporterPath(args.importer);
|
|
192
|
-
if (
|
|
193
|
-
normalized.endsWith("/.docu/components/Lucide.tsx") ||
|
|
194
|
-
normalized.includes("/mdx-content/dist/")
|
|
195
|
-
) {
|
|
250
|
+
if (normalized.includes("/mdx-content/dist/")) {
|
|
196
251
|
return { path: getLucideRealEntry(), namespace: "file" };
|
|
197
252
|
}
|
|
198
253
|
}
|
|
254
|
+
// All other files get tree-shaken via the virtual module.
|
|
255
|
+
// Lucide.tsx renders only config-defined icons, which are
|
|
256
|
+
// collected by extractConfigIcons() + collectAllLucideIcons().
|
|
199
257
|
return { path: args.path, namespace: "lucide-virt" };
|
|
200
258
|
});
|
|
201
259
|
build.onLoad({ filter: /.*/, namespace: "lucide-virt" }, () => {
|
|
@@ -226,7 +284,6 @@ export async function buildClientBundle(): Promise<{ js: string; css: string }>
|
|
|
226
284
|
});
|
|
227
285
|
},
|
|
228
286
|
},
|
|
229
|
-
|
|
230
287
|
],
|
|
231
288
|
});
|
|
232
289
|
} finally {
|
|
@@ -235,10 +292,7 @@ export async function buildClientBundle(): Promise<{ js: string; css: string }>
|
|
|
235
292
|
await esbuild.stop();
|
|
236
293
|
}
|
|
237
294
|
|
|
238
|
-
//
|
|
239
|
-
// `entryPoint` is set on the user entry AND on every dynamic-import chunk
|
|
240
|
-
// (esbuild treats dynamic imports as entry points), so match the resolved
|
|
241
|
-
// source path instead of grabbing the first truthy `entryPoint`.
|
|
295
|
+
// The single entry produces one output. Match resolved source path.
|
|
242
296
|
const { outputs } = result.metafile!;
|
|
243
297
|
const jsOutput = Object.keys(outputs).find((p) => {
|
|
244
298
|
const o = outputs[p];
|
|
@@ -249,26 +303,7 @@ export async function buildClientBundle(): Promise<{ js: string; css: string }>
|
|
|
249
303
|
}
|
|
250
304
|
const jsFile = basename(jsOutput);
|
|
251
305
|
|
|
252
|
-
const
|
|
253
|
-
await runTailwind(tmpCss);
|
|
254
|
-
|
|
255
|
-
let cssContent = await readFile(tmpCss, "utf-8");
|
|
256
|
-
|
|
257
|
-
try {
|
|
258
|
-
const themeColors = getThemeConfig();
|
|
259
|
-
if (themeColors) {
|
|
260
|
-
cssContent = buildThemeCss(cssContent, themeColors);
|
|
261
|
-
}
|
|
262
|
-
} catch (err) {
|
|
263
|
-
console.warn(
|
|
264
|
-
`[flame] Failed to resolve theme config, falling back to globals.css only: ${err instanceof Error ? err.message : String(err)}`
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
const cssHash = createHash("md5").update(cssContent).digest("hex").slice(0, 8);
|
|
269
|
-
const cssFile = `client-${cssHash}.css`;
|
|
270
|
-
await writeFile(join(ASSETS_DIR, cssFile), cssContent);
|
|
271
|
-
await unlink(tmpCss);
|
|
306
|
+
const { file: cssFile } = await buildTailwindCss(twKey);
|
|
272
307
|
|
|
273
308
|
await writeFile(join(ASSETS_DIR, "manifest.json"), JSON.stringify({ js: jsFile, css: cssFile }));
|
|
274
309
|
|
package/.docu/node/hydrate.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { mkdir, unlink } from "node:fs/promises";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
3
5
|
import { resolveTheme, generateThemeCss, presetRegistry } from "@docubook/themes-colors";
|
|
4
6
|
import { ASSETS_DIR, cleanOldBundles, LIB_DIR, STYLES_DIR, loadDocuConfig } from "./paths";
|
|
5
7
|
import { resolveRoutes } from "./fs-scanner";
|
|
@@ -55,24 +57,86 @@ export function computeInlineThemeCss(): string | undefined {
|
|
|
55
57
|
return undefined;
|
|
56
58
|
}
|
|
57
59
|
|
|
60
|
+
/** Compute Tailwind cache key from globals.css + theme config. */
|
|
61
|
+
function twCacheKey(): string {
|
|
62
|
+
const globalsPath = join(STYLES_DIR, "globals.css");
|
|
63
|
+
const globals = existsSync(globalsPath) ? readFileSync(globalsPath, "utf-8") : "";
|
|
64
|
+
let themeSuffix = "";
|
|
65
|
+
try {
|
|
66
|
+
const themeColors = getThemeConfig();
|
|
67
|
+
if (themeColors) themeSuffix = JSON.stringify(themeColors);
|
|
68
|
+
} catch {
|
|
69
|
+
// theme config unavailable — proceed without
|
|
70
|
+
}
|
|
71
|
+
return createHash("md5")
|
|
72
|
+
.update(globals + themeSuffix)
|
|
73
|
+
.digest("hex")
|
|
74
|
+
.slice(0, 16);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Run Tailwind CLI, caching by content hash. */
|
|
78
|
+
async function buildTailwindCss(key: string): Promise<{ file: string; content: string }> {
|
|
79
|
+
const cachedFile = `client-${key}.css`;
|
|
80
|
+
const cachedPath = join(ASSETS_DIR, cachedFile);
|
|
81
|
+
|
|
82
|
+
if (existsSync(cachedPath)) {
|
|
83
|
+
const content = await Bun.file(cachedPath).text();
|
|
84
|
+
return { file: cachedFile, content };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const tmpCss = join(ASSETS_DIR, `_tmp-${key}.css`);
|
|
88
|
+
const proc = Bun.spawn(
|
|
89
|
+
[
|
|
90
|
+
"bun",
|
|
91
|
+
"x",
|
|
92
|
+
"@tailwindcss/cli",
|
|
93
|
+
"-i",
|
|
94
|
+
join(STYLES_DIR, "globals.css"),
|
|
95
|
+
"-o",
|
|
96
|
+
tmpCss,
|
|
97
|
+
"--minify",
|
|
98
|
+
],
|
|
99
|
+
{ stdout: "ignore", stderr: "pipe" }
|
|
100
|
+
);
|
|
101
|
+
await proc.exited;
|
|
102
|
+
if (proc.exitCode !== 0) {
|
|
103
|
+
const err = await new Response(proc.stderr).text();
|
|
104
|
+
throw new Error(`Tailwind CSS build failed:\n${err}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let cssContent = await Bun.file(tmpCss).text();
|
|
108
|
+
await unlink(tmpCss);
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const themeColors = getThemeConfig();
|
|
112
|
+
if (themeColors) cssContent = buildThemeCss(cssContent, themeColors);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
console.warn(
|
|
115
|
+
`[flame] Failed to resolve theme config: ${err instanceof Error ? err.message : String(err)}`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
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
|
+
const cssFile = `client-${key}.css`;
|
|
122
|
+
const outPath = join(ASSETS_DIR, cssFile);
|
|
123
|
+
if (!existsSync(outPath)) await Bun.write(outPath, cssContent);
|
|
124
|
+
|
|
125
|
+
return { file: cssFile, content: cssContent };
|
|
126
|
+
}
|
|
127
|
+
|
|
58
128
|
export async function buildClientBundle(): Promise<{ js: string; css: string }> {
|
|
59
129
|
await mkdir(ASSETS_DIR, { recursive: true });
|
|
60
|
-
|
|
130
|
+
const twKey = twCacheKey();
|
|
131
|
+
await cleanOldBundles(new Set([`client-${twKey}.css`]));
|
|
61
132
|
|
|
62
133
|
const nodeEnv = process.env.NODE_ENV || "development";
|
|
63
134
|
const result = await Bun.build({
|
|
64
135
|
entrypoints: [join(LIB_DIR, "client.ts")],
|
|
65
136
|
outdir: ASSETS_DIR,
|
|
66
|
-
|
|
67
|
-
splitting: true,
|
|
68
|
-
naming: {
|
|
69
|
-
entry: "client-[hash].[ext]",
|
|
70
|
-
chunk: "chunks/[name]-[hash].[ext]",
|
|
71
|
-
asset: "[name]-[hash].[ext]",
|
|
72
|
-
},
|
|
137
|
+
naming: "client-[hash].[ext]",
|
|
73
138
|
target: "browser",
|
|
74
139
|
minify: nodeEnv === "production",
|
|
75
|
-
optimizeImports: ["lucide-react"],
|
|
76
140
|
define: { "process.env.NODE_ENV": JSON.stringify(nodeEnv) },
|
|
77
141
|
plugins: [
|
|
78
142
|
{
|
|
@@ -92,7 +156,6 @@ export async function buildClientBundle(): Promise<{ js: string; css: string }>
|
|
|
92
156
|
});
|
|
93
157
|
},
|
|
94
158
|
},
|
|
95
|
-
|
|
96
159
|
],
|
|
97
160
|
});
|
|
98
161
|
|
|
@@ -104,50 +167,13 @@ export async function buildClientBundle(): Promise<{ js: string; css: string }>
|
|
|
104
167
|
if (!result.outputs[0]) {
|
|
105
168
|
throw new Error("Client bundle produced no output files");
|
|
106
169
|
}
|
|
107
|
-
// With splitting enabled Bun emits entry + chunk artifacts; select the entry
|
|
108
|
-
// explicitly rather than by position (chunks may precede it in the array).
|
|
109
170
|
const entry = result.outputs.find((o) => o.kind === "entry-point");
|
|
110
171
|
if (!entry) {
|
|
111
172
|
throw new Error("Client bundle produced no entry-point output");
|
|
112
173
|
}
|
|
113
174
|
const jsFile = entry.path.split("/").pop()!;
|
|
114
|
-
const tmpCss = join(ASSETS_DIR, "_tmp.css");
|
|
115
|
-
const proc = Bun.spawn(
|
|
116
|
-
[
|
|
117
|
-
"bun",
|
|
118
|
-
"x",
|
|
119
|
-
"@tailwindcss/cli",
|
|
120
|
-
"-i",
|
|
121
|
-
join(STYLES_DIR, "globals.css"),
|
|
122
|
-
"-o",
|
|
123
|
-
tmpCss,
|
|
124
|
-
"--minify",
|
|
125
|
-
],
|
|
126
|
-
{ stdout: "ignore", stderr: "pipe" }
|
|
127
|
-
);
|
|
128
|
-
await proc.exited;
|
|
129
|
-
if (proc.exitCode !== 0) {
|
|
130
|
-
const err = await new Response(proc.stderr).text();
|
|
131
|
-
throw new Error(`Tailwind CSS build failed:\n${err}`);
|
|
132
|
-
}
|
|
133
175
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
const themeColors = getThemeConfig();
|
|
138
|
-
if (themeColors) {
|
|
139
|
-
cssContent = buildThemeCss(cssContent, themeColors);
|
|
140
|
-
}
|
|
141
|
-
} catch (err) {
|
|
142
|
-
console.warn(
|
|
143
|
-
`[flame] Failed to resolve theme config, falling back to globals.css only: ${err instanceof Error ? err.message : String(err)}`
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const cssHash = new Bun.CryptoHasher("md5").update(cssContent).digest("hex").slice(0, 8);
|
|
148
|
-
const cssFile = `client-${cssHash}.css`;
|
|
149
|
-
await Bun.write(join(ASSETS_DIR, cssFile), cssContent);
|
|
150
|
-
await unlink(tmpCss);
|
|
176
|
+
const { file: cssFile } = await buildTailwindCss(twKey);
|
|
151
177
|
|
|
152
178
|
await Bun.write(join(ASSETS_DIR, "manifest.json"), JSON.stringify({ js: jsFile, css: cssFile }));
|
|
153
179
|
|
package/.docu/node/paths.ts
CHANGED
|
@@ -32,11 +32,12 @@ export const DOCU_CONFIG_PATH = join(PROJECT_ROOT, "docu.json");
|
|
|
32
32
|
// Config singleton
|
|
33
33
|
let _config: DocuConfig | null = null;
|
|
34
34
|
|
|
35
|
-
/** Clean stale client bundles
|
|
36
|
-
export async function cleanOldBundles() {
|
|
35
|
+
/** Clean stale client bundles from a previous build. */
|
|
36
|
+
export async function cleanOldBundles(preserve?: Set<string>) {
|
|
37
37
|
try {
|
|
38
38
|
const files = await readdir(ASSETS_DIR);
|
|
39
39
|
for (const file of files) {
|
|
40
|
+
if (preserve?.has(file)) continue;
|
|
40
41
|
if (file.startsWith("client.") || file.startsWith("client-")) {
|
|
41
42
|
await unlink(join(ASSETS_DIR, file));
|
|
42
43
|
}
|
|
@@ -46,12 +47,13 @@ export async function cleanOldBundles() {
|
|
|
46
47
|
console.error("Failed to clean old bundles:", (err as Error).message);
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
|
-
// Stale split chunks accumulate across builds (content-hashed names); drop
|
|
50
|
-
// the whole chunks dir so only the new build's chunks remain.
|
|
51
50
|
try {
|
|
52
51
|
await rm(join(ASSETS_DIR, "chunks"), { recursive: true, force: true });
|
|
53
52
|
} catch (err) {
|
|
54
|
-
|
|
53
|
+
// chunks dir may not exist, or permission error — log to avoid silent failure
|
|
54
|
+
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
55
|
+
console.warn("[flame] Failed to clean chunks dir:", (err as Error).message);
|
|
56
|
+
}
|
|
55
57
|
}
|
|
56
58
|
}
|
|
57
59
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { DocuConfig } from "./types";
|
|
2
|
+
|
|
3
|
+
export interface SeoMeta {
|
|
4
|
+
/** Absolute canonical URL */
|
|
5
|
+
url: string;
|
|
6
|
+
/** Site name for og:site_name */
|
|
7
|
+
siteName: string;
|
|
8
|
+
/** Absolute OG image URL (from frontmatter.image, if set) */
|
|
9
|
+
image?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Build SEO metadata from config and per-page frontmatter.
|
|
14
|
+
* All fields are derived from existing data — no extra config required.
|
|
15
|
+
*/
|
|
16
|
+
export function buildSeoMeta(
|
|
17
|
+
config: DocuConfig,
|
|
18
|
+
frontmatter: Record<string, unknown>,
|
|
19
|
+
slug: string
|
|
20
|
+
): SeoMeta {
|
|
21
|
+
const baseURL = config.meta?.baseURL?.replace(/\/+$/, "") || "";
|
|
22
|
+
const url = slug ? `${baseURL}/docs/${slug}` : `${baseURL}/`;
|
|
23
|
+
|
|
24
|
+
const result: SeoMeta = {
|
|
25
|
+
url,
|
|
26
|
+
siteName: config.meta?.title || "",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Per-page image from frontmatter, fallback to global default from config
|
|
30
|
+
const image =
|
|
31
|
+
(typeof frontmatter.image === "string" && frontmatter.image) || config.meta?.ogImage;
|
|
32
|
+
if (image) {
|
|
33
|
+
// Resolve using URL constructor — handles absolute, root-relative, and relative paths
|
|
34
|
+
try {
|
|
35
|
+
result.image = new URL(image, image.startsWith("/") ? baseURL : `${baseURL}/docs/`).href;
|
|
36
|
+
} catch {
|
|
37
|
+
result.image = image;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
@@ -279,7 +279,8 @@ export function serveStatic(pathname: string): Response | null {
|
|
|
279
279
|
|
|
280
280
|
export function serverErrorResponse(error: unknown): Response {
|
|
281
281
|
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
282
|
-
const st =
|
|
282
|
+
const st =
|
|
283
|
+
process.env.NODE_ENV !== "production" && error instanceof Error ? error.stack : undefined;
|
|
283
284
|
return new Response(errorHtml(msg, st), {
|
|
284
285
|
status: 500,
|
|
285
286
|
headers: {
|
package/.docu/node/types.ts
CHANGED
|
@@ -20,6 +20,8 @@ export interface DocuMeta {
|
|
|
20
20
|
description: string;
|
|
21
21
|
baseURL: string;
|
|
22
22
|
favicon?: string;
|
|
23
|
+
/** Default OG image path (e.g. /docs/assets/images/og.png). Used when page frontmatter has no image. */
|
|
24
|
+
ogImage?: string;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export interface SocialLink {
|
package/bin/cli.js
CHANGED
|
@@ -12,10 +12,13 @@ const __dirname = import.meta.dirname;
|
|
|
12
12
|
// Deno's npm compat layer may expose `Bun` via globals, check execPath first.
|
|
13
13
|
const runtime =
|
|
14
14
|
process.env.FLAME_RUNTIME ||
|
|
15
|
-
(process.execPath.includes("deno")
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
(process.execPath.includes("deno")
|
|
16
|
+
? "deno"
|
|
17
|
+
: typeof Bun !== "undefined"
|
|
18
|
+
? "bun"
|
|
19
|
+
: typeof Deno !== "undefined"
|
|
20
|
+
? "deno"
|
|
21
|
+
: "node");
|
|
19
22
|
|
|
20
23
|
const COMMAND_MAP = {
|
|
21
24
|
bun: {
|
|
@@ -48,11 +51,15 @@ if (!COMMAND_MAP) {
|
|
|
48
51
|
|
|
49
52
|
const command = process.argv[2];
|
|
50
53
|
|
|
51
|
-
// Parse
|
|
54
|
+
// Parse flags
|
|
52
55
|
const themeIndex = process.argv.indexOf("--theme");
|
|
53
56
|
if (themeIndex !== -1 && themeIndex + 1 < process.argv.length) {
|
|
54
57
|
process.env.FLAME_THEME = process.argv[themeIndex + 1];
|
|
55
58
|
}
|
|
59
|
+
const hasDocker = process.argv.includes("--docker");
|
|
60
|
+
const hasSilent = process.argv.includes("--silent");
|
|
61
|
+
if (hasDocker) process.env.FLAME_DEPLOY_DOCKER = "1";
|
|
62
|
+
if (hasSilent) process.env.FLAME_DEPLOY_SILENT = "1";
|
|
56
63
|
|
|
57
64
|
if (!command || command === "--help" || command === "-h") {
|
|
58
65
|
console.log(`
|
|
@@ -71,6 +78,8 @@ if (!command || command === "--help" || command === "-h") {
|
|
|
71
78
|
Options:
|
|
72
79
|
--help Show this help message
|
|
73
80
|
--theme <name> Override theme preset (e.g. freshlime, coffee). Works with dev, build, preview.
|
|
81
|
+
--docker Generate Docker deployment files (Dockerfile + nginx.conf). Works with deploy.
|
|
82
|
+
--silent Suppress non-essential output. Works with deploy.
|
|
74
83
|
`);
|
|
75
84
|
process.exit(0);
|
|
76
85
|
}
|
|
@@ -120,11 +129,15 @@ if (command === "init") {
|
|
|
120
129
|
}
|
|
121
130
|
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
122
131
|
|
|
132
|
+
// Runtime detection via shebang (#!/usr/bin/env node) makes typeof Bun
|
|
133
|
+
// unavailable — check for bun.lock as a reliable Bun indicator.
|
|
134
|
+
const isBun = existsSync(join(targetDir, "bun.lock"));
|
|
135
|
+
const pkgManager = runtime === "deno" ? "deno" : isBun ? "bun" : "node";
|
|
123
136
|
const nextSteps = {
|
|
124
137
|
bun: " bun install\n bun run dev",
|
|
125
138
|
node: " npm install\n npm run dev",
|
|
126
139
|
deno: " deno task dev\n\n ⚠️ If you see a freshness error, run:\n DENO_ALLOW_NEWER=true deno task dev",
|
|
127
|
-
}[
|
|
140
|
+
}[pkgManager];
|
|
128
141
|
console.log(`\n ✓ Project scaffolded!\n\n Next steps:\n${nextSteps}\n`);
|
|
129
142
|
process.exit(0);
|
|
130
143
|
} catch (err) {
|
package/docu.schema.json
CHANGED
|
@@ -15,7 +15,11 @@
|
|
|
15
15
|
"title": { "type": "string", "description": "Site title" },
|
|
16
16
|
"description": { "type": "string", "description": "Site description" },
|
|
17
17
|
"baseURL": { "type": "string", "description": "Base URL for the site" },
|
|
18
|
-
"favicon": { "type": "string", "description": "Path to favicon" }
|
|
18
|
+
"favicon": { "type": "string", "description": "Path to favicon" },
|
|
19
|
+
"ogImage": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"description": "Default OG image path (e.g. /docs/assets/images/og.png)"
|
|
22
|
+
}
|
|
19
23
|
},
|
|
20
24
|
"required": ["title"]
|
|
21
25
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@docubook/flame",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.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": {
|
|
@@ -54,11 +54,11 @@
|
|
|
54
54
|
"react": "^19.2.7",
|
|
55
55
|
"react-dom": "^19.2.7",
|
|
56
56
|
"unified": "^11.0.0",
|
|
57
|
-
"@docubook/mdx-content": "^3.4.3",
|
|
58
57
|
"@docubook/core": "^1.8.2",
|
|
58
|
+
"@docubook/mdx-content": "^3.4.5",
|
|
59
|
+
"@docubook/runt": "^1.0.0",
|
|
59
60
|
"@docubook/themes-colors": "^1.0.2",
|
|
60
|
-
"@docubook/ui-react": "^1.0.0"
|
|
61
|
-
"@docubook/runt": "^1.0.0"
|
|
61
|
+
"@docubook/ui-react": "^1.0.0"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
64
|
"@sentry/bun": "^10.0.0"
|
|
Binary file
|
package/template/docu.json
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
"title": "My Docs",
|
|
5
5
|
"description": "Documentation powered by DocuBook Flame",
|
|
6
6
|
"baseURL": "http://localhost:3000",
|
|
7
|
-
"favicon": "/docs/assets/images/favicon.ico"
|
|
7
|
+
"favicon": "/docs/assets/images/favicon.ico",
|
|
8
|
+
"ogImage": "/docs/assets/images/og.png"
|
|
8
9
|
},
|
|
9
10
|
"themes": {
|
|
10
11
|
"colors": "default"
|