@docubook/flame 1.5.4 → 1.6.1

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.
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  runPreview
3
- } from "./chunk-XLH7Z4QO.js";
3
+ } from "./chunk-P3ZT4S3D.js";
4
4
  import "./chunk-EOK6KATZ.js";
5
- import "./chunk-GXQB4ETN.js";
6
- import "./chunk-J5NMYSBJ.js";
5
+ import "./chunk-2VUUBIN3.js";
6
+ import "./chunk-4IQXHHPF.js";
7
7
 
8
8
  // .docu/node/preview.node.ts
9
9
  import { nodeAdapter } from "@docubook/runt";
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  runServer
3
- } from "./chunk-BDWN6AHQ.js";
4
- import "./chunk-C67LPMQA.js";
3
+ } from "./chunk-GC62TPBC.js";
4
+ import "./chunk-IJFMQFWW.js";
5
5
  import "./chunk-EOK6KATZ.js";
6
- import "./chunk-GXQB4ETN.js";
7
- import "./chunk-J5NMYSBJ.js";
6
+ import "./chunk-2VUUBIN3.js";
7
+ import "./chunk-4IQXHHPF.js";
8
8
 
9
9
  // .docu/node/server.deno.ts
10
10
  import { denoAdapter } from "@docubook/runt";
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  runServer
3
- } from "./chunk-BDWN6AHQ.js";
4
- import "./chunk-C67LPMQA.js";
3
+ } from "./chunk-GC62TPBC.js";
4
+ import "./chunk-IJFMQFWW.js";
5
5
  import "./chunk-EOK6KATZ.js";
6
- import "./chunk-GXQB4ETN.js";
7
- import "./chunk-J5NMYSBJ.js";
6
+ import "./chunk-2VUUBIN3.js";
7
+ import "./chunk-4IQXHHPF.js";
8
8
 
9
9
  // .docu/node/server.node.ts
10
10
  import { nodeAdapter } from "@docubook/runt";
@@ -31,7 +31,7 @@ import { loadPlugins } from "./plugin-loader";
31
31
  import { BuildPluginBuilder } from "./plugin-builder";
32
32
  import { scanMdxFiles } from "./utils";
33
33
  import type { BuildCache, CliArgs } from "./types";
34
- import { generateNonce } from "./security";
34
+ import { generateNonce, cspHeader } from "./security";
35
35
  import type { PageMeta, PageContext } from "./plugin";
36
36
  import { buildSeoMeta } from "./seo";
37
37
  import DocsPage from "../pages/docs/[[...slug]]";
@@ -149,12 +149,14 @@ async function renderDocsPage(
149
149
  const depth = slug ? slug.split("/").length : 1;
150
150
  const favicon = docuConfig.meta?.favicon || "/docs/assets/images/favicon.ico";
151
151
  const seo = buildSeoMeta(docuConfig, frontmatter, slug || "");
152
+ const csp = cspHeader(nonce, process.env.NODE_ENV !== "production");
152
153
  let html = htmlShell({
153
154
  title,
154
155
  description,
155
156
  body,
156
157
  favicon,
157
158
  seo,
159
+ csp,
158
160
  css: assetManifest.css,
159
161
  js: assetManifest.js,
160
162
  nonce,
@@ -349,15 +351,17 @@ export async function runBuild(): Promise<void> {
349
351
  const landingPage = React.createElement(IndexPage);
350
352
  const landingFavicon = docuConfig.meta?.favicon || "/docs/assets/images/favicon.ico";
351
353
  const landingSeo = buildSeoMeta(docuConfig, docuConfig.meta as Record<string, unknown>, "");
354
+ const landingNonce = generateNonce();
352
355
  const landingHtml = htmlShell({
353
356
  title: docuConfig.meta?.title || "DocuBook",
354
357
  description: docuConfig.meta?.description || "",
355
358
  body: renderToString(landingPage),
356
359
  favicon: landingFavicon,
357
360
  seo: landingSeo,
361
+ csp: cspHeader(landingNonce, process.env.NODE_ENV !== "production"),
358
362
  css: assetManifest.css,
359
363
  js: assetManifest.js,
360
- nonce: generateNonce(),
364
+ nonce: landingNonce,
361
365
  themeCss: inlineThemeCss,
362
366
  });
363
367
  await writeFile(join(DIST_DIR, "index.html"), landingHtml);
@@ -368,15 +372,17 @@ export async function runBuild(): Promise<void> {
368
372
  React.createElement(NotFoundPage)
369
373
  );
370
374
  const notFoundFavicon = docuConfig.meta?.favicon || "/docs/assets/images/favicon.ico";
375
+ const notFoundNonce = generateNonce();
371
376
  const notFoundHtml = htmlShell({
372
377
  title: "404 - Not Found",
373
378
  description: "",
374
379
  body: renderToString(notFoundPage),
375
380
  favicon: notFoundFavicon,
376
381
  headExtra: ['<meta name="robots" content="noindex,follow">'],
382
+ csp: cspHeader(notFoundNonce, process.env.NODE_ENV !== "production"),
377
383
  css: assetManifest.css,
378
384
  js: assetManifest.js,
379
- nonce: generateNonce(),
385
+ nonce: notFoundNonce,
380
386
  themeCss: inlineThemeCss,
381
387
  });
382
388
  await writeFile(join(DIST_DIR, "404.html"), notFoundHtml);
@@ -25,6 +25,8 @@ function mountIsland(
25
25
  }
26
26
 
27
27
  function mountIslands() {
28
+ // forceCreate: SSR sidebar renders <Menu> only; client renders full <Sidebar>
29
+ // (DesktopSidebar + MobileBar) — structure mismatch forces full createRoot.
28
30
  mountIsland(
29
31
  "sidebar-island",
30
32
  (el) => {
@@ -38,18 +40,16 @@ function mountIslands() {
38
40
  true
39
41
  );
40
42
 
41
- mountIsland(
42
- "mobile-bar-island",
43
- (el) => {
44
- const tocs: TocItem[] = safeParseTocs(el.dataset.tocs);
45
- return React.createElement(MobileBar, {
46
- tocs,
47
- title: el.dataset.title || "",
48
- repoUrl: el.dataset.repo || "",
49
- });
50
- },
51
- true
52
- );
43
+ // mobile-bar-island SSR div is empty (data attributes only),
44
+ // so hydrateRoot child check falls through to createRoot automatically.
45
+ mountIsland("mobile-bar-island", (el) => {
46
+ const tocs: TocItem[] = safeParseTocs(el.dataset.tocs);
47
+ return React.createElement(MobileBar, {
48
+ tocs,
49
+ title: el.dataset.title || "",
50
+ repoUrl: el.dataset.repo || "",
51
+ });
52
+ });
53
53
 
54
54
  mountIsland("toc-island", (el) => {
55
55
  const tocs: TocItem[] = safeParseTocs(el.dataset.tocs);
@@ -69,7 +69,8 @@ function hydrateMdxContent() {
69
69
  try {
70
70
  const compiledSource = JSON.parse(sourceEl.textContent || "");
71
71
  const components = createMdxComponents();
72
- createRoot(island).render(
72
+ hydrateRoot(
73
+ island,
73
74
  React.createElement(MDXRemote, { compiledSource, scope: {}, frontmatter: {}, components })
74
75
  );
75
76
  } catch (e) {
@@ -82,3 +83,27 @@ if (document.readyState === "loading") {
82
83
  } else {
83
84
  mountIslands();
84
85
  }
86
+
87
+ // ── Hash scroll compensation ──────────────────────────────────────
88
+ // After hydration, lazy-rendered content (Mermaid via IntersectionObserver) can
89
+ // shift layout and push the hash target (#section-2) off-screen.
90
+ // Poll with rAF for ~1s and re-scroll if the target is below viewport.
91
+ function scrollToHashOnLoad() {
92
+ const hash = window.location.hash;
93
+ if (!hash || hash === "#") return;
94
+ const id = hash.slice(1);
95
+ const deadline = performance.now() + 1000;
96
+ function tick() {
97
+ const el = document.getElementById(id);
98
+ if (el) {
99
+ const top = el.getBoundingClientRect().top;
100
+ if (top <= window.innerHeight - 100 && top >= 0) return; // already in view
101
+ el.scrollIntoView();
102
+ return; // scrolled once, done
103
+ }
104
+ if (performance.now() < deadline) requestAnimationFrame(tick);
105
+ }
106
+ requestAnimationFrame(tick);
107
+ }
108
+
109
+ scrollToHashOnLoad();
@@ -1,7 +1,11 @@
1
1
  /**
2
- * Runtime-neutral deploy — mirror of `deploy.ts` (Bun-only, protected) for
3
- * Node.js and Deno. Instead of spawning `bun run build`, it runs the neutral
4
- * build in-process, then prepares `.docu/dist` for GitHub Pages.
2
+ * Runtime-neutral deploy — 3 modes:
3
+ * 1. `flame deploy` → build + generate GitHub Actions workflow
4
+ * 2. `flame deploy --docker` → build + generate Docker deployment files
5
+ * 3. `flame deploy --docker --silent` → same as #2, minimal output
6
+ *
7
+ * Mirror of deploy.ts (Bun-only, protected) for Node.js and Deno.
8
+ * Runs the neutral build in-process, then prepares .docu/dist.
5
9
  */
6
10
 
7
11
  import { writeFile, mkdir } from "node:fs/promises";
@@ -12,74 +16,291 @@ import { DIST_DIR, PROJECT_ROOT } from "./paths";
12
16
  const WORKFLOW_DIR = join(PROJECT_ROOT, ".github/workflows");
13
17
  const WORKFLOW_FILE = join(WORKFLOW_DIR, "deploy.yml");
14
18
 
15
- export async function runDeploy(): Promise<void> {
16
- console.log("📦 Building for production...\n");
19
+ export const NGINX_CONF = `server {
20
+ listen 80;
21
+ server_name _;
22
+ root /usr/share/nginx/html;
23
+ index index.html;
24
+
25
+ gzip on;
26
+ gzip_types text/html text/css application/javascript image/svg+xml;
27
+
28
+ # Security headers (HSTS effective when HTTPS is terminated upstream)
29
+ add_header X-Frame-Options "DENY" always;
30
+ add_header X-Content-Type-Options "nosniff" always;
31
+ add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
32
+ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
33
+ add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
34
+
35
+ location /assets/ {
36
+ expires 1y;
37
+ add_header Cache-Control "public, immutable";
38
+ add_header X-Frame-Options "DENY" always;
39
+ add_header X-Content-Type-Options "nosniff" always;
40
+ add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
41
+ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
42
+ add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
43
+ }
44
+
45
+ location /docs/assets/ {
46
+ expires 7d;
47
+ add_header Cache-Control "public";
48
+ add_header X-Frame-Options "DENY" always;
49
+ add_header X-Content-Type-Options "nosniff" always;
50
+ add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
51
+ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
52
+ add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
53
+ }
54
+
55
+ location / {
56
+ try_files $uri $uri.html $uri/ =404;
57
+ }
58
+ }
59
+ `;
60
+
61
+ export const DOCKERIGNORE = `node_modules
62
+ .git
63
+ *.DS_Store
64
+ .docu/dist
65
+ .docu/lib
66
+ .env
67
+ .env.*
68
+ .npmrc
69
+ *.log
70
+ `;
17
71
 
72
+ export function detectPkgManager(dir: string): {
73
+ baseImage: string;
74
+ lockFile: string;
75
+ installCmd: string;
76
+ runCmd: string;
77
+ cache: string;
78
+ setupAction: string;
79
+ } {
80
+ const bunLockFile = existsSync(join(dir, "bun.lock"))
81
+ ? "bun.lock"
82
+ : existsSync(join(dir, "bun.lockb"))
83
+ ? "bun.lockb"
84
+ : null;
85
+ if (bunLockFile) {
86
+ return {
87
+ baseImage: "oven/bun:1",
88
+ lockFile: bunLockFile,
89
+ installCmd: "bun install --frozen-lockfile",
90
+ runCmd: "bun",
91
+ cache: "",
92
+ setupAction: "bun",
93
+ };
94
+ }
95
+ if (existsSync(join(dir, "pnpm-lock.yaml"))) {
96
+ return {
97
+ baseImage: "node:22-alpine",
98
+ lockFile: "pnpm-lock.yaml",
99
+ installCmd: "corepack enable && pnpm install --frozen-lockfile",
100
+ runCmd: "pnpm",
101
+ cache: "pnpm",
102
+ setupAction: "pnpm",
103
+ };
104
+ }
105
+ if (existsSync(join(dir, "yarn.lock"))) {
106
+ return {
107
+ baseImage: "node:22-alpine",
108
+ lockFile: "yarn.lock",
109
+ installCmd: "yarn install --frozen-lockfile",
110
+ runCmd: "yarn",
111
+ cache: "yarn",
112
+ setupAction: "node",
113
+ };
114
+ }
115
+ // default: npm
116
+ return {
117
+ baseImage: "node:22-alpine",
118
+ lockFile: "package-lock.json",
119
+ installCmd: "npm ci",
120
+ runCmd: "npm",
121
+ cache: "npm",
122
+ setupAction: "node",
123
+ };
124
+ }
125
+
126
+ export const HEADERS_FILE = `/*
127
+ X-Frame-Options: DENY
128
+ X-Content-Type-Options: nosniff
129
+ Referrer-Policy: strict-origin-when-cross-origin
130
+
131
+ /assets/*
132
+ Cache-Control: public, max-age=31536000, immutable
133
+ `;
134
+
135
+ const isDocker = !!process.env.FLAME_DEPLOY_DOCKER;
136
+ const isSilent = !!process.env.FLAME_DEPLOY_SILENT;
137
+
138
+ /** Logger that no-ops all non-error output in silent mode. */
139
+ const log = isSilent
140
+ ? { info: () => {}, ok: () => {}, created: () => {}, out: () => {} }
141
+ : {
142
+ info: (m: string) => console.log(m),
143
+ ok: () => console.log("\n✅ Ready to deploy!"),
144
+ created: (m: string) => console.log(m),
145
+ out: (m: string) => console.log(m),
146
+ };
147
+
148
+ async function runBuild() {
18
149
  process.env.NODE_ENV = "production";
150
+ if (isSilent) {
151
+ process.env.FLAME_BUILD_SILENT = "1";
152
+ process.env.LOG_LEVEL = "error";
153
+ }
19
154
  const { runBuildCli } = await import("./build.impl");
20
155
  await runBuildCli();
156
+ }
21
157
 
22
- // Add .nojekyll
23
- await writeFile(join(DIST_DIR, ".nojekyll"), "");
158
+ async function writeDockerFiles() {
159
+ const dockerDir = PROJECT_ROOT;
160
+ const pm = detectPkgManager(dockerDir);
161
+
162
+ if (!existsSync(join(dockerDir, "Dockerfile"))) {
163
+ await writeFile(
164
+ join(dockerDir, "Dockerfile"),
165
+ `FROM ${pm.baseImage} AS builder
166
+ WORKDIR /app
167
+ COPY package.json ${pm.lockFile} ./
168
+ RUN ${pm.installCmd}
169
+ COPY . .
170
+ RUN ${pm.runCmd} run build
171
+
172
+ FROM nginx:alpine
173
+ COPY --from=builder /app/.docu/dist /usr/share/nginx/html
174
+ COPY nginx.conf /etc/nginx/conf.d/default.conf
175
+ USER nginx
176
+ EXPOSE 80
177
+ CMD ["nginx", "-g", "daemon off;"]
178
+ `
179
+ );
180
+ log.created("📄 Created Dockerfile");
181
+ }
182
+
183
+ if (!existsSync(join(dockerDir, "nginx.conf"))) {
184
+ await writeFile(join(dockerDir, "nginx.conf"), NGINX_CONF);
185
+ log.created("📄 Created nginx.conf");
186
+ }
187
+
188
+ if (!existsSync(join(dockerDir, ".dockerignore"))) {
189
+ await writeFile(join(dockerDir, ".dockerignore"), DOCKERIGNORE);
190
+ log.created("📄 Created .dockerignore");
191
+ }
192
+ }
193
+
194
+ function generateWorkflowYml(): string {
195
+ const pm = detectPkgManager(PROJECT_ROOT);
196
+
197
+ const setupSteps: string[] = [
198
+ " - uses: actions/checkout@v7",
199
+ " with:",
200
+ " fetch-depth: 0",
201
+ "",
202
+ ];
203
+
204
+ if (pm.setupAction === "bun") {
205
+ setupSteps.push(
206
+ " - uses: oven-sh/setup-bun@v2",
207
+ " with:",
208
+ " bun-version: latest",
209
+ ""
210
+ );
211
+ } else if (pm.setupAction === "pnpm") {
212
+ setupSteps.push(
213
+ " - uses: pnpm/action-setup@v6",
214
+ "",
215
+ " - uses: actions/setup-node@v6",
216
+ " with:",
217
+ " node-version: 22",
218
+ ` cache: ${pm.cache}`,
219
+ ""
220
+ );
221
+ } else {
222
+ // npm / yarn
223
+ setupSteps.push(
224
+ " - uses: actions/setup-node@v6",
225
+ " with:",
226
+ " node-version: 22",
227
+ ` cache: ${pm.cache}`,
228
+ ""
229
+ );
230
+ }
24
231
 
25
- // Generate GitHub Actions workflow
232
+ return [
233
+ `name: Deploy to GitHub Pages`,
234
+ "",
235
+ "on:",
236
+ " push:",
237
+ " branches: [main]",
238
+ " workflow_dispatch:",
239
+ "",
240
+ "permissions:",
241
+ " contents: read",
242
+ " pages: write",
243
+ " id-token: write",
244
+ "",
245
+ "concurrency:",
246
+ ' group: "pages"',
247
+ " cancel-in-progress: false",
248
+ "",
249
+ "jobs:",
250
+ " build:",
251
+ " runs-on: ubuntu-latest",
252
+ " steps:",
253
+ ...setupSteps,
254
+ ` - run: ${pm.installCmd}`,
255
+ "",
256
+ ` - run: ${pm.runCmd} run build`,
257
+ "",
258
+ " - name: Add .nojekyll",
259
+ " run: touch .docu/dist/.nojekyll",
260
+ "",
261
+ " - uses: actions/upload-pages-artifact@v3",
262
+ " with:",
263
+ " path: .docu/dist",
264
+ "",
265
+ " deploy:",
266
+ " environment:",
267
+ " name: github-pages",
268
+ " url: ${{ steps.deployment.outputs.page_url }}",
269
+ " runs-on: ubuntu-latest",
270
+ " needs: build",
271
+ " steps:",
272
+ " - id: deployment",
273
+ " uses: actions/deploy-pages@v4",
274
+ ].join("\n");
275
+ }
276
+
277
+ async function writeGhaWorkflow() {
26
278
  if (!existsSync(WORKFLOW_FILE)) {
27
279
  await mkdir(WORKFLOW_DIR, { recursive: true });
28
- await writeFile(WORKFLOW_FILE, GITHUB_ACTIONS_WORKFLOW);
29
- console.log("\n📄 Created .github/workflows/deploy.yml");
280
+ await writeFile(WORKFLOW_FILE, generateWorkflowYml());
281
+ log.created("📄 Created .github/workflows/deploy.yml");
30
282
  }
31
-
32
- console.log("\n✅ Ready to deploy!");
33
- console.log(" Output: .docu/dist/");
34
- console.log(" Push to GitHub and enable Pages (Settings → Pages → Source: GitHub Actions)");
35
283
  }
36
284
 
37
- const GITHUB_ACTIONS_WORKFLOW = `name: Deploy to GitHub Pages
38
-
39
- on:
40
- push:
41
- branches: [main]
42
- workflow_dispatch:
43
-
44
- permissions:
45
- contents: read
46
- pages: write
47
- id-token: write
48
-
49
- concurrency:
50
- group: "pages"
51
- cancel-in-progress: false
52
-
53
- jobs:
54
- build:
55
- runs-on: ubuntu-latest
56
- steps:
57
- - uses: actions/checkout@v4
58
- with:
59
- fetch-depth: 0
60
-
61
- - uses: actions/setup-node@v4
62
- with:
63
- node-version: 22
64
-
65
- - run: npm install
66
-
67
- - run: npm run build
68
-
69
- - name: Add .nojekyll
70
- run: touch .docu/dist/.nojekyll
71
-
72
- - uses: actions/upload-pages-artifact@v3
73
- with:
74
- path: .docu/dist
75
-
76
- deploy:
77
- environment:
78
- name: github-pages
79
- url: \${{ steps.deployment.outputs.page_url }}
80
- runs-on: ubuntu-latest
81
- needs: build
82
- steps:
83
- - id: deployment
84
- uses: actions/deploy-pages@v4
85
- `;
285
+ export async function runDeploy(): Promise<void> {
286
+ log.info("📦 Building for production...\n");
287
+ await runBuild();
288
+
289
+ // Common: .nojekyll + _headers
290
+ await writeFile(join(DIST_DIR, ".nojekyll"), "");
291
+ await writeFile(join(DIST_DIR, "_headers"), HEADERS_FILE);
292
+
293
+ if (isDocker) {
294
+ await writeDockerFiles();
295
+ } else {
296
+ await writeGhaWorkflow();
297
+ }
298
+
299
+ log.ok();
300
+ log.out(" Output: .docu/dist/");
301
+ if (isDocker) {
302
+ log.out(" Run: docker build -t my-docs . && docker run -p 80:80 my-docs");
303
+ } else {
304
+ log.out(" Push to GitHub and enable Pages (Settings → Pages → Source: GitHub Actions)");
305
+ }
306
+ }