@docubook/flame 1.3.2 → 1.3.4
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/node/plugin-builder.ts +26 -10
- package/.docu/node/plugin.ts +6 -0
- package/.docu/node/server-routes.ts +16 -8
- package/.docu/node/server.ts +20 -2
- package/package.json +3 -3
- package/template/docu.json +1 -1
|
@@ -52,11 +52,7 @@ export class BuildPluginBuilder implements PluginBuilder {
|
|
|
52
52
|
try {
|
|
53
53
|
const result = cb(context);
|
|
54
54
|
if (result) {
|
|
55
|
-
|
|
56
|
-
items.push(...result);
|
|
57
|
-
} else {
|
|
58
|
-
items.push(result);
|
|
59
|
-
}
|
|
55
|
+
this.collectItems(items, result, "injectBody");
|
|
60
56
|
}
|
|
61
57
|
} catch (err) {
|
|
62
58
|
throw new Error(
|
|
@@ -83,11 +79,7 @@ export class BuildPluginBuilder implements PluginBuilder {
|
|
|
83
79
|
try {
|
|
84
80
|
const result = cb(context);
|
|
85
81
|
if (result) {
|
|
86
|
-
|
|
87
|
-
items.push(...result);
|
|
88
|
-
} else {
|
|
89
|
-
items.push(result);
|
|
90
|
-
}
|
|
82
|
+
this.collectItems(items, result, "injectHead");
|
|
91
83
|
}
|
|
92
84
|
} catch (err) {
|
|
93
85
|
throw new Error(
|
|
@@ -462,4 +454,28 @@ export class BuildPluginBuilder implements PluginBuilder {
|
|
|
462
454
|
transformHtml(callback: (html: string, context: PageContext) => Awaitable<string>): void {
|
|
463
455
|
this._transformHtml.push(callback);
|
|
464
456
|
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Collect items from a callback result, filtering only valid strings.
|
|
460
|
+
* Non-string items and unexpected types are logged as warnings.
|
|
461
|
+
*/
|
|
462
|
+
private collectItems(items: string[], result: string | string[], hookName: string): void {
|
|
463
|
+
if (Array.isArray(result)) {
|
|
464
|
+
for (const item of result) {
|
|
465
|
+
if (typeof item === "string") {
|
|
466
|
+
items.push(item);
|
|
467
|
+
} else {
|
|
468
|
+
console.warn(
|
|
469
|
+
`[plugin] ${hookName} callback returned non-string item (got ${typeof item}), skipping`
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
} else if (typeof result === "string") {
|
|
474
|
+
items.push(result);
|
|
475
|
+
} else {
|
|
476
|
+
console.warn(
|
|
477
|
+
`[plugin] ${hookName} callback returned unexpected type (got ${typeof result}), expected string or string[], skipping`
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
465
481
|
}
|
package/.docu/node/plugin.ts
CHANGED
|
@@ -167,6 +167,9 @@ export interface PluginBuilder {
|
|
|
167
167
|
* Register a callback that returns HTML strings to inject inside `<head>`.
|
|
168
168
|
* Results from all plugins are merged and deduplicated.
|
|
169
169
|
*
|
|
170
|
+
* ⚠️ Sanitize any user-controlled or external data before injecting.
|
|
171
|
+
* Plugin-provided strings are injected raw into the final HTML.
|
|
172
|
+
*
|
|
170
173
|
* Use for: analytics snippets, meta tags, stylesheet links.
|
|
171
174
|
*
|
|
172
175
|
* @param callback - Returns a single HTML string or an array. Called once per page.
|
|
@@ -182,6 +185,9 @@ export interface PluginBuilder {
|
|
|
182
185
|
* Register a callback that returns HTML strings to inject before `</body>`.
|
|
183
186
|
* Results from all plugins are merged and deduplicated.
|
|
184
187
|
*
|
|
188
|
+
* ⚠️ Sanitize any user-controlled or external data before injecting.
|
|
189
|
+
* Plugin-provided strings are injected raw into the final HTML.
|
|
190
|
+
*
|
|
185
191
|
* Use for: chat widgets, live-script loaders, deferred scripts.
|
|
186
192
|
*
|
|
187
193
|
* @param callback - Returns a single HTML string or an array. Called once per page.
|
|
@@ -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)
|
|
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 {
|
package/.docu/node/server.ts
CHANGED
|
@@ -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
|
|
|
@@ -107,13 +108,30 @@ const server = Bun.serve({
|
|
|
107
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
|
-
|
|
131
|
+
securedResponse.status,
|
|
114
132
|
Math.round(performance.now() - startTime)
|
|
115
133
|
);
|
|
116
|
-
return
|
|
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.
|
|
3
|
+
"version": "1.3.4",
|
|
4
4
|
"description": "A blazing-fast React + MDX framework powered by Bun, built for modern documentation experiences.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -53,9 +53,9 @@
|
|
|
53
53
|
"react-dom": "^19.2.7",
|
|
54
54
|
"unified": "^11.0.0",
|
|
55
55
|
"@docubook/core": "^1.7.2",
|
|
56
|
-
"@docubook/
|
|
56
|
+
"@docubook/themes-colors": "^0.10.2",
|
|
57
57
|
"@docubook/ui-react": "^0.1.4",
|
|
58
|
-
"@docubook/
|
|
58
|
+
"@docubook/mdx-content": "^3.2.2"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"@sentry/bun": "^10.0.0"
|