@docubook/flame 2.0.0-beta.1 → 2.0.0-beta.2

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.
Files changed (52) hide show
  1. package/.docu/lib/build.deno.js +6 -9
  2. package/.docu/lib/build.deno.js.map +1 -0
  3. package/.docu/lib/build.impl-BJsi28mD.js +377 -0
  4. package/.docu/lib/build.impl-BJsi28mD.js.map +1 -0
  5. package/.docu/lib/build.impl-yVLaa1eQ.js +2 -0
  6. package/.docu/lib/build.node.js +6 -9
  7. package/.docu/lib/build.node.js.map +1 -0
  8. package/.docu/lib/clean.js +25 -25
  9. package/.docu/lib/clean.js.map +1 -0
  10. package/.docu/lib/deno-DmEaKggj.js +20 -0
  11. package/.docu/lib/deno-DmEaKggj.js.map +1 -0
  12. package/.docu/lib/deploy.deno.js +9 -9
  13. package/.docu/lib/deploy.deno.js.map +1 -0
  14. package/.docu/lib/deploy.node.js +8 -8
  15. package/.docu/lib/deploy.node.js.map +1 -0
  16. package/.docu/lib/deploy.shared-D3UWafa6.js +295 -0
  17. package/.docu/lib/deploy.shared-D3UWafa6.js.map +1 -0
  18. package/.docu/lib/html.shared-DSn-7_6t.js +2467 -0
  19. package/.docu/lib/html.shared-DSn-7_6t.js.map +1 -0
  20. package/.docu/lib/logger-CycvLCrQ.js +312 -0
  21. package/.docu/lib/logger-CycvLCrQ.js.map +1 -0
  22. package/.docu/lib/node-DPTySdwG.js +80 -0
  23. package/.docu/lib/node-DPTySdwG.js.map +1 -0
  24. package/.docu/lib/paths-BtOIPBQ9.js +52 -0
  25. package/.docu/lib/paths-BtOIPBQ9.js.map +1 -0
  26. package/.docu/lib/preview.deno.js +7 -11
  27. package/.docu/lib/preview.deno.js.map +1 -0
  28. package/.docu/lib/preview.impl-CXJS2tQS.js +77 -0
  29. package/.docu/lib/preview.impl-CXJS2tQS.js.map +1 -0
  30. package/.docu/lib/preview.node.js +7 -11
  31. package/.docu/lib/preview.node.js.map +1 -0
  32. package/.docu/lib/server.deno.js +7 -12
  33. package/.docu/lib/server.deno.js.map +1 -0
  34. package/.docu/lib/server.impl-CJuWimfN.js +358 -0
  35. package/.docu/lib/server.impl-CJuWimfN.js.map +1 -0
  36. package/.docu/lib/server.node.js +7 -12
  37. package/.docu/lib/server.node.js.map +1 -0
  38. package/.docu/lib/utils-DA17MyQG.js +167 -0
  39. package/.docu/lib/utils-DA17MyQG.js.map +1 -0
  40. package/.docu/node/hydrate.node.ts +116 -166
  41. package/bin/compile-lib.mjs +30 -28
  42. package/package.json +6 -6
  43. package/.docu/lib/build.impl-3ZOQORN3.js +0 -12
  44. package/.docu/lib/chunk-4IQXHHPF.js +0 -62
  45. package/.docu/lib/chunk-6EOUQCRM.js +0 -301
  46. package/.docu/lib/chunk-6HFBJUMG.js +0 -434
  47. package/.docu/lib/chunk-EOK6KATZ.js +0 -191
  48. package/.docu/lib/chunk-FERG25YW.js +0 -470
  49. package/.docu/lib/chunk-MQRFIDS4.js +0 -92
  50. package/.docu/lib/chunk-U7M5SK76.js +0 -330
  51. package/.docu/lib/chunk-UISOJ4RW.js +0 -114
  52. package/.docu/lib/chunk-URBATJEC.js +0 -2720
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils-DA17MyQG.js","names":[],"sources":["../node/security.ts","../node/utils.ts"],"sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport { resolve } from \"node:path\";\nimport { realpathSync } from \"node:fs\";\n\nexport const SECURITY_HEADERS: Record<string, string> = {\n \"Strict-Transport-Security\": \"max-age=63072000; includeSubDomains; preload\",\n \"X-Frame-Options\": \"DENY\",\n \"X-Content-Type-Options\": \"nosniff\",\n \"Referrer-Policy\": \"strict-origin-when-cross-origin\",\n \"Permissions-Policy\": \"camera=(), microphone=(), geolocation=()\",\n};\n\nexport function generateNonce(): string {\n return randomBytes(16).toString(\"base64\");\n}\n\nexport function cspHeader(nonce: string, allowEval = false): string {\n const scriptSrc = allowEval\n ? `script-src 'self' 'nonce-${nonce}' 'unsafe-eval'`\n : `script-src 'self' 'nonce-${nonce}'`;\n return [\n \"default-src 'self'\",\n scriptSrc,\n \"style-src 'self' 'unsafe-inline'\",\n \"img-src 'self' https: data:\",\n \"font-src 'self' data:\",\n \"connect-src 'self' https:\",\n \"frame-src https://www.youtube-nocookie.com\",\n \"frame-ancestors 'none'\",\n ].join(\"; \");\n}\n\nexport function isPathSafe(pathname: string, baseDir: string): boolean {\n const decoded = decodeURIComponent(pathname);\n const resolved = resolve(baseDir, decoded.slice(1));\n const baseDirSlash = baseDir.endsWith(\"/\") ? baseDir : baseDir + \"/\";\n if (!(resolved === baseDir || resolved.startsWith(baseDirSlash))) {\n return false;\n }\n\n if (resolved === baseDir) return true;\n try {\n const real = realpathSync(resolved);\n const realBase = realpathSync(baseDir);\n const realBaseDirSlash = realBase.endsWith(\"/\") ? realBase : realBase + \"/\";\n return real.startsWith(realBaseDirSlash);\n } catch (err) {\n const nodeErr = err as NodeJS.ErrnoException;\n if (nodeErr.code === \"ENOENT\") {\n return true;\n }\n console.error(\n `[security] isPathSafe error for pathname=\"${pathname}\": ${nodeErr.message} (code=${nodeErr.code})`\n );\n return false;\n }\n}\n\nexport function isSlugSafe(slug: string, docsDir: string): boolean {\n const resolved = resolve(docsDir, slug);\n const docsDirSlash = docsDir.endsWith(\"/\") ? docsDir : docsDir + \"/\";\n if (!(resolved === docsDir || resolved.startsWith(docsDirSlash))) {\n return false;\n }\n\n if (resolved === docsDir) return true;\n try {\n const real = realpathSync(resolved);\n const realDocsDir = realpathSync(docsDir);\n const realDocsDirSlash = realDocsDir.endsWith(\"/\") ? realDocsDir : realDocsDir + \"/\";\n return real.startsWith(realDocsDirSlash);\n } catch (err) {\n const nodeErr = err as NodeJS.ErrnoException;\n if (nodeErr.code === \"ENOENT\") {\n return true;\n }\n console.error(\n `[security] isSlugSafe error for slug=\"${slug}\": ${nodeErr.message} (code=${nodeErr.code})`\n );\n return false;\n }\n}\n\n/** Normalize an esbuild importer path to a canonical forward-slash absolute form. */\nexport function normalizeImporterPath(importer: string): string {\n return resolve(importer).replace(/\\\\/g, \"/\");\n}\n\nexport function injectNonce(html: string, nonce: string): string {\n return html.replace(/<script\\b(?![^>]*\\bsrc\\s*=)([^>]*)>/gi, (match) => {\n if (/nonce\\s*=/i.test(match)) {\n return match.replace(/nonce=\"[^\"]*\"/i, `nonce=\"${nonce}\"`);\n }\n return match.replace(/>$/, ` nonce=\"${nonce}\">`);\n });\n}\n\nexport interface PluginResponseLike {\n status: number;\n statusText?: string;\n headers: Headers;\n body: BodyInit | null;\n}\n\n/**\n * Wrap a plugin response with security headers.\n * - Fills in SECURITY_HEADERS defaults where plugin hasn't set a value\n * - Adds Content-Security-Policy for HTML responses (with optional unsafe-eval)\n * - Preserves plugin body, status, statusText unchanged\n */\nexport function wrapPluginResponse(\n pluginResponse: PluginResponseLike,\n allowEval = false\n): Response {\n const securedHeaders = new Headers(pluginResponse.headers);\n for (const [key, value] of Object.entries(SECURITY_HEADERS)) {\n if (!securedHeaders.has(key)) {\n securedHeaders.set(key, value);\n }\n }\n let body = pluginResponse.body;\n const contentType = securedHeaders.get(\"Content-Type\") || \"\";\n if (contentType.includes(\"text/html\") && !securedHeaders.has(\"Content-Security-Policy\")) {\n const nonce = generateNonce();\n securedHeaders.set(\"Content-Security-Policy\", cspHeader(nonce, allowEval));\n if (typeof body === \"string\") {\n body = injectNonce(body, nonce);\n }\n }\n return new Response(body, {\n status: pluginResponse.status,\n statusText: pluginResponse.statusText,\n headers: securedHeaders,\n });\n}\n\nexport function htmlResponse(\n html: string,\n nonce: string,\n status = 200,\n allowEval = false\n): Response {\n return new Response(html, {\n status,\n headers: {\n \"Content-Type\": \"text/html\",\n ...SECURITY_HEADERS,\n \"Content-Security-Policy\": cspHeader(nonce, allowEval),\n },\n });\n}\n","export { cn, parseDate, formatDate, formatDate2 } from \"@docubook/core\";\n\nimport { readdir, stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nexport interface ScannedMdxFile {\n path: string;\n absPath: string;\n mtime: number;\n}\n\n/**\n * Scan a directory recursively for MDX/MD files.\n * Skips \"assets\" directories, hidden directories (dot-prefixed), and root-level index.mdx.\n * Shared between build.ts and search-indexer.ts.\n */\nexport async function scanMdxFiles(dir: string, baseDir = \"\"): Promise<ScannedMdxFile[]> {\n const files: ScannedMdxFile[] = [];\n const entries = await readdir(dir, { withFileTypes: true });\n\n for (const entry of entries) {\n const fullPath = join(dir, entry.name);\n const relativePath = baseDir ? `${baseDir}/${entry.name}` : entry.name;\n\n if (entry.isDirectory()) {\n if (entry.name === \"assets\" || entry.name.startsWith(\".\")) continue;\n files.push(...(await scanMdxFiles(fullPath, relativePath)));\n } else if (entry.name.endsWith(\".mdx\") || entry.name.endsWith(\".md\")) {\n if (entry.name === \"index.mdx\" && !baseDir) continue;\n const stats = await stat(fullPath);\n let path = relativePath.replace(/\\.(mdx|md)$/, \"\");\n\n if (/\\/index$/.test(path)) {\n path = path.replace(/\\/index$/, \"\");\n }\n files.push({ path, absPath: fullPath, mtime: stats.mtimeMs });\n }\n }\n\n return files;\n}\n\nexport function isExternalUrl(url: string): boolean {\n return /^(https?:\\/\\/|\\/\\/)/.test(url);\n}\n\n/** Suffix an internal docs link with `.html` to match the flat static build output. */\nexport function docsHtmlHref(path: string): string {\n return `${path}.html`;\n}\n\n/** Map a `/docs{.html,/*.html}` request back to its extensionless route (dev server).\n * Handles both `/docs/*.html` (pages) and `/docs.html` (edge case). */\nexport function stripDocsHtmlSuffix(pathname: string): string {\n if (pathname === \"/docs.html\") return \"/docs\";\n if (pathname.startsWith(\"/docs/\") && pathname.endsWith(\".html\"))\n return pathname.slice(0, -\".html\".length);\n return pathname;\n}\n\nexport function getPath(url: string): string {\n try {\n return new URL(url).pathname;\n } catch (err) {\n console.error(\"Failed to parse URL\", url, err);\n return url;\n }\n}\n\n/** Get git last modified date for a file */\nexport async function getGitLastModified(filePath: string): Promise<string | null> {\n try {\n const cleanPath = filePath.replace(/^\\//, \"\");\n if (\n !cleanPath ||\n !/^[a-zA-Z0-9\\-_/.\\s]+$/.test(cleanPath) ||\n /(^|\\/)\\.\\.($|\\/)/.test(cleanPath)\n )\n return null;\n const proc = Bun.spawn([\"git\", \"log\", \"-1\", \"--format=%cI\", \"--\", cleanPath], {\n stderr: \"ignore\",\n });\n const text = await new Response(proc.stdout).text();\n const date = text.trim();\n return date || null;\n } catch (err) {\n console.error(\"Failed to get git last modified for\", filePath, err);\n return null;\n }\n}\n\n/** Batch git last modified dates for multiple files in a single spawn */\nexport async function getGitLastModifiedBatch(filePaths: string[]): Promise<Map<string, string>> {\n const result = new Map<string, string>();\n if (filePaths.length === 0) return result;\n\n // Filter and validate paths — same guard as getGitLastModified\n const safePaths: string[] = [];\n for (const fp of filePaths) {\n const cleanPath = fp.replace(/^\\//, \"\");\n if (\n !cleanPath ||\n !/^[a-zA-Z0-9\\-_/.\\s]+$/.test(cleanPath) ||\n /(^|\\/)\\.\\.($|\\/)/.test(cleanPath)\n ) {\n console.warn(`[utils] getGitLastModifiedBatch: skipping invalid path \"${fp}\"`);\n continue;\n }\n safePaths.push(cleanPath);\n }\n\n if (safePaths.length === 0) return result;\n\n try {\n const proc = Bun.spawn(\n [\"git\", \"log\", \"--format=%cI\", \"--name-only\", \"--diff-filter=ACMR\", ...safePaths],\n { stderr: \"ignore\" }\n );\n const text = await new Response(proc.stdout).text();\n let currentDate = \"\";\n\n for (const line of text.split(\"\\n\")) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n if (/^\\d{4}-\\d{2}-\\d{2}T/.test(trimmed)) {\n currentDate = trimmed;\n } else if (currentDate && !result.has(trimmed)) {\n result.set(trimmed, currentDate);\n }\n }\n } catch (err) {\n console.error(\"Failed to get git last modified batch for\", filePaths, err);\n }\n\n return result;\n}\n\nconst MIME_TYPES: Record<string, string> = {\n html: \"text/html\",\n css: \"text/css\",\n js: \"application/javascript\",\n json: \"application/json\",\n png: \"image/png\",\n jpg: \"image/jpeg\",\n jpeg: \"image/jpeg\",\n svg: \"image/svg+xml\",\n ico: \"image/x-icon\",\n woff: \"font/woff\",\n woff2: \"font/woff2\",\n};\n\nexport function getContentType(pathname: string): string {\n const ext = pathname.split(\".\").pop()?.toLowerCase();\n return MIME_TYPES[ext || \"\"] || \"application/octet-stream\";\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Client bundle builder for Node/Deno runtimes.
3
3
  *
4
- * Wraps esbuild with the plugins needed to produce a browser-ready client
4
+ * Uses Vite with Rolldown plugins to produce a browser-ready client
5
5
  * bundle (JS + CSS) from the same components Bun.build handles natively.
6
6
  * Theme helpers (getThemeConfig, buildThemeCss, computeInlineThemeCss)
7
7
  * are re-exported from `hydrate.ts` — that module's `buildClientBundle`
@@ -15,6 +15,7 @@ import { existsSync, readFileSync, readdirSync } from "node:fs";
15
15
  import { promisify } from "node:util";
16
16
  import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
17
17
  import { createHash } from "node:crypto";
18
+ import { build as viteBuild } from "vite";
18
19
  import {
19
20
  ASSETS_DIR,
20
21
  FRAMEWORK_ROOT,
@@ -203,180 +204,129 @@ export async function buildClientBundle(
203
204
  await cleanOldBundles(new Set([`client-${twKey}.css`]));
204
205
 
205
206
  const nodeEnv = process.env.NODE_ENV || "development";
206
- const esbuild = await import("esbuild");
207
- const { build } = esbuild;
208
207
 
209
208
  const entryPath = join(LIB_DIR, "client.ts");
210
- const workingDir = process.cwd();
211
- let result: Awaited<ReturnType<typeof build>>;
212
- try {
213
- result = await build({
214
- entryPoints: [entryPath],
215
- bundle: true,
216
- outdir: ASSETS_DIR,
217
- entryNames: "client-[hash]",
218
- platform: "browser",
219
- format: "esm",
220
- minify: nodeEnv === "production",
221
- define: { "process.env.NODE_ENV": JSON.stringify(nodeEnv) },
222
- jsx: "automatic",
223
- jsxDev: nodeEnv !== "production",
224
- metafile: true,
225
- logLevel: "silent",
226
- plugins: [
227
- {
228
- name: "node-builtin-stub",
229
- setup(build) {
230
- build.onResolve({ filter: NODE_BUILTINS_RE }, (args) => ({
231
- path: args.path,
232
- namespace: "node-stub",
233
- }));
234
- build.onLoad({ filter: /.*/, namespace: "node-stub" }, () => ({
235
- contents: "module.exports = {};",
236
- loader: "js",
237
- }));
238
- },
209
+ const bundle = await viteBuild({
210
+ configFile: false,
211
+ publicDir: false,
212
+ define: { "process.env.NODE_ENV": JSON.stringify(nodeEnv) },
213
+ plugins: [
214
+ {
215
+ name: "node-builtin-stub",
216
+ resolveId(id) {
217
+ if (NODE_BUILTINS_RE.test(id)) return `\0node-stub:${id}`;
218
+ return null;
239
219
  },
240
- {
241
- name: "lucide-optimize",
242
- setup(build) {
243
- build.onResolve({ filter: /^lucide-react$/ }, (args) => {
244
- // Imports from within our virtual module go to the real package.
245
- if (args.namespace === "lucide-virt") {
246
- return { path: getLucideRealEntry(), namespace: "file" };
247
- }
248
- // markdown Icon.tsx uses namespace import for arbitrary
249
- // user-provided icon names in MDX — keep full barrel there.
250
- if (args.importer) {
251
- const normalized = normalizeImporterPath(args.importer);
252
- if (normalized.includes("/markdown/dist/")) {
253
- return { path: getLucideRealEntry(), namespace: "file" };
254
- }
255
- }
256
- // All other files get tree-shaken via the virtual module.
257
- // Lucide.tsx renders only config-defined icons, which are
258
- // collected by extractConfigIcons() + collectAllLucideIcons().
259
- return { path: args.path, namespace: "lucide-virt" };
260
- });
261
- build.onLoad({ filter: /.*/, namespace: "lucide-virt" }, () => {
262
- const scanned = collectAllLucideIcons();
263
- const configured = extractConfigIcons(loadDocuConfig());
264
- const allIcons = [...new Set([...scanned, ...configured])];
265
- return {
266
- contents: `export { ${allIcons.join(", ")} } from "lucide-react";`,
267
- loader: "js",
268
- };
269
- });
270
- },
220
+ load(id) {
221
+ if (!id.startsWith("\0node-stub:")) return null;
222
+ return "export default {}; export {};";
271
223
  },
272
- {
273
- name: "docu-config",
274
- setup(build) {
275
- // Components import as "../node/client-routes" (no .ts extension),
276
- // so filter matches the path tail without requiring the extension.
277
- build.onResolve({ filter: /client-routes$/ }, (args) => ({
278
- path: args.path,
279
- namespace: "client-routes",
280
- }));
281
- build.onLoad({ filter: /.*/, namespace: "client-routes" }, () => {
282
- const config = loadDocuConfig();
283
- const resolved = {
284
- ...config,
285
- routes: resolveRoutes(config.routes as DocuRoute[] | undefined),
286
- };
287
- return {
288
- contents: [
289
- `import type { DocuRoute, DocuConfig } from "./types";`,
290
- `const docuConfig = ${JSON.stringify(resolved)};`,
291
- `export const routes = docuConfig.routes || [];`,
292
- `export const config = docuConfig;`,
293
- ].join("\n"),
294
- loader: "ts",
295
- };
296
- });
297
- },
224
+ },
225
+ {
226
+ name: "lucide-optimize",
227
+ resolveId(id, importer) {
228
+ if (id !== "lucide-react") return null;
229
+ if (importer) {
230
+ const normalized = normalizeImporterPath(importer);
231
+ if (normalized.includes("/markdown/dist/")) return null;
232
+ }
233
+ return "\0lucide-virt";
298
234
  },
299
- {
300
- // Serves per-page compiled MDX (program format) as real modules so
301
- // the client hydrates the content island without `new Function`.
302
- // client.ts imports `{ mdxModules } from "./mdx-manifest"`.
303
- name: "mdx-hydrate",
304
- setup(build) {
305
- // Compiled MDX (program format) imports the JSX runtime and the
306
- // MDX provider. Virtual modules have no real directory, so esbuild
307
- // cannot resolve bare specifiers from them — map them to the
308
- // installed real paths explicitly (same pattern as lucide-optimize).
309
- const require = createRequire(import.meta.url);
310
- const mdxModuleDeps: Record<string, string> = {
311
- "react/jsx-runtime": require.resolve("react/jsx-runtime"),
312
- "react/jsx-dev-runtime": require.resolve("react/jsx-dev-runtime"),
313
- "@mdx-js/react": require.resolve("@mdx-js/react"),
314
- };
315
- build.onResolve(
316
- { filter: /^(react\/jsx-runtime|react\/jsx-dev-runtime|@mdx-js\/react)$/ },
317
- (args) => {
318
- if (args.namespace !== "mdx-module") return;
319
- return { path: mdxModuleDeps[args.path], namespace: "file" };
320
- }
321
- );
322
- build.onResolve({ filter: /^mdx-module:/ }, (args) => ({
323
- path: args.path,
324
- namespace: "mdx-module",
325
- }));
326
- build.onLoad({ filter: /.*/, namespace: "mdx-module" }, (args) => {
327
- const slug = args.path.slice("mdx-module:".length);
328
- const contents = mdxSources[slug];
329
- if (contents == null) {
330
- return {
331
- errors: [{ text: `unknown mdx module: ${slug}` }],
332
- contents: "",
333
- loader: "js",
334
- };
335
- }
336
- return { contents, loader: "js" };
337
- });
338
- build.onResolve({ filter: /mdx-manifest$/ }, (args) => ({
339
- path: args.path,
340
- namespace: "mdx-manifest",
341
- }));
342
- build.onLoad({ filter: /.*/, namespace: "mdx-manifest" }, () => {
343
- // Sort keys: the prePass fills mdxSources via Promise.all, so
344
- // insertion order = resolution order (non-deterministic across
345
- // processes). Stable key order keeps the bundle hash stable so
346
- // the build cache (`assetsChanged`) actually hits.
347
- const slugs = Object.keys(mdxSources).sort();
348
- const imports = slugs
349
- .map((slug, i) => {
350
- const key = slug.replace(/["\\]/g, "");
351
- return `import * as _mdx${i} from "mdx-module:${key}";`;
352
- })
353
- .join("\n");
354
- const map = slugs.map((slug, i) => `${JSON.stringify(slug)}: _mdx${i}`).join(", ");
355
- return {
356
- contents: `${imports}\nexport const mdxModules = { ${map} };\n`,
357
- loader: "js",
358
- };
359
- });
360
- },
235
+ load(id) {
236
+ if (id !== "\0lucide-virt") return null;
237
+ const scanned = collectAllLucideIcons();
238
+ const configured = extractConfigIcons(loadDocuConfig());
239
+ const allIcons = [...new Set([...scanned, ...configured])];
240
+ return `export { ${allIcons.join(", ")} } from ${JSON.stringify(getLucideRealEntry())};`;
361
241
  },
362
- ],
363
- });
364
- } finally {
365
- // esbuild's service child process keeps Deno's event loop alive after a
366
- // one-shot build (node-compat gap) — stop it so `flame build` exits.
367
- await esbuild.stop();
368
- }
369
-
370
- // The single entry produces one output. Match resolved source path.
371
- const { outputs } = result.metafile!;
372
- const jsOutput = Object.keys(outputs).find((p) => {
373
- const o = outputs[p];
374
- return o.entryPoint && resolve(workingDir, o.entryPoint) === entryPath;
242
+ },
243
+ {
244
+ name: "docu-config",
245
+ resolveId(id) {
246
+ if (!/client-routes$/.test(id)) return null;
247
+ return "\0client-routes";
248
+ },
249
+ load(id) {
250
+ if (id !== "\0client-routes") return null;
251
+ const config = loadDocuConfig();
252
+ const resolved = {
253
+ ...config,
254
+ routes: resolveRoutes(config.routes as DocuRoute[] | undefined),
255
+ };
256
+ return [
257
+ `const docuConfig = ${JSON.stringify(resolved)};`,
258
+ `export const routes = docuConfig.routes || [];`,
259
+ `export const config = docuConfig;`,
260
+ ].join("\n");
261
+ },
262
+ },
263
+ {
264
+ name: "mdx-hydrate",
265
+ resolveId(id) {
266
+ if (/mdx-manifest$/.test(id)) return "\0mdx-manifest";
267
+ if (id.startsWith("mdx-module:")) return `\0${id}`;
268
+ return null;
269
+ },
270
+ load(id) {
271
+ if (id === "\0mdx-manifest") {
272
+ const slugs = Object.keys(mdxSources).sort();
273
+ const imports = slugs
274
+ .map((slug, i) => {
275
+ const key = slug.replace(/["\\]/g, "");
276
+ return `import * as _mdx${i} from ${JSON.stringify(`mdx-module:${key}`)};`;
277
+ })
278
+ .join("\n");
279
+ const map = slugs.map((slug, i) => `${JSON.stringify(slug)}: _mdx${i}`).join(", ");
280
+ return `${imports}\nexport const mdxModules = { ${map} };\n`;
281
+ }
282
+ if (!id.startsWith("\0mdx-module:")) return null;
283
+ const slug = id.slice("\0mdx-module:".length);
284
+ const contents = mdxSources[slug];
285
+ if (contents == null) {
286
+ throw new Error(`unknown mdx module: ${slug}`);
287
+ }
288
+ return contents;
289
+ },
290
+ },
291
+ ],
292
+ build: {
293
+ outDir: ASSETS_DIR,
294
+ emptyOutDir: false,
295
+ sourcemap: true,
296
+ minify: nodeEnv === "production",
297
+ target: "es2020",
298
+ rollupOptions: {
299
+ input: entryPath,
300
+ output: {
301
+ format: "es",
302
+ entryFileNames: "client-[hash].js",
303
+ chunkFileNames: "chunks/[name]-[hash].js",
304
+ assetFileNames: "assets/[name]-[hash][extname]",
305
+ },
306
+ },
307
+ },
375
308
  });
376
- if (!jsOutput) {
309
+
310
+ const outputs = Array.isArray(bundle) ? bundle : [bundle];
311
+ let jsFile: string | undefined;
312
+ for (const item of outputs) {
313
+ if (!("output" in item)) continue;
314
+ for (const output of item.output) {
315
+ if (
316
+ output.type === "chunk" &&
317
+ output.isEntry &&
318
+ output.facadeModuleId &&
319
+ resolve(output.facadeModuleId) === entryPath
320
+ ) {
321
+ jsFile = basename(output.fileName);
322
+ break;
323
+ }
324
+ }
325
+ if (jsFile) break;
326
+ }
327
+ if (!jsFile) {
377
328
  throw new Error("Client bundle produced no output files");
378
329
  }
379
- const jsFile = basename(jsOutput);
380
330
 
381
331
  const { file: cssFile } = await buildTailwindCss(twKey);
382
332
 
@@ -7,37 +7,39 @@
7
7
  * The CLI routes non-Bun runtimes here (see `bin/cli.js`).
8
8
  */
9
9
 
10
- import { rmSync } from "node:fs";
11
- import { build, stop } from "esbuild";
10
+ import { build } from "vite";
12
11
 
13
- const ENTRIES = [
14
- "server.node.ts",
15
- "server.deno.ts",
16
- "build.node.ts",
17
- "build.deno.ts",
18
- "preview.node.ts",
19
- "preview.deno.ts",
20
- "deploy.node.ts",
21
- "deploy.deno.ts",
22
- "clean.ts",
12
+ const entries = [
13
+ "server.node",
14
+ "server.deno",
15
+ "build.node",
16
+ "build.deno",
17
+ "preview.node",
18
+ "preview.deno",
19
+ "deploy.node",
20
+ "deploy.deno",
21
+ "clean",
23
22
  ];
24
23
 
25
- rmSync(".docu/lib", { recursive: true, force: true });
24
+ const entry = Object.fromEntries(entries.map((name) => [name, `.docu/node/${name}.ts`]));
26
25
 
27
26
  await build({
28
- entryPoints: ENTRIES.map((entry) => `.docu/node/${entry}`),
29
- outdir: ".docu/lib",
30
- bundle: true,
31
- splitting: true,
32
- format: "esm",
33
- platform: "node",
34
- target: "node20",
35
- packages: "external",
36
- jsx: "automatic",
37
- logLevel: "info",
27
+ build: {
28
+ outDir: ".docu/lib",
29
+ emptyOutDir: true,
30
+ ssr: true,
31
+ target: "node20",
32
+ minify: false,
33
+ sourcemap: true,
34
+ lib: {
35
+ entry,
36
+ formats: ["es"],
37
+ fileName: (_format, entryName) => `${entryName}.js`,
38
+ },
39
+ rollupOptions: {
40
+ // Keep npm and runtime dependencies resolvable by the target runtime.
41
+ // Relative imports are bundled from the TypeScript source tree.
42
+ external: (id) => !id.startsWith(".") && !id.startsWith("/") && !id.startsWith("\0"),
43
+ },
44
+ },
38
45
  });
39
-
40
- // esbuild's service process keeps Deno's event loop alive after one-shot
41
- // build() calls — stop it explicitly so `flame` can invoke this script
42
- // under Deno. Harmless under Node.
43
- await stop();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docubook/flame",
3
- "version": "2.0.0-beta.1",
3
+ "version": "2.0.0-beta.2",
4
4
  "description": "A blazing-fast React + MDX framework powered by Bun, built for modern documentation experiences.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -50,16 +50,16 @@
50
50
  "@tailwindcss/typography": "0.5.16",
51
51
  "bun-plugin-tailwind": "^0.1.2",
52
52
  "daisyui": "^5.5.19",
53
- "esbuild": "^0.28.1",
53
+ "vite": "^8.2.1",
54
54
  "lucide-react": "^1.14.0",
55
55
  "react": "^19.2.7",
56
56
  "react-dom": "^19.2.7",
57
57
  "unified": "^11.0.0",
58
58
  "zod": "^4.4.3",
59
- "@docubook/markdown": "^2.0.0-beta.1",
60
- "@docubook/core": "^2.0.0-beta.0",
61
- "@docubook/ui-react": "^2.0.0-beta.0",
62
- "@docubook/themes-colors": "^2.0.0-beta.0"
59
+ "@docubook/core": "^2.0.0-beta.2",
60
+ "@docubook/markdown": "^2.0.0-beta.2",
61
+ "@docubook/themes-colors": "^2.0.0-beta.2",
62
+ "@docubook/ui-react": "^2.0.0-beta.2"
63
63
  },
64
64
  "peerDependencies": {
65
65
  "@sentry/bun": "^10.0.0"
@@ -1,12 +0,0 @@
1
- import {
2
- runBuild,
3
- runBuildCli
4
- } from "./chunk-FERG25YW.js";
5
- import "./chunk-URBATJEC.js";
6
- import "./chunk-EOK6KATZ.js";
7
- import "./chunk-6EOUQCRM.js";
8
- import "./chunk-4IQXHHPF.js";
9
- export {
10
- runBuild,
11
- runBuildCli
12
- };
@@ -1,62 +0,0 @@
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(preserve) {
20
- try {
21
- const files = await readdir(ASSETS_DIR);
22
- for (const file of files) {
23
- if (preserve?.has(file)) continue;
24
- if (file.startsWith("client.") || file.startsWith("client-")) {
25
- await unlink(join(ASSETS_DIR, file));
26
- }
27
- }
28
- } catch (err) {
29
- if (err.code !== "ENOENT") {
30
- console.error("Failed to clean old bundles:", err.message);
31
- }
32
- }
33
- try {
34
- await rm(join(ASSETS_DIR, "chunks"), { recursive: true, force: true });
35
- } catch (err) {
36
- if (err.code !== "ENOENT") {
37
- console.warn("[flame] Failed to clean chunks dir:", err.message);
38
- }
39
- }
40
- }
41
- function loadDocuConfig() {
42
- if (_config) return _config;
43
- if (!existsSync(DOCU_CONFIG_PATH)) {
44
- throw new Error(`docu.json not found at ${DOCU_CONFIG_PATH}`);
45
- }
46
- _config = JSON.parse(readFileSync(DOCU_CONFIG_PATH, "utf-8"));
47
- return _config;
48
- }
49
-
50
- export {
51
- FRAMEWORK_ROOT,
52
- PROJECT_ROOT,
53
- STYLES_DIR,
54
- LIB_DIR,
55
- DIST_DIR,
56
- ASSETS_DIR,
57
- CACHE_FILE,
58
- DOCS_DIR,
59
- DOCS_ASSETS_DIR,
60
- cleanOldBundles,
61
- loadDocuConfig
62
- };