@docubook/flame 2.0.0-beta.5 → 2.0.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/components/Theme.tsx +42 -34
- package/.docu/components/home/BackgroundBlobs.tsx +31 -0
- package/.docu/components/home/index.tsx +1 -0
- package/.docu/lib/build.deno.js +1 -1
- package/.docu/lib/{build.impl-BBlR3si3.js → build.impl-CAQr352X.js} +60 -29
- package/.docu/lib/build.impl-CAQr352X.js.map +1 -0
- package/.docu/lib/build.impl-NLhjTqTa.js +2 -0
- package/.docu/lib/build.node.js +1 -1
- package/.docu/lib/clean.js +1 -1
- package/.docu/lib/deploy.deno.js +1 -1
- package/.docu/lib/deploy.node.js +1 -1
- package/.docu/lib/{deploy.shared-Dxv3lsOb.js → deploy.shared-CvgObu6_.js} +45 -17
- package/.docu/lib/deploy.shared-CvgObu6_.js.map +1 -0
- package/.docu/lib/{html.shared-DBMwv_Z6.js → html.shared-CsEFmy_Y.js} +181 -158
- package/.docu/lib/html.shared-CsEFmy_Y.js.map +1 -0
- package/.docu/lib/{logger-DYmvFF1d.js → logger-DwxYdOL7.js} +4 -3
- package/.docu/lib/logger-DwxYdOL7.js.map +1 -0
- package/.docu/lib/{paths-Bl2cdp9E.js → paths-Cbz-dj4s.js} +5 -9
- package/.docu/lib/{paths-Bl2cdp9E.js.map → paths-Cbz-dj4s.js.map} +1 -1
- package/.docu/lib/preview.deno.js +1 -1
- package/.docu/lib/{preview.impl-anVcZv2Z.js → preview.impl-BwrVLNIQ.js} +3 -3
- package/.docu/lib/{preview.impl-anVcZv2Z.js.map → preview.impl-BwrVLNIQ.js.map} +1 -1
- package/.docu/lib/preview.node.js +1 -1
- package/.docu/lib/server.deno.js +1 -1
- package/.docu/lib/{server.impl-Cvoa6Diq.js → server.impl-Cs6DEy4V.js} +43 -52
- package/.docu/lib/server.impl-Cs6DEy4V.js.map +1 -0
- package/.docu/lib/server.node.js +1 -1
- package/.docu/node/build.impl.ts +71 -34
- package/.docu/node/build.ts +68 -34
- package/.docu/node/cache-key.ts +8 -4
- package/.docu/node/client.ts +36 -27
- package/.docu/node/deploy.shared.ts +44 -18
- package/.docu/node/deploy.ts +5 -20
- package/.docu/node/home-client.ts +6 -0
- package/.docu/node/html.shared.ts +5 -4
- package/.docu/node/html.ts +4 -3
- package/.docu/node/hydrate.node.ts +87 -115
- package/.docu/node/hydrate.ts +74 -80
- package/.docu/node/mdx-manifest.d.ts +2 -2
- package/.docu/node/paths.ts +2 -9
- package/.docu/node/plugin-builder.ts +25 -13
- package/.docu/node/plugin.ts +33 -9
- package/.docu/node/server-routes.ts +59 -50
- package/.docu/node/server.impl.ts +4 -2
- package/.docu/node/server.ts +12 -16
- package/.docu/node/types.ts +11 -0
- package/.docu/pages/404.tsx +12 -9
- package/.docu/pages/index.tsx +8 -29
- package/.docu/styles/base.css +128 -0
- package/.docu/styles/globals.css +5 -137
- package/.docu/styles/site.css +7 -0
- package/package.json +6 -5
- package/.docu/lib/build.impl-BBlR3si3.js.map +0 -1
- package/.docu/lib/build.impl-DpoDUmei.js +0 -2
- package/.docu/lib/deploy.shared-Dxv3lsOb.js.map +0 -1
- package/.docu/lib/html.shared-DBMwv_Z6.js.map +0 -1
- package/.docu/lib/logger-DYmvFF1d.js.map +0 -1
- package/.docu/lib/server.impl-Cvoa6Diq.js.map +0 -1
package/.docu/node/build.ts
CHANGED
|
@@ -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 = {
|
|
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<
|
|
134
|
+
): Promise<AssetManifest> {
|
|
130
135
|
if (!bundleHit) return buildClientBundle(mdxSources);
|
|
131
136
|
try {
|
|
132
|
-
const manifest = JSON.parse(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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 = {
|
|
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:
|
|
242
|
-
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
|
|
384
|
-
|
|
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
|
|
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:
|
|
523
|
-
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
|
|
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: [
|
|
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:
|
|
544
|
-
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();
|
package/.docu/node/cache-key.ts
CHANGED
|
@@ -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 =
|
|
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
|
|
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
|
-
|
|
240
|
-
return existsSync(globalsPath) ? readFileSync(globalsPath, "utf-8") : "";
|
|
244
|
+
return readStyleCss();
|
|
241
245
|
}
|
|
242
246
|
|
|
243
247
|
/**
|
package/.docu/node/client.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
107
|
-
if (!
|
|
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
|
|
16
|
+
const FLAME_VERSION = JSON.parse(
|
|
17
17
|
readFileSync(resolve(FRAMEWORK_ROOT, "package.json"), "utf-8")
|
|
18
|
-
).version
|
|
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
|
|
package/.docu/node/deploy.ts
CHANGED
|
@@ -8,14 +8,10 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { mkdir } from "node:fs/promises";
|
|
11
|
-
import { existsSync
|
|
12
|
-
import { join
|
|
13
|
-
import { DIST_DIR, PROJECT_ROOT
|
|
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 =
|
|
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;
|
|
@@ -17,7 +17,7 @@ export interface HtmlShellOptions {
|
|
|
17
17
|
body: string;
|
|
18
18
|
favicon: string;
|
|
19
19
|
css: string;
|
|
20
|
-
js
|
|
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
|
}
|
package/.docu/node/html.ts
CHANGED
|
@@ -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
|
}
|