@docubook/flame 1.4.4 → 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.
Files changed (54) hide show
  1. package/.docu/lib/build.deno.js +11 -0
  2. package/.docu/lib/build.impl-ST63VRTV.js +12 -0
  3. package/.docu/lib/build.node.js +10 -0
  4. package/.docu/lib/chunk-2QHMGZIL.js +2419 -0
  5. package/.docu/lib/chunk-654THQOR.js +461 -0
  6. package/.docu/lib/chunk-C6RZ2KBH.js +79 -0
  7. package/.docu/lib/chunk-HH4YXWEF.js +300 -0
  8. package/.docu/lib/chunk-J5NMYSBJ.js +59 -0
  9. package/.docu/lib/chunk-RE4NGTMT.js +185 -0
  10. package/.docu/lib/chunk-X6GYOIYZ.js +383 -0
  11. package/.docu/lib/chunk-ZOWTASXL.js +92 -0
  12. package/.docu/lib/clean.js +32 -0
  13. package/.docu/lib/deploy.deno.js +13 -0
  14. package/.docu/lib/deploy.node.js +10 -0
  15. package/.docu/lib/preview.deno.js +10 -0
  16. package/.docu/lib/preview.node.js +10 -0
  17. package/.docu/lib/server.deno.js +11 -0
  18. package/.docu/lib/server.node.js +11 -0
  19. package/.docu/node/build-summary.ts +126 -0
  20. package/.docu/node/build.deno.ts +7 -0
  21. package/.docu/node/build.impl.ts +424 -0
  22. package/.docu/node/build.node.ts +3 -0
  23. package/.docu/node/deploy.deno.ts +11 -0
  24. package/.docu/node/deploy.node.ts +6 -0
  25. package/.docu/node/deploy.shared.ts +85 -0
  26. package/.docu/node/deploy.ts +11 -0
  27. package/.docu/node/escapeHtml.ts +18 -0
  28. package/.docu/node/git.ts +79 -0
  29. package/.docu/node/html.shared.ts +110 -0
  30. package/.docu/node/hydrate.node.ts +287 -0
  31. package/.docu/node/hydrate.ts +16 -19
  32. package/.docu/node/mdx.ts +1 -1
  33. package/.docu/node/paths.ts +24 -0
  34. package/.docu/node/plugin-builder.ts +6 -2
  35. package/.docu/node/plugin.ts +11 -2
  36. package/.docu/node/preview.deno.ts +4 -0
  37. package/.docu/node/preview.impl.ts +96 -0
  38. package/.docu/node/preview.node.ts +4 -0
  39. package/.docu/node/security.ts +5 -0
  40. package/.docu/node/server-routes.ts +4 -4
  41. package/.docu/node/server.deno.ts +4 -0
  42. package/.docu/node/server.impl.ts +184 -0
  43. package/.docu/node/server.node.ts +4 -0
  44. package/.docu/styles/globals.css +20 -5
  45. package/README.md +57 -506
  46. package/bin/cli.js +89 -14
  47. package/bin/compile-lib.mjs +67 -0
  48. package/package.json +9 -5
  49. package/template/docs/getting-started/configuration.mdx +18 -0
  50. package/template/docs/getting-started/overview.mdx +50 -0
  51. package/template/docs/guide/deployment.mdx +27 -0
  52. package/template/docs/guide/routing.mdx +25 -0
  53. package/template/docs/index.mdx +8 -205
  54. package/template/docu.json +32 -1
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Runtime-neutral preview server — mirror of `preview.ts` (Bun-only,
3
+ * protected) driven by a `RuntimeAdapter` and `node:fs` file reads.
4
+ */
5
+
6
+ import { existsSync, statSync, readFileSync } from "node:fs";
7
+ import { readFile } from "node:fs/promises";
8
+ import { resolve } from "node:path";
9
+ import type { RuntimeAdapter, ServerHandle } from "@docubook/runt";
10
+ import { logger } from "./logger";
11
+ import { DIST_DIR } from "./paths";
12
+ import { getContentType } from "./utils";
13
+ import { SECURITY_HEADERS, generateNonce, cspHeader, injectNonce } from "./security";
14
+
15
+ function resolveFile(pathname: string): string | null {
16
+ const path = pathname.slice(1);
17
+ const isWithinDist = (p: string) => p === DIST_DIR || p.startsWith(DIST_DIR + "/");
18
+
19
+ if (!path.includes(".")) {
20
+ const withIndex = resolve(DIST_DIR, path, "index.html");
21
+ if (isWithinDist(withIndex) && existsSync(withIndex)) return withIndex;
22
+ const withHtml = resolve(DIST_DIR, path + ".html");
23
+ if (isWithinDist(withHtml) && existsSync(withHtml)) return withHtml;
24
+ }
25
+
26
+ const exact = resolve(DIST_DIR, path);
27
+ if (!isWithinDist(exact)) return null;
28
+ try {
29
+ if (statSync(exact).isFile()) return exact;
30
+ } catch (err) {
31
+ if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
32
+ }
33
+ return null;
34
+ }
35
+
36
+ export async function runPreview(adapter: RuntimeAdapter): Promise<ServerHandle | null> {
37
+ const PORT = parseInt(process.env.PORT || "4173", 10);
38
+
39
+ logger.buildStart();
40
+
41
+ if (!existsSync(DIST_DIR)) {
42
+ logger.spinner.start("Checking build output...");
43
+ logger.spinner.info("dist not found. Run \x1b[1mflame build\x1b[0m first.");
44
+ process.exit(0);
45
+ }
46
+
47
+ const notFoundPath = resolve(DIST_DIR, "404.html");
48
+
49
+ const handle = await adapter.serve(
50
+ async (req) => {
51
+ const url = new URL(req.url);
52
+ const pathname = decodeURIComponent(url.pathname);
53
+
54
+ const filePath = resolveFile(pathname);
55
+
56
+ if (filePath) {
57
+ const contentType = getContentType(filePath);
58
+ if (contentType === "text/html") {
59
+ const nonce = generateNonce();
60
+ const html = await readFile(filePath, "utf-8");
61
+ const modified = injectNonce(html, nonce);
62
+ return new Response(modified, {
63
+ headers: {
64
+ "Content-Type": "text/html",
65
+ ...SECURITY_HEADERS,
66
+ "Content-Security-Policy": cspHeader(nonce, true),
67
+ },
68
+ });
69
+ }
70
+ return new Response(readFileSync(filePath), {
71
+ headers: { "Content-Type": contentType },
72
+ });
73
+ }
74
+
75
+ if (existsSync(notFoundPath)) {
76
+ const nonce = generateNonce();
77
+ const html = await readFile(notFoundPath, "utf-8");
78
+ const modified = injectNonce(html, nonce);
79
+ return new Response(modified, {
80
+ status: 404,
81
+ headers: {
82
+ "Content-Type": "text/html",
83
+ ...SECURITY_HEADERS,
84
+ "Content-Security-Policy": cspHeader(nonce, true),
85
+ },
86
+ });
87
+ }
88
+
89
+ return new Response("404 - Not Found", { status: 404 });
90
+ },
91
+ { port: PORT }
92
+ );
93
+
94
+ logger.ready(handle.port);
95
+ return handle;
96
+ }
@@ -0,0 +1,4 @@
1
+ import { nodeAdapter } from "@docubook/runt";
2
+ import { runPreview } from "./preview.impl";
3
+
4
+ await runPreview(nodeAdapter);
@@ -80,6 +80,11 @@ export function isSlugSafe(slug: string, docsDir: string): boolean {
80
80
  }
81
81
  }
82
82
 
83
+ /** Normalize an esbuild importer path to a canonical forward-slash absolute form. */
84
+ export function normalizeImporterPath(importer: string): string {
85
+ return resolve(importer).replace(/\\/g, "/");
86
+ }
87
+
83
88
  export function injectNonce(html: string, nonce: string): string {
84
89
  return html.replace(/<script\b(?![^>]*\bsrc\s*=)([^>]*)>/gi, (match) => {
85
90
  if (/nonce\s*=/i.test(match)) {
@@ -1,6 +1,6 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { resolve } from "node:path";
3
- import { statSync } from "node:fs";
3
+ import { readFileSync, statSync } from "node:fs";
4
4
  import React, { type ReactNode } from "react";
5
5
  import { renderToString } from "react-dom/server";
6
6
  import { compileMdx } from "./mdx";
@@ -14,7 +14,7 @@ import NotFoundPage from "../pages/404";
14
14
  import IndexPage from "../pages/index";
15
15
  import { DocsLayout } from "../components/DocsLayout";
16
16
  import { generateNonce, isPathSafe, isSlugSafe, htmlResponse, SECURITY_HEADERS } from "./security";
17
- import { htmlShell as createHtmlShell, hmrScript, errorHtml } from "./html";
17
+ import { htmlShell as createHtmlShell, hmrScript, errorHtml } from "./html.shared";
18
18
 
19
19
  export interface ServerState {
20
20
  docuConfig: DocuConfig;
@@ -249,7 +249,7 @@ export function serveStatic(pathname: string): Response | null {
249
249
  try {
250
250
  const s = statSync(assetPath);
251
251
  if (s.isFile()) {
252
- return new Response(Bun.file(assetPath), {
252
+ return new Response(readFileSync(assetPath), {
253
253
  headers: { "Content-Type": getContentType(pathname) },
254
254
  });
255
255
  }
@@ -266,7 +266,7 @@ export function serveStatic(pathname: string): Response | null {
266
266
  try {
267
267
  const s = statSync(docsAsset);
268
268
  if (s.isFile()) {
269
- return new Response(Bun.file(docsAsset), {
269
+ return new Response(readFileSync(docsAsset), {
270
270
  headers: { "Content-Type": getContentType(pathname) },
271
271
  });
272
272
  }
@@ -0,0 +1,4 @@
1
+ import { denoAdapter } from "@docubook/runt";
2
+ import { runServer } from "./server.impl";
3
+
4
+ await runServer(denoAdapter);
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Runtime-neutral dev server — mirror of `server.ts` (Bun-only, protected)
3
+ * driven by a `RuntimeAdapter` from `@docubook/runt` instead of `Bun.serve`,
4
+ * with manual route matching instead of `Bun.FileSystemRouter`. The page set
5
+ * is static (`/`, `/docs/[[...slug]]`, `/404`), so a router is unnecessary.
6
+ */
7
+
8
+ import { watch } from "node:fs";
9
+ import type { RuntimeAdapter, ServerHandle } from "@docubook/runt";
10
+ import { DOCS_DIR, loadDocuConfig } from "./paths";
11
+ import { loadPlugins } from "./plugin-loader";
12
+ import { BuildPluginBuilder } from "./plugin-builder";
13
+ import { buildClientBundle, computeInlineThemeCss } from "./hydrate.node";
14
+ import { generateSearchIndex } from "./search-indexer";
15
+ import { logger } from "./logger";
16
+ import { initSentry, captureException } from "./sentry";
17
+ import {
18
+ serveStatic,
19
+ handleDocsIndex,
20
+ handleDocsRoute,
21
+ handleIndex,
22
+ handleNotFound,
23
+ serverErrorResponse,
24
+ type ServerState,
25
+ } from "./server-routes";
26
+ import { wrapPluginResponse } from "./security";
27
+ import { stripDocsHtmlSuffix } from "./utils";
28
+
29
+ export async function runServer(adapter: RuntimeAdapter): Promise<ServerHandle> {
30
+ const docuConfig = loadDocuConfig();
31
+
32
+ const parsedPort = parseInt(process.env.PORT ?? "3000", 10);
33
+ const PORT =
34
+ Number.isInteger(parsedPort) && parsedPort > 0 && parsedPort <= 65535 ? parsedPort : 3000;
35
+
36
+ logger.buildStart();
37
+
38
+ await initSentry();
39
+
40
+ logger.bundleStart();
41
+ let t = performance.now();
42
+ const assetManifest = await buildClientBundle();
43
+ logger.bundleDone(Math.round(performance.now() - t));
44
+
45
+ const inlineThemeCss = computeInlineThemeCss();
46
+
47
+ logger.indexStart();
48
+ t = performance.now();
49
+ const records = await generateSearchIndex();
50
+ logger.indexDone(records, Math.round(performance.now() - t));
51
+
52
+ logger.routes();
53
+
54
+ // Plugin setup — all hooks active (onLoad, remark/rehype, frontmatter, head/body, html transform, handleRequest)
55
+ const pluginsConfig = docuConfig.plugins ?? [];
56
+ const builder = pluginsConfig.length > 0 ? new BuildPluginBuilder(docuConfig) : null;
57
+
58
+ if (builder) {
59
+ const plugins = await loadPlugins(pluginsConfig);
60
+
61
+ for (const plugin of plugins) {
62
+ await plugin.setup(builder);
63
+ }
64
+ }
65
+
66
+ const state: ServerState = {
67
+ docuConfig,
68
+ assetManifest,
69
+ inlineThemeCss,
70
+ builder,
71
+ };
72
+
73
+ const hmrClients = new Set<ReadableStreamDefaultController>();
74
+
75
+ let hmrTimeout: ReturnType<typeof setTimeout> | null = null;
76
+ const watcher = watch(DOCS_DIR, { recursive: true }, (_event, filename) => {
77
+ if (!filename || (!filename.endsWith(".mdx") && !filename.endsWith(".md"))) return;
78
+ if (hmrTimeout) clearTimeout(hmrTimeout);
79
+ hmrTimeout = setTimeout(() => {
80
+ for (const client of [...hmrClients]) {
81
+ try {
82
+ client.enqueue(new TextEncoder().encode("data: reload\n\n"));
83
+ } catch {
84
+ hmrClients.delete(client);
85
+ }
86
+ }
87
+ }, 300);
88
+ });
89
+
90
+ process.on("SIGINT", () => {
91
+ watcher.close();
92
+ process.exit(0);
93
+ });
94
+ process.on("SIGTERM", () => {
95
+ watcher.close();
96
+ process.exit(0);
97
+ });
98
+
99
+ let handle: ServerHandle | null = null;
100
+
101
+ const fetchHandler = async (req: Request): Promise<Response> => {
102
+ const url = new URL(req.url);
103
+ // Generated links carry `.html` (matching the static build output);
104
+ // route them to the same handler as their extensionless form.
105
+ const pathname = stripDocsHtmlSuffix(url.pathname);
106
+ const startTime = performance.now();
107
+
108
+ if (builder) {
109
+ const pluginResponse = await builder.runHandleRequest(req, {
110
+ port: handle?.port ?? PORT,
111
+ hostname: handle?.hostname ?? "localhost",
112
+ });
113
+ if (pluginResponse) {
114
+ const securedResponse = wrapPluginResponse(pluginResponse, true);
115
+ logger.request(
116
+ req.method,
117
+ pathname,
118
+ securedResponse.status,
119
+ Math.round(performance.now() - startTime)
120
+ );
121
+ return securedResponse;
122
+ }
123
+ }
124
+
125
+ try {
126
+ if (pathname === "/__hmr") {
127
+ const stream = new ReadableStream({
128
+ start(controller) {
129
+ hmrClients.add(controller);
130
+ controller.enqueue(new TextEncoder().encode("data: connected\n\n"));
131
+ },
132
+ cancel(controller) {
133
+ hmrClients.delete(controller);
134
+ },
135
+ });
136
+ return new Response(stream, {
137
+ headers: {
138
+ "Content-Type": "text/event-stream",
139
+ "Cache-Control": "no-cache",
140
+ Connection: "keep-alive",
141
+ },
142
+ });
143
+ }
144
+
145
+ if (pathname.startsWith("/assets/") || /\.\w+$/.test(pathname)) {
146
+ const staticRes = serveStatic(pathname);
147
+ if (staticRes) return staticRes;
148
+ }
149
+
150
+ let response: Response;
151
+
152
+ // Manual route matching — same routes Bun.FileSystemRouter derives
153
+ // from `.docu/pages/` ("/", "/docs/[[...slug]]", "/404").
154
+ if (pathname === "/") {
155
+ response = handleIndex(state);
156
+ } else if (pathname === "/docs" || pathname === "/docs/") {
157
+ response = await handleDocsIndex(state);
158
+ } else if (pathname.startsWith("/docs/")) {
159
+ const slug = pathname.slice("/docs/".length).split("/").filter(Boolean);
160
+ response =
161
+ slug.length === 0 ? await handleDocsIndex(state) : await handleDocsRoute(slug, state);
162
+ } else {
163
+ response = handleNotFound(state);
164
+ }
165
+
166
+ logger.request(
167
+ req.method,
168
+ pathname,
169
+ response.status,
170
+ Math.round(performance.now() - startTime)
171
+ );
172
+ return response;
173
+ } catch (err) {
174
+ captureException(err, { method: req.method, pathname });
175
+ logger.request(req.method, pathname, 500, Math.round(performance.now() - startTime));
176
+ return serverErrorResponse(err);
177
+ }
178
+ };
179
+
180
+ handle = await adapter.serve(fetchHandler, { port: PORT, idleTimeout: 255 });
181
+
182
+ logger.ready(handle.port, true);
183
+ return handle;
184
+ }
@@ -0,0 +1,4 @@
1
+ import { nodeAdapter } from "@docubook/runt";
2
+ import { runServer } from "./server.impl";
3
+
4
+ await runServer(nodeAdapter);
@@ -1,10 +1,25 @@
1
- @import "daisyui/daisyui.css";
2
- @import "@docubook/mdx-content/styles.css";
3
1
  @import "tailwindcss";
2
+ @import "@docubook/mdx-content/styles.css";
4
3
 
4
+ @plugin "daisyui" {
5
+ themes:
6
+ light --default,
7
+ dark;
8
+ }
5
9
  @plugin "@tailwindcss/typography";
6
10
  @source "../../.docu/components";
7
11
  @source "../../.docu/pages";
12
+ @source inline("breadcrumbs collapse collapse-open collapse-close collapse-arrow collapse-plus collapse-title collapse-content");
13
+ @source inline("modal modal-top modal-middle modal-bottom modal-box modal-backdrop");
14
+ @source inline("drawer drawer-end drawer-toggle drawer-side drawer-overlay drawer-content");
15
+ @source inline("sm:drawer-open md:drawer-open lg:drawer-open xl:drawer-open");
16
+ @source inline("navbar menu menu-horizontal");
17
+ @source inline("kbd kbd-xs kbd-sm kbd-md kbd-lg kbd-xl");
18
+ @source inline("toggle theme-controller toggle-xs toggle-sm toggle-md toggle-lg");
19
+ @source inline("toggle-primary toggle-secondary toggle-accent toggle-neutral toggle-success toggle-warning toggle-info toggle-error");
20
+ @source inline("input input-ghost input-xs input-sm input-md input-lg input-xl");
21
+ @source inline("input-primary input-secondary input-accent input-neutral input-success input-warning input-info input-error");
22
+ @source inline("label");
8
23
 
9
24
  @custom-variant dark (&:is(.dark *));
10
25
 
@@ -13,7 +28,7 @@
13
28
  --color-base-200: oklch(var(--base-200, 100% 0 0));
14
29
  --color-base-300: oklch(var(--base-300, 100% 0 0));
15
30
  --color-base-content: oklch(var(--base-content, 100% 0 0));
16
- --color-border: hsl(var(--border));
31
+ --color-border: hsl(var(--border-color));
17
32
  --color-input: hsl(var(--input));
18
33
  --color-ring: hsl(var(--ring));
19
34
  --color-background: hsl(var(--background));
@@ -80,7 +95,7 @@
80
95
  --accent-foreground: 0 0% 100%;
81
96
  --destructive: 0 85% 60%;
82
97
  --destructive-foreground: 0 0% 100%;
83
- --border: 210 20% 85%;
98
+ --border-color: 210 20% 85%;
84
99
  --input: 210 20% 85%;
85
100
  --ring: 210 81% 56%;
86
101
  --radius: 0.5rem;
@@ -107,7 +122,7 @@
107
122
  --accent-foreground: 0 0% 100%;
108
123
  --destructive: 0 80% 65%;
109
124
  --destructive-foreground: 0 0% 100%;
110
- --border: 220 10% 28%;
125
+ --border-color: 220 10% 28%;
111
126
  --input: 220 10% 28%;
112
127
  --ring: 210 100% 67%;
113
128
  --radius: 0.5rem;