@docubook/flame 1.4.3 → 1.5.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 +11 -0
- package/.docu/lib/build.impl-ST63VRTV.js +12 -0
- package/.docu/lib/build.node.js +10 -0
- package/.docu/lib/chunk-2QHMGZIL.js +2419 -0
- package/.docu/lib/chunk-654THQOR.js +461 -0
- package/.docu/lib/chunk-C6RZ2KBH.js +79 -0
- package/.docu/lib/chunk-HH4YXWEF.js +300 -0
- package/.docu/lib/chunk-J5NMYSBJ.js +59 -0
- package/.docu/lib/chunk-RE4NGTMT.js +185 -0
- package/.docu/lib/chunk-X6GYOIYZ.js +383 -0
- package/.docu/lib/chunk-ZOWTASXL.js +92 -0
- package/.docu/lib/clean.js +32 -0
- package/.docu/lib/deploy.deno.js +13 -0
- package/.docu/lib/deploy.node.js +10 -0
- package/.docu/lib/preview.deno.js +10 -0
- package/.docu/lib/preview.node.js +10 -0
- package/.docu/lib/server.deno.js +11 -0
- package/.docu/lib/server.node.js +11 -0
- package/.docu/node/build-summary.ts +126 -0
- package/.docu/node/build.deno.ts +7 -0
- package/.docu/node/build.impl.ts +424 -0
- package/.docu/node/build.node.ts +3 -0
- package/.docu/node/deploy.deno.ts +11 -0
- package/.docu/node/deploy.node.ts +6 -0
- package/.docu/node/deploy.shared.ts +85 -0
- package/.docu/node/deploy.ts +11 -0
- package/.docu/node/escapeHtml.ts +18 -0
- package/.docu/node/git.ts +79 -0
- package/.docu/node/html.shared.ts +110 -0
- package/.docu/node/html.ts +1 -1
- package/.docu/node/hydrate.node.ts +287 -0
- package/.docu/node/hydrate.ts +16 -19
- package/.docu/node/mdx.ts +1 -1
- package/.docu/node/paths.ts +24 -0
- package/.docu/node/plugin-builder.ts +6 -2
- package/.docu/node/plugin.ts +11 -2
- package/.docu/node/preview.deno.ts +4 -0
- package/.docu/node/preview.impl.ts +96 -0
- package/.docu/node/preview.node.ts +4 -0
- package/.docu/node/security.ts +5 -0
- package/.docu/node/server-routes.ts +4 -4
- package/.docu/node/server.deno.ts +4 -0
- package/.docu/node/server.impl.ts +184 -0
- package/.docu/node/server.node.ts +4 -0
- package/.docu/styles/globals.css +20 -5
- package/README.md +57 -506
- package/bin/cli.js +89 -14
- package/bin/compile-lib.mjs +67 -0
- package/package.json +9 -5
- package/template/docs/getting-started/configuration.mdx +18 -0
- package/template/docs/getting-started/overview.mdx +50 -0
- package/template/docs/guide/deployment.mdx +27 -0
- package/template/docs/guide/routing.mdx +25 -0
- package/template/docs/index.mdx +8 -205
- package/template/docu.json +32 -1
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import {
|
|
2
|
+
loadDocuConfig
|
|
3
|
+
} from "./chunk-J5NMYSBJ.js";
|
|
4
|
+
|
|
5
|
+
// package.json
|
|
6
|
+
var package_default = {
|
|
7
|
+
name: "@docubook/flame",
|
|
8
|
+
version: "1.5.0",
|
|
9
|
+
description: "A blazing-fast React + MDX framework powered by Bun, built for modern documentation experiences.",
|
|
10
|
+
type: "module",
|
|
11
|
+
bin: {
|
|
12
|
+
flame: "./bin/cli.js"
|
|
13
|
+
},
|
|
14
|
+
files: [
|
|
15
|
+
"bin",
|
|
16
|
+
"template",
|
|
17
|
+
"docu.schema.json",
|
|
18
|
+
".docu",
|
|
19
|
+
"!.docu/__tests__",
|
|
20
|
+
"!.docu/dist",
|
|
21
|
+
"!.docu/build-cache.json",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
publishConfig: {
|
|
26
|
+
registry: "https://registry.npmjs.org/",
|
|
27
|
+
access: "public"
|
|
28
|
+
},
|
|
29
|
+
engines: {
|
|
30
|
+
node: ">=20.11",
|
|
31
|
+
bun: ">=1.1.0"
|
|
32
|
+
},
|
|
33
|
+
scripts: {
|
|
34
|
+
dev: "bun .docu/node/server.ts",
|
|
35
|
+
build: "NODE_ENV=production bun .docu/node/build.ts",
|
|
36
|
+
clean: "bun .docu/node/clean.ts",
|
|
37
|
+
preview: "bun .docu/node/preview.ts",
|
|
38
|
+
deploy: "bun .docu/node/deploy.ts",
|
|
39
|
+
"compile:lib": "node bin/compile-lib.mjs",
|
|
40
|
+
test: "vitest run",
|
|
41
|
+
lint: "eslint .",
|
|
42
|
+
prepublishOnly: "bun .docu/node/clean.ts && eslint . && vitest run && node bin/compile-lib.mjs"
|
|
43
|
+
},
|
|
44
|
+
keywords: [
|
|
45
|
+
"docubook",
|
|
46
|
+
"documentation",
|
|
47
|
+
"static-site",
|
|
48
|
+
"daisyui",
|
|
49
|
+
"bun",
|
|
50
|
+
"docs framework",
|
|
51
|
+
"react",
|
|
52
|
+
"react-dom"
|
|
53
|
+
],
|
|
54
|
+
homepage: "https://github.com/DocuBook/docubook/tree/main/packages/flame",
|
|
55
|
+
repository: {
|
|
56
|
+
type: "git",
|
|
57
|
+
url: "git+https://github.com/DocuBook/docubook.git",
|
|
58
|
+
directory: "packages/flame"
|
|
59
|
+
},
|
|
60
|
+
author: "wildan.nrs",
|
|
61
|
+
"author-url": "https://wildan.dev",
|
|
62
|
+
license: "MIT",
|
|
63
|
+
dependencies: {
|
|
64
|
+
"@docubook/core": "workspace:^",
|
|
65
|
+
"@docubook/mdx-content": "workspace:^",
|
|
66
|
+
"@docubook/runt": "workspace:^",
|
|
67
|
+
"@docubook/themes-colors": "workspace:^",
|
|
68
|
+
"@docubook/ui-react": "workspace:^",
|
|
69
|
+
"@tailwindcss/cli": "4.3.0",
|
|
70
|
+
"@tailwindcss/typography": "0.5.16",
|
|
71
|
+
"bun-plugin-tailwind": "^0.1.2",
|
|
72
|
+
daisyui: "^5.5.19",
|
|
73
|
+
esbuild: "^0.28.1",
|
|
74
|
+
"lucide-react": "^1.14.0",
|
|
75
|
+
react: "^19.2.7",
|
|
76
|
+
"react-dom": "^19.2.7",
|
|
77
|
+
unified: "^11.0.0"
|
|
78
|
+
},
|
|
79
|
+
peerDependencies: {
|
|
80
|
+
"@sentry/bun": "^10.0.0"
|
|
81
|
+
},
|
|
82
|
+
peerDependenciesMeta: {
|
|
83
|
+
"@sentry/bun": {
|
|
84
|
+
optional: true
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
devDependencies: {
|
|
88
|
+
"@eslint/js": "^10.0.1",
|
|
89
|
+
"@types/node": "^22.0.0",
|
|
90
|
+
"@types/react": "^19.0.0",
|
|
91
|
+
"@types/react-dom": "^19.0.0",
|
|
92
|
+
autoprefixer: "^10.4.0",
|
|
93
|
+
"bun-types": "^1.3.0",
|
|
94
|
+
eslint: "^10.3.0",
|
|
95
|
+
"eslint-plugin-perfectionist": "^5.9.0",
|
|
96
|
+
"eslint-plugin-react": "^7.37.5",
|
|
97
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
98
|
+
postcss: "^8.4.0",
|
|
99
|
+
tailwindcss: "^4.3.0",
|
|
100
|
+
typescript: "^5.9.3",
|
|
101
|
+
"typescript-eslint": "^8.59.2"
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// .docu/node/logger.ts
|
|
106
|
+
var docuConfig = loadDocuConfig();
|
|
107
|
+
var isCI = !!(process.env.CI || process.env.NO_COLOR || !process.stdout.isTTY);
|
|
108
|
+
var LOG_FORMAT = process.env.LOG_FORMAT || "pretty";
|
|
109
|
+
var isJSON = LOG_FORMAT === "json";
|
|
110
|
+
var LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
111
|
+
var currentLevel = LEVELS[process.env.LOG_LEVEL || "info"] ?? LEVELS.info;
|
|
112
|
+
function shouldLog(level) {
|
|
113
|
+
return LEVELS[level] >= currentLevel;
|
|
114
|
+
}
|
|
115
|
+
function jsonLog(level, msg, meta) {
|
|
116
|
+
if (!shouldLog(level)) return;
|
|
117
|
+
const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), ...meta, level, msg };
|
|
118
|
+
const out = level === "error" ? console.error : level === "warn" ? console.warn : console.log;
|
|
119
|
+
out(JSON.stringify(entry));
|
|
120
|
+
}
|
|
121
|
+
function guard(level, msg, meta) {
|
|
122
|
+
if (isJSON) {
|
|
123
|
+
jsonLog(level, msg, meta);
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
return !shouldLog(level);
|
|
127
|
+
}
|
|
128
|
+
var c = isCI || isJSON ? {
|
|
129
|
+
reset: "",
|
|
130
|
+
bold: "",
|
|
131
|
+
dim: "",
|
|
132
|
+
green: "",
|
|
133
|
+
cyan: "",
|
|
134
|
+
magenta: "",
|
|
135
|
+
yellow: "",
|
|
136
|
+
red: "",
|
|
137
|
+
white: "",
|
|
138
|
+
gray: ""
|
|
139
|
+
} : {
|
|
140
|
+
reset: "\x1B[0m",
|
|
141
|
+
bold: "\x1B[1m",
|
|
142
|
+
dim: "\x1B[2m",
|
|
143
|
+
green: "\x1B[32m",
|
|
144
|
+
cyan: "\x1B[36m",
|
|
145
|
+
magenta: "\x1B[35m",
|
|
146
|
+
yellow: "\x1B[33m",
|
|
147
|
+
red: "\x1B[31m",
|
|
148
|
+
white: "\x1B[37m",
|
|
149
|
+
gray: "\x1B[90m"
|
|
150
|
+
};
|
|
151
|
+
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
152
|
+
var Spinner = class {
|
|
153
|
+
frame = 0;
|
|
154
|
+
message = "";
|
|
155
|
+
timer = null;
|
|
156
|
+
info(finalMsg) {
|
|
157
|
+
if (this.timer) clearInterval(this.timer);
|
|
158
|
+
this.timer = null;
|
|
159
|
+
if (isCI) {
|
|
160
|
+
console.log(`\u2139 ${finalMsg}`);
|
|
161
|
+
} else {
|
|
162
|
+
process.stdout.write(`\r\x1B[K${c.cyan}\u2139${c.reset} ${finalMsg}
|
|
163
|
+
`);
|
|
164
|
+
process.stdout.write("\x1B[?25h");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
start(msg) {
|
|
168
|
+
this.message = msg;
|
|
169
|
+
if (isCI) {
|
|
170
|
+
console.log(`\u2026 ${msg}`);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
this.frame = 0;
|
|
174
|
+
process.stdout.write("\x1B[?25l");
|
|
175
|
+
this.render();
|
|
176
|
+
this.timer = setInterval(() => this.render(), 80);
|
|
177
|
+
}
|
|
178
|
+
stop(finalMsg) {
|
|
179
|
+
if (this.timer) clearInterval(this.timer);
|
|
180
|
+
this.timer = null;
|
|
181
|
+
if (isCI) {
|
|
182
|
+
console.log(`\u2713 ${finalMsg}`);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
process.stdout.write(`\r\x1B[K${c.green}\u2713${c.reset} ${finalMsg}
|
|
186
|
+
`);
|
|
187
|
+
process.stdout.write("\x1B[?25h");
|
|
188
|
+
}
|
|
189
|
+
render() {
|
|
190
|
+
const f = SPINNER_FRAMES[this.frame % SPINNER_FRAMES.length];
|
|
191
|
+
process.stdout.write(`\r${c.cyan}${f}${c.reset} ${this.message}`);
|
|
192
|
+
this.frame++;
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
var MAX_ROUTE_LINES = 5;
|
|
196
|
+
var routeLineCount = 0;
|
|
197
|
+
var routeTruncated = false;
|
|
198
|
+
function printRouteTree(routes, prefix = "", isRoot = true) {
|
|
199
|
+
for (let i = 0; i < routes.length; i++) {
|
|
200
|
+
if (routeTruncated) return;
|
|
201
|
+
if (routeLineCount >= MAX_ROUTE_LINES) {
|
|
202
|
+
routeTruncated = true;
|
|
203
|
+
process.stdout.write(`${prefix}${c.dim}...${c.reset}
|
|
204
|
+
`);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const route = routes[i];
|
|
208
|
+
const last = i === routes.length - 1;
|
|
209
|
+
const connector = isRoot ? "" : last ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
210
|
+
const childPrefix = isRoot ? "" : last ? " " : "\u2502 ";
|
|
211
|
+
const path = route.noLink ? `${c.dim}${route.href}${c.reset}` : `${c.white}${route.href}${c.reset}`;
|
|
212
|
+
const label = route.noLink ? `${c.gray}(group)${c.reset}` : "";
|
|
213
|
+
process.stdout.write(`${prefix}${connector}${path} ${label}
|
|
214
|
+
`);
|
|
215
|
+
routeLineCount++;
|
|
216
|
+
if (route.items?.length) {
|
|
217
|
+
printRouteTree(route.items, prefix + childPrefix, false);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
var logger = {
|
|
222
|
+
spinner: new Spinner(),
|
|
223
|
+
buildStart() {
|
|
224
|
+
if (guard("info", "build_start", { package: "@docubook/flame", version: package_default.version })) return;
|
|
225
|
+
console.log(
|
|
226
|
+
`
|
|
227
|
+
${c.bold}${c.cyan} \u{1F525} DocuBook Flame${c.reset} ${c.dim}v${package_default.version}${c.reset}
|
|
228
|
+
`
|
|
229
|
+
);
|
|
230
|
+
},
|
|
231
|
+
bundleStart() {
|
|
232
|
+
if (guard("info", "bundle_start")) return;
|
|
233
|
+
this.spinner.start("Building client bundle...");
|
|
234
|
+
},
|
|
235
|
+
bundleDone(ms) {
|
|
236
|
+
if (guard("info", "bundle_done", { duration_ms: ms })) return;
|
|
237
|
+
this.spinner.stop(`Client bundle compiled ${c.dim}(${ms}ms)${c.reset}`);
|
|
238
|
+
},
|
|
239
|
+
indexStart() {
|
|
240
|
+
if (guard("info", "index_start")) return;
|
|
241
|
+
this.spinner.start("Generating search index...");
|
|
242
|
+
},
|
|
243
|
+
indexDone(records, ms) {
|
|
244
|
+
if (guard("info", "index_done", { records, duration_ms: ms })) return;
|
|
245
|
+
this.spinner.stop(`Search index generated ${c.dim}(${records} records, ${ms}ms)${c.reset}`);
|
|
246
|
+
},
|
|
247
|
+
routes() {
|
|
248
|
+
if (guard("debug", "routes")) return;
|
|
249
|
+
const routes = docuConfig.routes;
|
|
250
|
+
if (!routes?.length) return;
|
|
251
|
+
console.log(`
|
|
252
|
+
${c.bold} Routes${c.reset} ${c.dim}(${countRoutes(routes)} pages)${c.reset}
|
|
253
|
+
`);
|
|
254
|
+
routeLineCount = 0;
|
|
255
|
+
routeTruncated = false;
|
|
256
|
+
printRouteTree(routes, " ");
|
|
257
|
+
},
|
|
258
|
+
ready(port, hmr = false) {
|
|
259
|
+
if (guard("info", "server_ready", { port, hmr })) return;
|
|
260
|
+
console.log(
|
|
261
|
+
`
|
|
262
|
+
${c.green}${c.bold} \u2713 Ready${c.reset} in ${c.bold}http://localhost:${port}/docs/${c.reset}`
|
|
263
|
+
);
|
|
264
|
+
if (hmr) console.log(`${c.dim} HMR enabled \u2014 watching docs/ for changes${c.reset}`);
|
|
265
|
+
console.log("");
|
|
266
|
+
},
|
|
267
|
+
request(method, pathname, status, duration_ms) {
|
|
268
|
+
if (guard("info", "http_request", { method, path: pathname, status, duration_ms })) return;
|
|
269
|
+
const color = status >= 500 ? c.red : status >= 400 ? c.gray : c.green;
|
|
270
|
+
const ts = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
271
|
+
const dur = duration_ms != null ? ` ${c.dim}${duration_ms}ms${c.reset}` : "";
|
|
272
|
+
console.log(
|
|
273
|
+
`${c.dim} ${ts}${c.reset} ${method} ${pathname} ${color}${status}${c.reset}${dur}`
|
|
274
|
+
);
|
|
275
|
+
},
|
|
276
|
+
debug(msg, meta) {
|
|
277
|
+
if (guard("debug", msg, meta)) return;
|
|
278
|
+
console.log(`${c.gray} [debug] ${msg}${c.reset}`);
|
|
279
|
+
},
|
|
280
|
+
warn(msg, meta) {
|
|
281
|
+
if (guard("warn", msg, meta)) return;
|
|
282
|
+
console.warn(`${c.yellow} \u26A0 ${msg}${c.reset}`);
|
|
283
|
+
},
|
|
284
|
+
error(msg, meta) {
|
|
285
|
+
if (guard("error", msg, meta)) return;
|
|
286
|
+
console.error(`${c.red} \u2716 ${msg}${c.reset}`);
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
function countRoutes(routes) {
|
|
290
|
+
let count = 0;
|
|
291
|
+
for (const r of routes) {
|
|
292
|
+
if (!r.noLink) count++;
|
|
293
|
+
if (r.items) count += countRoutes(r.items);
|
|
294
|
+
}
|
|
295
|
+
return count;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export {
|
|
299
|
+
logger
|
|
300
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// .docu/node/paths.ts
|
|
2
|
+
import { resolve, join } from "node:path";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { readdir, rm, unlink } from "node:fs/promises";
|
|
5
|
+
var FRAMEWORK_ROOT = resolve(import.meta.dirname, "../..");
|
|
6
|
+
var PROJECT_ROOT = process.cwd();
|
|
7
|
+
var PAGES_DIR = join(FRAMEWORK_ROOT, ".docu/pages");
|
|
8
|
+
var STYLES_DIR = join(FRAMEWORK_ROOT, ".docu/styles");
|
|
9
|
+
var nodeDir = join(FRAMEWORK_ROOT, ".docu/node");
|
|
10
|
+
var libDir = join(FRAMEWORK_ROOT, ".docu/lib");
|
|
11
|
+
var LIB_DIR = existsSync(nodeDir) ? nodeDir : libDir;
|
|
12
|
+
var DIST_DIR = join(PROJECT_ROOT, ".docu/dist");
|
|
13
|
+
var ASSETS_DIR = join(DIST_DIR, "assets");
|
|
14
|
+
var CACHE_FILE = join(PROJECT_ROOT, ".docu/build-cache.json");
|
|
15
|
+
var DOCS_DIR = join(PROJECT_ROOT, "docs");
|
|
16
|
+
var DOCS_ASSETS_DIR = join(PROJECT_ROOT, "docs/assets");
|
|
17
|
+
var DOCU_CONFIG_PATH = join(PROJECT_ROOT, "docu.json");
|
|
18
|
+
var _config = null;
|
|
19
|
+
async function cleanOldBundles() {
|
|
20
|
+
try {
|
|
21
|
+
const files = await readdir(ASSETS_DIR);
|
|
22
|
+
for (const file of files) {
|
|
23
|
+
if (file.startsWith("client.") || file.startsWith("client-")) {
|
|
24
|
+
await unlink(join(ASSETS_DIR, file));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
} catch (err) {
|
|
28
|
+
if (err.code !== "ENOENT") {
|
|
29
|
+
console.error("Failed to clean old bundles:", err.message);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
await rm(join(ASSETS_DIR, "chunks"), { recursive: true, force: true });
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error("Failed to clean old chunks:", err.message);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function loadDocuConfig() {
|
|
39
|
+
if (_config) return _config;
|
|
40
|
+
if (!existsSync(DOCU_CONFIG_PATH)) {
|
|
41
|
+
throw new Error(`docu.json not found at ${DOCU_CONFIG_PATH}`);
|
|
42
|
+
}
|
|
43
|
+
_config = JSON.parse(readFileSync(DOCU_CONFIG_PATH, "utf-8"));
|
|
44
|
+
return _config;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export {
|
|
48
|
+
FRAMEWORK_ROOT,
|
|
49
|
+
PROJECT_ROOT,
|
|
50
|
+
STYLES_DIR,
|
|
51
|
+
LIB_DIR,
|
|
52
|
+
DIST_DIR,
|
|
53
|
+
ASSETS_DIR,
|
|
54
|
+
CACHE_FILE,
|
|
55
|
+
DOCS_DIR,
|
|
56
|
+
DOCS_ASSETS_DIR,
|
|
57
|
+
cleanOldBundles,
|
|
58
|
+
loadDocuConfig
|
|
59
|
+
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// .docu/node/security.ts
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { realpathSync } from "node:fs";
|
|
4
|
+
var SECURITY_HEADERS = {
|
|
5
|
+
"Strict-Transport-Security": "max-age=63072000; includeSubDomains; preload",
|
|
6
|
+
"X-Frame-Options": "DENY",
|
|
7
|
+
"X-Content-Type-Options": "nosniff",
|
|
8
|
+
"Referrer-Policy": "strict-origin-when-cross-origin",
|
|
9
|
+
"Permissions-Policy": "camera=(), microphone=(), geolocation=()"
|
|
10
|
+
};
|
|
11
|
+
function generateNonce() {
|
|
12
|
+
return crypto.randomUUID();
|
|
13
|
+
}
|
|
14
|
+
function cspHeader(nonce, allowEval = false) {
|
|
15
|
+
const scriptSrc = allowEval ? `script-src 'self' 'nonce-${nonce}' 'unsafe-eval'` : `script-src 'self' 'nonce-${nonce}'`;
|
|
16
|
+
return [
|
|
17
|
+
"default-src 'self'",
|
|
18
|
+
scriptSrc,
|
|
19
|
+
"style-src 'self' 'unsafe-inline'",
|
|
20
|
+
"img-src 'self' https: data:",
|
|
21
|
+
"font-src 'self' data:",
|
|
22
|
+
"connect-src 'self' https:",
|
|
23
|
+
"frame-src https://www.youtube-nocookie.com",
|
|
24
|
+
"frame-ancestors 'none'"
|
|
25
|
+
].join("; ");
|
|
26
|
+
}
|
|
27
|
+
function isPathSafe(pathname, baseDir) {
|
|
28
|
+
const decoded = decodeURIComponent(pathname);
|
|
29
|
+
const resolved = resolve(baseDir, decoded.slice(1));
|
|
30
|
+
const baseDirSlash = baseDir.endsWith("/") ? baseDir : baseDir + "/";
|
|
31
|
+
if (!(resolved === baseDir || resolved.startsWith(baseDirSlash))) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
if (resolved === baseDir) return true;
|
|
35
|
+
try {
|
|
36
|
+
const real = realpathSync(resolved);
|
|
37
|
+
const realBase = realpathSync(baseDir);
|
|
38
|
+
const realBaseDirSlash = realBase.endsWith("/") ? realBase : realBase + "/";
|
|
39
|
+
return real.startsWith(realBaseDirSlash);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
const nodeErr = err;
|
|
42
|
+
if (nodeErr.code === "ENOENT") {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
console.error(
|
|
46
|
+
`[security] isPathSafe error for pathname="${pathname}": ${nodeErr.message} (code=${nodeErr.code})`
|
|
47
|
+
);
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function isSlugSafe(slug, docsDir) {
|
|
52
|
+
const resolved = resolve(docsDir, slug);
|
|
53
|
+
const docsDirSlash = docsDir.endsWith("/") ? docsDir : docsDir + "/";
|
|
54
|
+
if (!(resolved === docsDir || resolved.startsWith(docsDirSlash))) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
if (resolved === docsDir) return true;
|
|
58
|
+
try {
|
|
59
|
+
const real = realpathSync(resolved);
|
|
60
|
+
const realDocsDir = realpathSync(docsDir);
|
|
61
|
+
const realDocsDirSlash = realDocsDir.endsWith("/") ? realDocsDir : realDocsDir + "/";
|
|
62
|
+
return real.startsWith(realDocsDirSlash);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
const nodeErr = err;
|
|
65
|
+
if (nodeErr.code === "ENOENT") {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
console.error(
|
|
69
|
+
`[security] isSlugSafe error for slug="${slug}": ${nodeErr.message} (code=${nodeErr.code})`
|
|
70
|
+
);
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function normalizeImporterPath(importer) {
|
|
75
|
+
return resolve(importer).replace(/\\/g, "/");
|
|
76
|
+
}
|
|
77
|
+
function injectNonce(html, nonce) {
|
|
78
|
+
return html.replace(/<script\b(?![^>]*\bsrc\s*=)([^>]*)>/gi, (match) => {
|
|
79
|
+
if (/nonce\s*=/i.test(match)) {
|
|
80
|
+
return match.replace(/nonce="[^"]*"/i, `nonce="${nonce}"`);
|
|
81
|
+
}
|
|
82
|
+
return match.replace(/>$/, ` nonce="${nonce}">`);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
function wrapPluginResponse(pluginResponse, allowEval = false) {
|
|
86
|
+
const securedHeaders = new Headers(pluginResponse.headers);
|
|
87
|
+
for (const [key, value] of Object.entries(SECURITY_HEADERS)) {
|
|
88
|
+
if (!securedHeaders.has(key)) {
|
|
89
|
+
securedHeaders.set(key, value);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const contentType = securedHeaders.get("Content-Type") || "";
|
|
93
|
+
if (contentType.includes("text/html") && !securedHeaders.has("Content-Security-Policy")) {
|
|
94
|
+
securedHeaders.set("Content-Security-Policy", cspHeader(generateNonce(), allowEval));
|
|
95
|
+
}
|
|
96
|
+
return new Response(pluginResponse.body, {
|
|
97
|
+
status: pluginResponse.status,
|
|
98
|
+
statusText: pluginResponse.statusText,
|
|
99
|
+
headers: securedHeaders
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function htmlResponse(html, nonce, status = 200, allowEval = false) {
|
|
103
|
+
return new Response(html, {
|
|
104
|
+
status,
|
|
105
|
+
headers: {
|
|
106
|
+
"Content-Type": "text/html",
|
|
107
|
+
...SECURITY_HEADERS,
|
|
108
|
+
"Content-Security-Policy": cspHeader(nonce, allowEval)
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// .docu/node/utils.ts
|
|
114
|
+
import { cn, parseDate, formatDate, formatDate2 } from "@docubook/core";
|
|
115
|
+
import { readdir, stat } from "node:fs/promises";
|
|
116
|
+
import { join } from "node:path";
|
|
117
|
+
async function scanMdxFiles(dir, baseDir = "") {
|
|
118
|
+
const files = [];
|
|
119
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
120
|
+
for (const entry of entries) {
|
|
121
|
+
const fullPath = join(dir, entry.name);
|
|
122
|
+
const relativePath = baseDir ? `${baseDir}/${entry.name}` : entry.name;
|
|
123
|
+
if (entry.isDirectory()) {
|
|
124
|
+
if (entry.name === "assets" || entry.name.startsWith(".")) continue;
|
|
125
|
+
files.push(...await scanMdxFiles(fullPath, relativePath));
|
|
126
|
+
} else if (entry.name.endsWith(".mdx") || entry.name.endsWith(".md")) {
|
|
127
|
+
if (entry.name === "index.mdx" && !baseDir) continue;
|
|
128
|
+
const stats = await stat(fullPath);
|
|
129
|
+
let path = relativePath.replace(/\.(mdx|md)$/, "");
|
|
130
|
+
if (/\/index$/.test(path)) {
|
|
131
|
+
path = path.replace(/\/index$/, "");
|
|
132
|
+
}
|
|
133
|
+
files.push({ path, absPath: fullPath, mtime: stats.mtimeMs });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return files;
|
|
137
|
+
}
|
|
138
|
+
function isExternalUrl(url) {
|
|
139
|
+
return /^(https?:\/\/|\/\/)/.test(url);
|
|
140
|
+
}
|
|
141
|
+
function docsHtmlHref(path) {
|
|
142
|
+
return `${path}.html`;
|
|
143
|
+
}
|
|
144
|
+
function stripDocsHtmlSuffix(pathname) {
|
|
145
|
+
if (pathname === "/docs.html") return "/docs";
|
|
146
|
+
if (pathname.startsWith("/docs/") && pathname.endsWith(".html"))
|
|
147
|
+
return pathname.slice(0, -".html".length);
|
|
148
|
+
return pathname;
|
|
149
|
+
}
|
|
150
|
+
var MIME_TYPES = {
|
|
151
|
+
html: "text/html",
|
|
152
|
+
css: "text/css",
|
|
153
|
+
js: "application/javascript",
|
|
154
|
+
json: "application/json",
|
|
155
|
+
png: "image/png",
|
|
156
|
+
jpg: "image/jpeg",
|
|
157
|
+
jpeg: "image/jpeg",
|
|
158
|
+
svg: "image/svg+xml",
|
|
159
|
+
ico: "image/x-icon",
|
|
160
|
+
woff: "font/woff",
|
|
161
|
+
woff2: "font/woff2"
|
|
162
|
+
};
|
|
163
|
+
function getContentType(pathname) {
|
|
164
|
+
const ext = pathname.split(".").pop()?.toLowerCase();
|
|
165
|
+
return MIME_TYPES[ext || ""] || "application/octet-stream";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export {
|
|
169
|
+
SECURITY_HEADERS,
|
|
170
|
+
generateNonce,
|
|
171
|
+
cspHeader,
|
|
172
|
+
isPathSafe,
|
|
173
|
+
isSlugSafe,
|
|
174
|
+
normalizeImporterPath,
|
|
175
|
+
injectNonce,
|
|
176
|
+
wrapPluginResponse,
|
|
177
|
+
htmlResponse,
|
|
178
|
+
scanMdxFiles,
|
|
179
|
+
isExternalUrl,
|
|
180
|
+
docsHtmlHref,
|
|
181
|
+
stripDocsHtmlSuffix,
|
|
182
|
+
getContentType,
|
|
183
|
+
cn,
|
|
184
|
+
formatDate2
|
|
185
|
+
};
|