@docubook/flame 2.0.0-beta.5 → 2.0.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.
Files changed (58) hide show
  1. package/.docu/components/Theme.tsx +42 -34
  2. package/.docu/components/home/BackgroundBlobs.tsx +31 -0
  3. package/.docu/components/home/index.tsx +1 -0
  4. package/.docu/lib/build.deno.js +1 -1
  5. package/.docu/lib/build.impl--Pm9uO30.js +2 -0
  6. package/.docu/lib/{build.impl-BBlR3si3.js → build.impl-CsP6pPug.js} +60 -29
  7. package/.docu/lib/build.impl-CsP6pPug.js.map +1 -0
  8. package/.docu/lib/build.node.js +1 -1
  9. package/.docu/lib/clean.js +1 -1
  10. package/.docu/lib/deploy.deno.js +1 -1
  11. package/.docu/lib/deploy.node.js +1 -1
  12. package/.docu/lib/{deploy.shared-Dxv3lsOb.js → deploy.shared-CsEa3L1D.js} +45 -17
  13. package/.docu/lib/deploy.shared-CsEa3L1D.js.map +1 -0
  14. package/.docu/lib/{html.shared-DBMwv_Z6.js → html.shared-CsEFmy_Y.js} +181 -158
  15. package/.docu/lib/html.shared-CsEFmy_Y.js.map +1 -0
  16. package/.docu/lib/{logger-DYmvFF1d.js → logger-DLU3A-z2.js} +4 -3
  17. package/.docu/lib/logger-DLU3A-z2.js.map +1 -0
  18. package/.docu/lib/{paths-Bl2cdp9E.js → paths-Cbz-dj4s.js} +5 -9
  19. package/.docu/lib/{paths-Bl2cdp9E.js.map → paths-Cbz-dj4s.js.map} +1 -1
  20. package/.docu/lib/preview.deno.js +1 -1
  21. package/.docu/lib/{preview.impl-anVcZv2Z.js → preview.impl-CaZTgmnB.js} +3 -3
  22. package/.docu/lib/{preview.impl-anVcZv2Z.js.map → preview.impl-CaZTgmnB.js.map} +1 -1
  23. package/.docu/lib/preview.node.js +1 -1
  24. package/.docu/lib/server.deno.js +1 -1
  25. package/.docu/lib/{server.impl-Cvoa6Diq.js → server.impl-D-CgD1bn.js} +43 -52
  26. package/.docu/lib/server.impl-D-CgD1bn.js.map +1 -0
  27. package/.docu/lib/server.node.js +1 -1
  28. package/.docu/node/build.impl.ts +71 -34
  29. package/.docu/node/build.ts +68 -34
  30. package/.docu/node/cache-key.ts +8 -4
  31. package/.docu/node/client.ts +36 -27
  32. package/.docu/node/deploy.shared.ts +44 -18
  33. package/.docu/node/deploy.ts +5 -20
  34. package/.docu/node/home-client.ts +6 -0
  35. package/.docu/node/html.shared.ts +5 -4
  36. package/.docu/node/html.ts +4 -3
  37. package/.docu/node/hydrate.node.ts +87 -115
  38. package/.docu/node/hydrate.ts +74 -80
  39. package/.docu/node/mdx-manifest.d.ts +2 -2
  40. package/.docu/node/paths.ts +2 -9
  41. package/.docu/node/plugin-builder.ts +25 -13
  42. package/.docu/node/plugin.ts +33 -9
  43. package/.docu/node/server-routes.ts +59 -50
  44. package/.docu/node/server.impl.ts +4 -2
  45. package/.docu/node/server.ts +12 -16
  46. package/.docu/node/types.ts +11 -0
  47. package/.docu/pages/404.tsx +12 -9
  48. package/.docu/pages/index.tsx +8 -29
  49. package/.docu/styles/base.css +128 -0
  50. package/.docu/styles/globals.css +5 -137
  51. package/.docu/styles/site.css +7 -0
  52. package/package.json +6 -5
  53. package/.docu/lib/build.impl-BBlR3si3.js.map +0 -1
  54. package/.docu/lib/build.impl-DpoDUmei.js +0 -2
  55. package/.docu/lib/deploy.shared-Dxv3lsOb.js.map +0 -1
  56. package/.docu/lib/html.shared-DBMwv_Z6.js.map +0 -1
  57. package/.docu/lib/logger-DYmvFF1d.js.map +0 -1
  58. package/.docu/lib/server.impl-Cvoa6Diq.js.map +0 -1
@@ -21,6 +21,7 @@ import {
21
21
  CACHE_FILE,
22
22
  DOCS_ASSETS_DIR,
23
23
  PROJECT_ROOT,
24
+ PAGES_DIR,
24
25
  loadDocuConfig,
25
26
  } from "./paths";
26
27
  import { htmlShell } from "./html";
@@ -31,7 +32,7 @@ import { initSentry, captureException } from "./sentry";
31
32
  import { loadPlugins } from "./plugin-loader";
32
33
  import { BuildPluginBuilder } from "./plugin-builder";
33
34
  import { scanMdxFiles, resolveDocsIndexSource, DEFAULT_FAVICON } from "./utils";
34
- import type { BuildCache, BuildCacheMeta, CliArgs } from "./types";
35
+ import type { AssetManifest, BuildCache, BuildCacheMeta, CliArgs } from "./types";
35
36
  import { isCacheEntry } from "./types";
36
37
  import {
37
38
  BUILD_CACHE_VERSION,
@@ -117,7 +118,11 @@ export function shouldRebuild(path: string, mtime: number, cache: BuildCache): R
117
118
  return "no";
118
119
  }
119
120
 
120
- let assetManifest = { js: "client.js", css: "client.css" };
121
+ let assetManifest: AssetManifest = {
122
+ docs: { js: "client.js", css: "docs.css" },
123
+ home: { js: "home-client.js", css: "site.css" },
124
+ notFound: { css: "site.css" },
125
+ };
121
126
 
122
127
  /**
123
128
  * Reuse manifest.json on bundle cache hit; fall back to a full rebuild
@@ -126,15 +131,20 @@ let assetManifest = { js: "client.js", css: "client.css" };
126
131
  async function resolveAssetManifest(
127
132
  bundleHit: boolean,
128
133
  mdxSources: Record<string, string>
129
- ): Promise<{ js: string; css: string }> {
134
+ ): Promise<AssetManifest> {
130
135
  if (!bundleHit) return buildClientBundle(mdxSources);
131
136
  try {
132
- const manifest = JSON.parse(await readFile(join(ASSETS_DIR, "manifest.json"), "utf-8")) as {
133
- js?: string;
134
- css?: string;
135
- };
136
- if (typeof manifest.js === "string" && typeof manifest.css === "string") {
137
- return { js: manifest.js, css: manifest.css };
137
+ const manifest = JSON.parse(
138
+ await readFile(join(ASSETS_DIR, "manifest.json"), "utf-8")
139
+ ) as Partial<AssetManifest>;
140
+ if (
141
+ typeof manifest.docs?.js === "string" &&
142
+ typeof manifest.docs.css === "string" &&
143
+ typeof manifest.home?.js === "string" &&
144
+ typeof manifest.home.css === "string" &&
145
+ typeof manifest.notFound?.css === "string"
146
+ ) {
147
+ return manifest as AssetManifest;
138
148
  }
139
149
  } catch {
140
150
  // corrupt/missing manifest — rebuild below
@@ -221,7 +231,15 @@ async function renderDocsPage(
221
231
 
222
232
  const body = renderToString(page);
223
233
 
224
- const ctx: PageContext = { slug, filePath, frontmatter, content, config: docuConfig };
234
+ const ctx: PageContext = {
235
+ pageType: "docs",
236
+ assets: assetManifest.docs,
237
+ slug,
238
+ filePath,
239
+ frontmatter,
240
+ content,
241
+ config: docuConfig,
242
+ };
225
243
  const headExtra = builder?.collectHead(ctx);
226
244
  const bodyExtra = builder?.collectBody(ctx);
227
245
 
@@ -238,8 +256,8 @@ async function renderDocsPage(
238
256
  favicon,
239
257
  seo,
240
258
  csp,
241
- css: assetManifest.css,
242
- js: assetManifest.js,
259
+ css: ctx.assets.css,
260
+ js: ctx.assets.js,
243
261
  nonce,
244
262
  themeCss: inlineThemeCss,
245
263
  depth,
@@ -380,14 +398,10 @@ async function build() {
380
398
  inlineThemeCss = computeInlineThemeCss();
381
399
 
382
400
  const lastManifest = cache["__assets__"];
383
- const assetsChanged =
384
- !isCacheEntry(lastManifest) || lastManifest.hash !== `${assetManifest.js}:${assetManifest.css}`;
401
+ const assetHash = JSON.stringify(assetManifest);
402
+ const assetsChanged = !isCacheEntry(lastManifest) || lastManifest.hash !== assetHash;
385
403
  if (assetsChanged) {
386
- cache["__assets__"] = {
387
- hash: `${assetManifest.js}:${assetManifest.css}`,
388
- mtime: 0,
389
- builtAt: Date.now(),
390
- };
404
+ cache["__assets__"] = { hash: assetHash, mtime: 0, builtAt: Date.now() };
391
405
  }
392
406
  if (!bundleHit) {
393
407
  cache["__bundle__"] = { hash: bundleHash, mtime: 0, builtAt: Date.now() };
@@ -410,7 +424,7 @@ async function build() {
410
424
  for (const file of mdxFiles) {
411
425
  const rebuildDecision = shouldRebuild(file.path, file.mtime, cache);
412
426
 
413
- if (rebuildDecision === "no") {
427
+ if (rebuildDecision === "no" && !builder) {
414
428
  const outputPath = join(DIST_DIR, "docs", `${file.path}.html`);
415
429
  if (existsSync(outputPath) && !assetsChanged) {
416
430
  skipped++;
@@ -428,7 +442,7 @@ async function build() {
428
442
  }
429
443
  }
430
444
 
431
- if (rebuildDecision === "hash_check") {
445
+ if (rebuildDecision === "hash_check" && !builder) {
432
446
  const contentHash = hashContent(rawMdx);
433
447
  const cached = cache[file.path];
434
448
  if (isCacheEntry(cached) && cached.hash === contentHash) {
@@ -512,42 +526,62 @@ async function build() {
512
526
  ""
513
527
  );
514
528
  const landingNonce = generateNonce();
515
- const landingHtml = htmlShell({
529
+ const landingContext: PageContext = {
530
+ pageType: "home",
531
+ assets: assetManifest.home,
532
+ slug: "",
533
+ filePath: join(PAGES_DIR, "index.tsx"),
534
+ frontmatter: { ...docuConfig.meta } as unknown as Record<string, unknown>,
535
+ config: docuConfig,
536
+ };
537
+ let landingHtml = htmlShell({
516
538
  title: docuConfig.meta?.title || "DocuBook",
517
539
  description: docuConfig.meta?.description || "",
518
540
  body: renderToString(landingPage),
519
541
  favicon: landingFavicon,
520
542
  seo: landingSeo,
521
543
  csp: cspHeader(landingNonce),
522
- css: assetManifest.css,
523
- js: assetManifest.js,
544
+ css: landingContext.assets.css,
545
+ js: landingContext.assets.js,
524
546
  nonce: landingNonce,
525
547
  themeCss: inlineThemeCss,
548
+ headExtra: builder?.collectHead(landingContext),
549
+ bodyExtra: builder?.collectBody(landingContext),
526
550
  });
551
+ if (builder) landingHtml = await builder.runTransformHtmlChain(landingHtml, landingContext);
527
552
  await writeFile(join(DIST_DIR, "index.html"), landingHtml);
528
553
 
529
- const notFoundPage = React.createElement(
530
- DocsLayout,
531
- { repoUrl: docuConfig.repo?.url },
532
- React.createElement(NotFoundPage)
533
- );
554
+ const notFoundPage = React.createElement(NotFoundPage);
534
555
  const notFoundFavicon = docuConfig.meta?.favicon || DEFAULT_FAVICON;
535
556
  const notFoundNonce = generateNonce();
536
- const notFoundHtml = htmlShell({
557
+ const notFoundContext: PageContext = {
558
+ pageType: "notFound",
559
+ assets: assetManifest.notFound,
560
+ slug: "404",
561
+ filePath: join(PAGES_DIR, "404.tsx"),
562
+ frontmatter: {},
563
+ config: docuConfig,
564
+ };
565
+ let notFoundHtml = htmlShell({
537
566
  title: "404 - Not Found",
538
567
  description: "",
539
568
  body: renderToString(notFoundPage),
540
569
  favicon: notFoundFavicon,
541
- headExtra: ['<meta name="robots" content="noindex,follow">'],
570
+ headExtra: [
571
+ '<meta name="robots" content="noindex,follow">',
572
+ ...(builder?.collectHead(notFoundContext) ?? []),
573
+ ],
574
+ bodyExtra: builder?.collectBody(notFoundContext),
542
575
  csp: cspHeader(notFoundNonce),
543
- css: assetManifest.css,
544
- js: assetManifest.js,
576
+ css: notFoundContext.assets.css,
577
+ js: notFoundContext.assets.js,
545
578
  nonce: notFoundNonce,
546
579
  themeCss: inlineThemeCss,
547
580
  // Served as the static-host fallback at ANY requested path — relative
548
581
  // depth can never be right there, so use root-absolute asset URLs.
549
582
  absoluteAssets: true,
550
583
  });
584
+ if (builder) notFoundHtml = await builder.runTransformHtmlChain(notFoundHtml, notFoundContext);
551
585
  await writeFile(join(DIST_DIR, "404.html"), notFoundHtml);
552
586
 
553
587
  logger.spinner.stop(
@@ -561,7 +595,7 @@ async function build() {
561
595
  filePath: join(DOCS_DIR, f.path),
562
596
  outputPath: join(DIST_DIR, "docs", `${f.path}.html`),
563
597
  }));
564
- await builder.runOnEnd(pages);
598
+ await builder.runOnEnd(pages, { assetManifest, outDir: DIST_DIR });
565
599
  }
566
600
 
567
601
  logger.indexStart();
@@ -8,7 +8,7 @@ import { FRAMEWORK_ROOT, STYLES_DIR, resolveProjectFile } from "./paths";
8
8
  * (e.g. Bun.build barrel optimization, Tailwind CLI upgrade). Old caches
9
9
  * with a mismatched version are discarded on read (see build.ts readCache).
10
10
  */
11
- export const BUILD_CACHE_VERSION = 4;
11
+ export const BUILD_CACHE_VERSION = 5;
12
12
 
13
13
  /** Toolchain fingerprint: Bun version on Bun, Deno version on Deno, Node elsewhere. */
14
14
  export function runtimeStamp(): string {
@@ -234,10 +234,14 @@ export function computeTailwindCacheKey(
234
234
  return h.digest("hex").slice(0, 16);
235
235
  }
236
236
 
237
- /** Read globals.css content ("" when missing). Shared by both hydrators. */
237
+ /** Read a Tailwind entry plus relative imports for cache invalidation. */
238
+ export function readStyleCss(file = "globals.css"): string {
239
+ return readCssWithImports(join(STYLES_DIR, file), new Set());
240
+ }
241
+
242
+ /** Backward-compatible alias for the docs stylesheet entry. */
238
243
  export function readGlobalsCss(): string {
239
- const globalsPath = join(STYLES_DIR, "globals.css");
240
- return existsSync(globalsPath) ? readFileSync(globalsPath, "utf-8") : "";
244
+ return readStyleCss();
241
245
  }
242
246
 
243
247
  /**
@@ -6,7 +6,7 @@ import { createMdxComponents } from "@docubook/markdown";
6
6
  import { mdxModules } from "./mdx-manifest";
7
7
  import Sidebar, { MobileBar } from "../components/Sidebar";
8
8
  import Toc from "../components/Toc";
9
- import { ThemeToggle } from "../components/Theme";
9
+
10
10
  import { safeParseTocs } from "./parse-tocs";
11
11
  import type { TocItem } from "./types";
12
12
 
@@ -21,14 +21,7 @@ import type { TocItem } from "./types";
21
21
  */
22
22
  type MountMode = "auto" | "hydrate" | "create";
23
23
 
24
- function mountIsland(
25
- id: string,
26
- render: (el: HTMLElement) => React.ReactElement | null,
27
- mode: MountMode = "auto"
28
- ) {
29
- const el = document.getElementById(id);
30
- if (!el) return;
31
- const node = render(el);
24
+ function mountNode(el: HTMLElement, node: React.ReactElement | null, mode: MountMode) {
32
25
  if (node === null) return; // island stays as-is (SSR HTML preserved)
33
26
  const hydrate = mode === "hydrate" || (mode === "auto" && el.childElementCount > 0);
34
27
  if (hydrate) {
@@ -39,6 +32,28 @@ function mountIsland(
39
32
  }
40
33
  }
41
34
 
35
+ function mountIsland(
36
+ id: string,
37
+ render: (el: HTMLElement) => React.ReactElement | null,
38
+ mode: MountMode = "auto"
39
+ ) {
40
+ const el = document.getElementById(id);
41
+ if (!el) return;
42
+ mountNode(el, render(el), mode);
43
+ }
44
+
45
+ function mountAsyncIsland(
46
+ id: string,
47
+ render: (el: HTMLElement) => Promise<React.ReactElement | null>,
48
+ mode: MountMode = "auto"
49
+ ) {
50
+ const el = document.getElementById(id);
51
+ if (!el) return;
52
+ render(el)
53
+ .then((node) => mountNode(el, node, mode))
54
+ .catch((error) => console.error(`[${id}]`, error));
55
+ }
56
+
42
57
  function mountIslands() {
43
58
  // SSR renders <Menu> only; client renders full <Sidebar> (DesktopSidebar +
44
59
  // MobileBar) — structural mismatch makes hydration impossible, so always
@@ -72,8 +87,6 @@ function mountIslands() {
72
87
  return React.createElement(Toc, { tocs });
73
88
  });
74
89
 
75
- mountIsland("theme-island", () => React.createElement(ThemeToggle));
76
-
77
90
  // MDX content: SSR renders the full content HTML; the client rebuilds the
78
91
  // identical tree. Two sources, same tree shape:
79
92
  // - static build: per-slug compiled ESM module bundled via ./mdx-manifest
@@ -81,30 +94,26 @@ function mountIslands() {
81
94
  // chain, so hydration matches;
82
95
  // - dev: legacy per-page compiledSource script → MDXRemote eval.
83
96
  // Hydrate when SSR markup exists, create only when the container is empty.
84
- mountIsland(
97
+ mountAsyncIsland(
85
98
  "mdx-content-island",
86
- (el) => {
99
+ async (el) => {
87
100
  const sourceEl = document.getElementById("mdx-compiled-source");
88
101
  if (sourceEl) {
89
- try {
90
- const compiledSource = JSON.parse(sourceEl.textContent || "");
91
- return React.createElement(MDXRemote, {
92
- compiledSource,
93
- scope: {},
94
- frontmatter: {},
95
- components: createMdxComponents(),
96
- });
97
- } catch (e) {
98
- console.error("[mdx-hydrate]", e);
99
- return null;
100
- }
102
+ const compiledSource = JSON.parse(sourceEl.textContent || "");
103
+ return React.createElement(MDXRemote, {
104
+ compiledSource,
105
+ scope: {},
106
+ frontmatter: {},
107
+ components: createMdxComponents(),
108
+ });
101
109
  }
102
110
  const slug = el.dataset.mdxSlug;
103
111
  // The docs root (index.mdx) renders with an empty slug — `mdxSlug != null`
104
112
  // keeps "" addressable (its module is stored under key ""), while a
105
113
  // missing marker stays undefined and skips hydration.
106
- const mod = slug != null ? mdxModules[slug] : undefined;
107
- if (!mod) return null;
114
+ const load = slug != null ? mdxModules[slug] : undefined;
115
+ if (!load) return null;
116
+ const mod = await load();
108
117
  return React.createElement(
109
118
  MDXProvider,
110
119
  { components: createMdxComponents() },
@@ -13,9 +13,9 @@ import { existsSync, readFileSync } from "node:fs";
13
13
  import { join, resolve } from "node:path";
14
14
  import { DIST_DIR, PROJECT_ROOT, FRAMEWORK_ROOT } from "./paths";
15
15
 
16
- const FLAME_MAJOR = JSON.parse(
16
+ const FLAME_VERSION = JSON.parse(
17
17
  readFileSync(resolve(FRAMEWORK_ROOT, "package.json"), "utf-8")
18
- ).version.split(".")[0];
18
+ ).version;
19
19
 
20
20
  const WORKFLOW_DIR = join(PROJECT_ROOT, ".github/workflows");
21
21
  const WORKFLOW_FILE = join(WORKFLOW_DIR, "deploy.yml");
@@ -25,6 +25,7 @@ export const NGINX_CONF = `server {
25
25
  server_name _;
26
26
  root /usr/share/nginx/html;
27
27
  index index.html;
28
+ error_page 404 /404.html;
28
29
 
29
30
  gzip on;
30
31
  gzip_types text/html text/css application/javascript image/svg+xml;
@@ -37,6 +38,16 @@ export const NGINX_CONF = `server {
37
38
  add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
38
39
  add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; font-src 'self' data:; connect-src 'self' https:; frame-src https://www.youtube-nocookie.com; frame-ancestors 'none'" always;
39
40
 
41
+ # Build metadata and search content use stable URLs, so clients must revalidate them.
42
+ location = /assets/manifest.json {
43
+ expires -1;
44
+ }
45
+
46
+ location = /assets/search-index.json {
47
+ expires -1;
48
+ }
49
+
50
+ # Generated JS, CSS, and chunks include content hashes in their filenames.
40
51
  location /assets/ {
41
52
  expires 1y;
42
53
  add_header Cache-Control "public, immutable";
@@ -59,6 +70,8 @@ export const NGINX_CONF = `server {
59
70
  add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; font-src 'self' data:; connect-src 'self' https:; frame-src https://www.youtube-nocookie.com; frame-ancestors 'none'" always;
60
71
  }
61
72
 
73
+ location = /404.html { }
74
+
62
75
  location / {
63
76
  try_files $uri $uri.html $uri/ =404;
64
77
  }
@@ -75,6 +88,19 @@ export const DOCKERIGNORE = `node_modules
75
88
  *.log
76
89
  `;
77
90
 
91
+ export const DOCKERFILE = `FROM ghcr.io/docubook/flame:${FLAME_VERSION} AS builder
92
+ ENV NODE_ENV=production
93
+ WORKDIR /app
94
+ COPY . .
95
+ RUN flame build --bun
96
+
97
+ FROM nginx:alpine
98
+ COPY --from=builder /app/.docu/dist /usr/share/nginx/html
99
+ COPY nginx.conf /etc/nginx/conf.d/default.conf
100
+ EXPOSE 80
101
+ CMD ["nginx", "-g", "daemon off;"]
102
+ `;
103
+
78
104
  export function detectPkgManager(dir: string): {
79
105
  baseImage: string;
80
106
  lockFile: string;
@@ -137,8 +163,22 @@ export const HEADERS_FILE = `/*
137
163
  Permissions-Policy: camera=(), microphone=(), geolocation=()
138
164
  Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; font-src 'self' data:; connect-src 'self' https:; frame-src https://www.youtube-nocookie.com; frame-ancestors 'none'
139
165
 
140
- /assets/*
166
+ /assets/client-*
141
167
  Cache-Control: public, max-age=31536000, immutable
168
+ /assets/home-client-*
169
+ Cache-Control: public, max-age=31536000, immutable
170
+ /assets/docs-*
171
+ Cache-Control: public, max-age=31536000, immutable
172
+ /assets/site-*
173
+ Cache-Control: public, max-age=31536000, immutable
174
+ /assets/chunks/*
175
+ Cache-Control: public, max-age=31536000, immutable
176
+ /assets/assets/*
177
+ Cache-Control: public, max-age=31536000, immutable
178
+ /assets/manifest.json
179
+ Cache-Control: no-cache
180
+ /assets/search-index.json
181
+ Cache-Control: no-cache
142
182
  `;
143
183
 
144
184
  const isDocker = !!process.env.FLAME_DEPLOY_DOCKER;
@@ -169,21 +209,7 @@ async function writeDockerFiles() {
169
209
  const dockerDir = PROJECT_ROOT;
170
210
 
171
211
  if (!existsSync(join(dockerDir, "Dockerfile"))) {
172
- await writeFile(
173
- join(dockerDir, "Dockerfile"),
174
- `FROM ghcr.io/docubook/flame:${FLAME_MAJOR} AS builder
175
- ENV NODE_ENV=production
176
- WORKDIR /app
177
- COPY . .
178
- RUN flame build
179
-
180
- FROM nginx:alpine
181
- COPY --from=builder /app/.docu/dist /usr/share/nginx/html
182
- COPY nginx.conf /etc/nginx/conf.d/default.conf
183
- EXPOSE 80
184
- CMD ["nginx", "-g", "daemon off;"]
185
- `
186
- );
212
+ await writeFile(join(dockerDir, "Dockerfile"), DOCKERFILE);
187
213
  log.created("📄 Created Dockerfile");
188
214
  }
189
215
 
@@ -8,14 +8,10 @@
8
8
  */
9
9
 
10
10
  import { mkdir } from "node:fs/promises";
11
- import { existsSync, readFileSync } from "node:fs";
12
- import { join, resolve } from "node:path";
13
- import { DIST_DIR, PROJECT_ROOT, FRAMEWORK_ROOT } from "./paths";
14
- import { HEADERS_FILE, NGINX_CONF, DOCKERIGNORE } from "./deploy.shared";
15
-
16
- const FLAME_PKG = JSON.parse(readFileSync(resolve(FRAMEWORK_ROOT, "package.json"), "utf-8"));
17
- const FLAME_VERSION = FLAME_PKG.version;
18
- const FLAME_MAJOR = FLAME_VERSION.split(".")[0];
11
+ import { existsSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { DIST_DIR, PROJECT_ROOT } from "./paths";
14
+ import { DOCKERFILE, HEADERS_FILE, NGINX_CONF, DOCKERIGNORE } from "./deploy.shared";
19
15
 
20
16
  export { HEADERS_FILE, NGINX_CONF, DOCKERIGNORE };
21
17
 
@@ -49,18 +45,7 @@ async function runBuild() {
49
45
  }
50
46
  }
51
47
 
52
- export const DOCKERFILE_BUN = `FROM ghcr.io/docubook/flame:${FLAME_MAJOR} AS builder
53
- ENV NODE_ENV=production
54
- WORKDIR /app
55
- COPY . .
56
- RUN flame build
57
-
58
- FROM nginx:alpine
59
- COPY --from=builder /app/.docu/dist /usr/share/nginx/html
60
- COPY nginx.conf /etc/nginx/conf.d/default.conf
61
- EXPOSE 80
62
- CMD ["nginx", "-g", "daemon off;"]
63
- `;
48
+ export const DOCKERFILE_BUN = DOCKERFILE;
64
49
 
65
50
  async function writeDockerFiles() {
66
51
  const dockerDir = PROJECT_ROOT;
@@ -0,0 +1,6 @@
1
+ import { hydrateRoot } from "react-dom/client";
2
+ import React from "react";
3
+ import { ThemeToggle } from "../components/Theme";
4
+
5
+ const root = document.getElementById("theme-toggle-island");
6
+ if (root) hydrateRoot(root, React.createElement(ThemeToggle));
@@ -17,7 +17,7 @@ export interface HtmlShellOptions {
17
17
  body: string;
18
18
  favicon: string;
19
19
  css: string;
20
- js: string;
20
+ js?: string;
21
21
  nonce?: string;
22
22
  /**
23
23
  * Content-Security-Policy value (from `cspHeader()` in security.ts).
@@ -63,6 +63,9 @@ export function htmlShell(opts: HtmlShellOptions): string {
63
63
  const bodyInjection = bodyExtra?.length ? `\n ${bodyExtra.join("\n ")}` : "";
64
64
  const depthPrefix = depth === 0 ? "" : "../".repeat(depth);
65
65
  const assetPrefix = absoluteAssets ? "/assets/" : depthPrefix + "assets/";
66
+ const clientScript = js
67
+ ? `\n <link rel="modulepreload" href="${escapeHtml(assetPrefix + js)}">\n <script type="module"${nonceAttr} src="${escapeHtml(assetPrefix + js)}"></script>`
68
+ : "";
66
69
  const resolvePath = (path: string) =>
67
70
  absoluteAssets ? path : path.startsWith("/") ? depthPrefix + path.slice(1) : path;
68
71
 
@@ -92,9 +95,7 @@ export function htmlShell(opts: HtmlShellOptions): string {
92
95
  <script${nonceAttr}>try{if(localStorage.getItem("theme")==="dark")document.documentElement.classList.add("dark")}catch(e){}</script>${headInjection}
93
96
  </head>
94
97
  <body>
95
- <div id="root">${body}</div>
96
- <link rel="modulepreload" href="${escapeHtml(assetPrefix + js)}">
97
- <script type="module"${nonceAttr} src="${escapeHtml(assetPrefix + js)}"></script>${extraScripts ? `\n ${extraScripts}` : ""}${bodyInjection}
98
+ <div id="root">${body}</div>${clientScript}${extraScripts ? `\n ${extraScripts}` : ""}${bodyInjection}
98
99
  </body>
99
100
  </html>`;
100
101
  }
@@ -25,6 +25,9 @@ export function htmlShell(opts: HtmlShellOptions): string {
25
25
  const bodyInjection = bodyExtra?.length ? `\n ${bodyExtra.join("\n ")}` : "";
26
26
  const depthPrefix = depth === 0 ? "" : "../".repeat(depth);
27
27
  const assetPrefix = absoluteAssets ? "/assets/" : depthPrefix + "assets/";
28
+ const clientScript = js
29
+ ? `\n <link rel="modulepreload" href="${Bun.escapeHTML(assetPrefix + js)}">\n <script type="module"${nonceAttr} src="${Bun.escapeHTML(assetPrefix + js)}"></script>`
30
+ : "";
28
31
  const resolvePath = (path: string) =>
29
32
  absoluteAssets ? path : path.startsWith("/") ? depthPrefix + path.slice(1) : path;
30
33
 
@@ -54,9 +57,7 @@ export function htmlShell(opts: HtmlShellOptions): string {
54
57
  <script${nonceAttr}>try{if(localStorage.getItem("theme")==="dark")document.documentElement.classList.add("dark")}catch(e){}</script>${headInjection}
55
58
  </head>
56
59
  <body>
57
- <div id="root">${body}</div>
58
- <link rel="modulepreload" href="${Bun.escapeHTML(assetPrefix + js)}">
59
- <script type="module"${nonceAttr} src="${Bun.escapeHTML(assetPrefix + js)}"></script>${extraScripts ? `\n ${extraScripts}` : ""}${bodyInjection}
60
+ <div id="root">${body}</div>${clientScript}${extraScripts ? `\n ${extraScripts}` : ""}${bodyInjection}
60
61
  </body>
61
62
  </html>`;
62
63
  }