@docubook/flame 1.3.1 → 1.3.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.
@@ -7,7 +7,7 @@ import type { LucideIcon } from "lucide-react";
7
7
  */
8
8
  export function getLucideIcon(name: string): LucideIcon | null {
9
9
  if (!name) return null;
10
- const icon = (LucideIcons as Record<string, LucideIcon | undefined>)[name];
10
+ const icon = (LucideIcons as unknown as Record<string, LucideIcon | undefined>)[name];
11
11
  return icon || null;
12
12
  }
13
13
 
@@ -212,16 +212,16 @@ export default function Search({ className }: SearchProps) {
212
212
 
213
213
  <div className="border-base-300 hidden items-center gap-3 border-t px-4 py-2.5 md:flex">
214
214
  <div className="flex items-center gap-1">
215
- <Kbd size="s">
215
+ <Kbd size="sm">
216
216
  <FnKey.Down />
217
217
  </Kbd>
218
- <Kbd size="s">
218
+ <Kbd size="sm">
219
219
  <FnKey.Up />
220
220
  </Kbd>
221
221
  <span className="text-base-content/50 text-xs">navigate</span>
222
222
  </div>
223
223
  <div className="flex items-center gap-1">
224
- <Kbd size="s">
224
+ <Kbd size="sm">
225
225
  <CornerDownLeft className="h-3 w-3" />
226
226
  </Kbd>
227
227
  <span className="text-base-content/50 text-xs">select</span>
@@ -138,7 +138,8 @@ async function renderDocsPage(
138
138
  const headExtra = builder?.collectHead(ctx);
139
139
  const bodyExtra = builder?.collectBody(ctx);
140
140
 
141
- const favicon = docuConfig.meta?.favicon || "/favicon.ico";
141
+ const depth = slug ? slug.split("/").length : 1;
142
+ const favicon = docuConfig.meta?.favicon || "/docs/assets/images/favicon.ico";
142
143
  let html = htmlShell({
143
144
  title,
144
145
  description,
@@ -148,6 +149,7 @@ async function renderDocsPage(
148
149
  js: assetManifest.js,
149
150
  nonce,
150
151
  themeCss: inlineThemeCss,
152
+ depth,
151
153
  headExtra,
152
154
  bodyExtra,
153
155
  });
@@ -332,7 +334,7 @@ async function build() {
332
334
  }
333
335
 
334
336
  const landingPage = React.createElement(IndexPage);
335
- const landingFavicon = docuConfig.meta?.favicon || "/favicon.ico";
337
+ const landingFavicon = docuConfig.meta?.favicon || "/docs/assets/images/favicon.ico";
336
338
  const landingHtml = htmlShell({
337
339
  title: docuConfig.meta?.title || "DocuBook",
338
340
  description: docuConfig.meta?.description || "",
@@ -350,7 +352,7 @@ async function build() {
350
352
  { repoUrl: docuConfig.repo?.url },
351
353
  React.createElement(NotFoundPage)
352
354
  );
353
- const notFoundFavicon = docuConfig.meta?.favicon || "/favicon.ico";
355
+ const notFoundFavicon = docuConfig.meta?.favicon || "/docs/assets/images/favicon.ico";
354
356
  const notFoundHtml = htmlShell({
355
357
  title: "404 - Not Found",
356
358
  description: "",
@@ -14,6 +14,8 @@ export interface HtmlShellOptions {
14
14
  csp?: string;
15
15
  extraScripts?: string;
16
16
  themeCss?: string;
17
+ /** Depth from document root (0=root, 1=subdir, 2=sub/subdir). Used for relative asset paths. */
18
+ depth?: number;
17
19
  /** HTML strings to inject before `</head>` (from plugin `injectHead` hooks). */
18
20
  headExtra?: string[];
19
21
  /** HTML strings to inject before `</body>`, after the main script (from plugin `injectBody` hooks). */
@@ -32,6 +34,7 @@ export function htmlShell(opts: HtmlShellOptions): string {
32
34
  csp,
33
35
  extraScripts,
34
36
  themeCss,
37
+ depth = 0,
35
38
  headExtra,
36
39
  bodyExtra,
37
40
  } = opts;
@@ -39,6 +42,9 @@ export function htmlShell(opts: HtmlShellOptions): string {
39
42
  const themeStyle = themeCss ? `\n <style${nonceAttr}>${Bun.escapeHTML(themeCss)}</style>` : "";
40
43
  const headInjection = headExtra?.length ? `\n ${headExtra.join("\n ")}` : "";
41
44
  const bodyInjection = bodyExtra?.length ? `\n ${bodyExtra.join("\n ")}` : "";
45
+ const depthPrefix = depth === 0 ? "" : "../".repeat(depth);
46
+ const assetPrefix = depthPrefix + "assets/";
47
+ const resolvePath = (path: string) => (path.startsWith("/") ? depthPrefix + path.slice(1) : path);
42
48
  return `<!DOCTYPE html>
43
49
  <html lang="en">
44
50
  <head>
@@ -46,14 +52,14 @@ export function htmlShell(opts: HtmlShellOptions): string {
46
52
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
47
53
  <title>${Bun.escapeHTML(title)}</title>
48
54
  <meta name="description" content="${Bun.escapeHTML(description)}">
49
- <link rel="icon" type="image/x-icon" href="${Bun.escapeHTML(favicon)}">${themeStyle}
50
- <link rel="stylesheet" href="/assets/${Bun.escapeHTML(css)}">
55
+ ${favicon ? `<link rel="icon" type="image/x-icon" href="${Bun.escapeHTML(resolvePath(favicon))}">` : ""}${themeStyle}
56
+ <link rel="stylesheet" href="${Bun.escapeHTML(assetPrefix + css)}">
51
57
  ${csp ? `<meta http-equiv="Content-Security-Policy" content="${Bun.escapeHTML(csp)}">` : ""}
52
58
  <script${nonceAttr}>try{if(localStorage.getItem("theme")==="dark")document.documentElement.classList.add("dark")}catch(e){}</script>${headInjection}
53
59
  </head>
54
60
  <body>
55
61
  <div id="root">${body}</div>
56
- <script${nonceAttr} src="/assets/${Bun.escapeHTML(js)}"></script>${extraScripts ? `\n ${extraScripts}` : ""}${bodyInjection}
62
+ <script${nonceAttr} src="${Bun.escapeHTML(assetPrefix + js)}"></script>${extraScripts ? `\n ${extraScripts}` : ""}${bodyInjection}
57
63
  </body>
58
64
  </html>`;
59
65
  }
@@ -55,6 +55,8 @@ export interface DevServerContext {
55
55
  hostname: string;
56
56
  }
57
57
 
58
+ export type { DocuConfig };
59
+
58
60
  // ─── PluginBuilder Interface ────────────────────────────
59
61
 
60
62
  /**
@@ -19,7 +19,7 @@ import { htmlShell as createHtmlShell, hmrScript, errorHtml } from "./html";
19
19
  export interface ServerState {
20
20
  docuConfig: DocuConfig;
21
21
  assetManifest: { css: string; js: string };
22
- inlineThemeCss: string;
22
+ inlineThemeCss?: string;
23
23
  builder: BuildPluginBuilder | null;
24
24
  }
25
25
 
@@ -28,7 +28,8 @@ function createHtmlResponse(
28
28
  description: string,
29
29
  body: string,
30
30
  status: number,
31
- state: ServerState
31
+ state: ServerState,
32
+ depth = 0
32
33
  ): Response {
33
34
  const nonce = generateNonce();
34
35
  const favicon = state.docuConfig.meta?.favicon || "/favicon.ico";
@@ -42,6 +43,7 @@ function createHtmlResponse(
42
43
  nonce,
43
44
  extraScripts: hmrScript(nonce),
44
45
  themeCss: state.inlineThemeCss,
46
+ depth,
45
47
  });
46
48
  return htmlResponse(html, nonce, status, process.env.NODE_ENV !== "production");
47
49
  }
@@ -140,6 +142,9 @@ async function renderDocsServerPage(
140
142
 
141
143
  const body = renderToString(page);
142
144
 
145
+ // Match build.ts depth calculation: slug.split("/").length, fallback to 1 for empty
146
+ const depth = slug.length || 1;
147
+
143
148
  if (state.builder) {
144
149
  const ctx: PageContext = {
145
150
  slug: slug.join("/") || "/",
@@ -164,12 +169,13 @@ async function renderDocsServerPage(
164
169
  themeCss: state.inlineThemeCss,
165
170
  headExtra,
166
171
  bodyExtra,
172
+ depth,
167
173
  });
168
174
  html = await state.builder.runTransformHtmlChain(html, ctx);
169
175
  return htmlResponse(html, nonce, 200, process.env.NODE_ENV !== "production");
170
176
  }
171
177
 
172
- return createHtmlResponse(title, description, body, 200, state);
178
+ return createHtmlResponse(title, description, body, 200, state, depth);
173
179
  }
174
180
 
175
181
  function renderPage(
@@ -178,7 +184,8 @@ function renderPage(
178
184
  description: string,
179
185
  status: number,
180
186
  state: ServerState,
181
- props: Record<string, unknown> = {}
187
+ props: Record<string, unknown> = {},
188
+ depth = 0
182
189
  ): Response {
183
190
  const page = React.createElement(
184
191
  DocsLayout,
@@ -186,19 +193,20 @@ function renderPage(
186
193
  React.createElement(Component, props)
187
194
  );
188
195
  const body = renderToString(page);
189
- return createHtmlResponse(title, description, body, status, state);
196
+ return createHtmlResponse(title, description, body, status, state, depth);
190
197
  }
191
198
 
192
199
  export async function handleDocsIndex(state: ServerState): Promise<Response> {
193
200
  const doc = await getDocsForSlug("", state);
194
- if (!doc) return renderPage(NotFoundPage, "404 - Not Found", "", 404, state);
201
+ if (!doc) return renderPage(NotFoundPage, "404 - Not Found", "", 404, state, {}, 1);
195
202
  return renderDocsServerPage(doc, [], "/docs", state);
196
203
  }
197
204
 
198
205
  export async function handleDocsRoute(slug: string[], state: ServerState): Promise<Response> {
199
206
  const path = slug.join("/");
200
207
  const doc = await getDocsForSlug(path, state);
201
- if (!doc) return renderPage(NotFoundPage, "404 - Not Found", "", 404, state);
208
+ if (!doc)
209
+ return renderPage(NotFoundPage, "404 - Not Found", "", 404, state, {}, slug.length || 1);
202
210
  return renderDocsServerPage(doc, slug, `/docs/${path}`, state);
203
211
  }
204
212
 
@@ -214,8 +222,8 @@ export function handleIndex(state: ServerState): Response {
214
222
  );
215
223
  }
216
224
 
217
- export function handleNotFound(state: ServerState): Response {
218
- return renderPage(NotFoundPage, "404 - Not Found", "", 404, state);
225
+ export function handleNotFound(state: ServerState, depth = 0): Response {
226
+ return renderPage(NotFoundPage, "404 - Not Found", "", 404, state, {}, depth);
219
227
  }
220
228
 
221
229
  export function serveStatic(pathname: string): Response | null {
@@ -15,6 +15,7 @@ import {
15
15
  serverErrorResponse,
16
16
  type ServerState,
17
17
  } from "./server-routes";
18
+ import { SECURITY_HEADERS, cspHeader, generateNonce } from "./security";
18
19
 
19
20
  const docuConfig = loadDocuConfig();
20
21
 
@@ -103,17 +104,34 @@ const server = Bun.serve({
103
104
 
104
105
  if (builder) {
105
106
  const pluginResponse = await builder.runHandleRequest(req, {
106
- port: server.port,
107
- hostname: server.hostname,
107
+ port: server.port!,
108
+ hostname: server.hostname!,
108
109
  });
109
110
  if (pluginResponse) {
111
+ // Wrap plugin response with security headers.
112
+ // Plugin's own headers take precedence over defaults.
113
+ const securedHeaders = new Headers(pluginResponse.headers);
114
+ for (const [key, value] of Object.entries(SECURITY_HEADERS)) {
115
+ if (!securedHeaders.has(key)) {
116
+ securedHeaders.set(key, value);
117
+ }
118
+ }
119
+ const contentType = securedHeaders.get("Content-Type") || "";
120
+ if (contentType.includes("text/html") && !securedHeaders.has("Content-Security-Policy")) {
121
+ securedHeaders.set("Content-Security-Policy", cspHeader(generateNonce(), true));
122
+ }
123
+ const securedResponse = new Response(pluginResponse.body, {
124
+ status: pluginResponse.status,
125
+ statusText: pluginResponse.statusText,
126
+ headers: securedHeaders,
127
+ });
110
128
  logger.request(
111
129
  req.method,
112
130
  pathname,
113
- pluginResponse.status,
131
+ securedResponse.status,
114
132
  Math.round(performance.now() - startTime)
115
133
  );
116
- return pluginResponse;
134
+ return securedResponse;
117
135
  }
118
136
  }
119
137
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docubook/flame",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
4
  "description": "A blazing-fast React + MDX framework powered by Bun, built for modern documentation experiences.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -49,10 +49,11 @@
49
49
  "bun-plugin-tailwind": "^0.1.2",
50
50
  "daisyui": "^5.5.19",
51
51
  "lucide-react": "^1.14.0",
52
- "react": "^19.0.0",
53
- "react-dom": "^19.0.0",
54
- "@docubook/mdx-content": "^3.2.2",
52
+ "react": "^19.2.7",
53
+ "react-dom": "^19.2.7",
54
+ "unified": "^11.0.0",
55
55
  "@docubook/core": "^1.7.2",
56
+ "@docubook/mdx-content": "^3.2.2",
56
57
  "@docubook/themes-colors": "^0.10.2",
57
58
  "@docubook/ui-react": "^0.1.4"
58
59
  },
@@ -17,7 +17,7 @@
17
17
  "actions": [
18
18
  {
19
19
  "text": "Get Started",
20
- "link": "/docs/getting-started/introduction",
20
+ "link": "/docs",
21
21
  "theme": "primary"
22
22
  },
23
23
  {