@docubook/flame 1.5.4 → 1.6.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/lib/build.deno.js +4 -4
- package/.docu/lib/build.impl-N2MBT3CM.js +12 -0
- package/.docu/lib/build.node.js +4 -4
- package/.docu/lib/{chunk-J5NMYSBJ.js → chunk-4IQXHHPF.js} +5 -2
- package/.docu/lib/{chunk-C67LPMQA.js → chunk-IJFMQFWW.js} +51 -25
- package/.docu/lib/{chunk-NYCIAZTI.js → chunk-JTH4JBBS.js} +12 -5
- package/.docu/lib/chunk-TI6M4643.js +204 -0
- package/.docu/lib/{chunk-XLH7Z4QO.js → chunk-UUS45SUC.js} +2 -2
- package/.docu/lib/{chunk-GXQB4ETN.js → chunk-Y2TAEAUQ.js} +2 -2
- package/.docu/lib/{chunk-BDWN6AHQ.js → chunk-ZA5QB5UK.js} +4 -4
- package/.docu/lib/clean.js +2 -2
- package/.docu/lib/deploy.deno.js +2 -2
- package/.docu/lib/deploy.node.js +2 -2
- package/.docu/lib/preview.deno.js +3 -3
- package/.docu/lib/preview.node.js +3 -3
- package/.docu/lib/server.deno.js +4 -4
- package/.docu/lib/server.node.js +4 -4
- package/.docu/node/build.impl.ts +9 -3
- package/.docu/node/client.ts +38 -13
- package/.docu/node/deploy.shared.ts +147 -12
- package/.docu/node/deploy.ts +141 -30
- package/.docu/node/html.shared.ts +2 -0
- package/.docu/node/html.ts +4 -27
- package/.docu/node/hydrate.node.ts +74 -39
- package/.docu/node/hydrate.ts +74 -48
- package/.docu/node/paths.ts +7 -5
- package/.docu/node/server-routes.ts +2 -1
- package/bin/cli.js +19 -6
- package/package.json +2 -2
- package/.docu/lib/build.impl-CPFOQ7JW.js +0 -12
- package/.docu/lib/chunk-VKMZLLG5.js +0 -79
package/.docu/node/build.impl.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
385
|
+
nonce: notFoundNonce,
|
|
380
386
|
themeCss: inlineThemeCss,
|
|
381
387
|
});
|
|
382
388
|
await writeFile(join(DIST_DIR, "404.html"), notFoundHtml);
|
package/.docu/node/client.ts
CHANGED
|
@@ -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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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 —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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,26 +16,157 @@ 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
|
|
16
|
-
|
|
19
|
+
export const HEADERS_FILE = `/*
|
|
20
|
+
X-Frame-Options: DENY
|
|
21
|
+
X-Content-Type-Options: nosniff
|
|
22
|
+
Referrer-Policy: strict-origin-when-cross-origin
|
|
23
|
+
|
|
24
|
+
/assets/*
|
|
25
|
+
Cache-Control: public, max-age=31536000, immutable
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
const isDocker = !!process.env.FLAME_DEPLOY_DOCKER;
|
|
29
|
+
const isSilent = !!process.env.FLAME_DEPLOY_SILENT;
|
|
17
30
|
|
|
31
|
+
/** Logger that no-ops all non-error output in silent mode. */
|
|
32
|
+
const log = isSilent
|
|
33
|
+
? { info: () => {}, ok: () => {}, created: () => {}, out: () => {} }
|
|
34
|
+
: {
|
|
35
|
+
info: (m: string) => console.log(m),
|
|
36
|
+
ok: () => console.log("\n✅ Ready to deploy!"),
|
|
37
|
+
created: (m: string) => console.log(m),
|
|
38
|
+
out: (m: string) => console.log(m),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
async function runBuild() {
|
|
18
42
|
process.env.NODE_ENV = "production";
|
|
43
|
+
if (isSilent) {
|
|
44
|
+
process.env.FLAME_BUILD_SILENT = "1";
|
|
45
|
+
process.env.LOG_LEVEL = "error";
|
|
46
|
+
}
|
|
19
47
|
const { runBuildCli } = await import("./build.impl");
|
|
20
48
|
await runBuildCli();
|
|
49
|
+
}
|
|
21
50
|
|
|
22
|
-
|
|
23
|
-
|
|
51
|
+
async function writeDockerFiles() {
|
|
52
|
+
const dockerDir = PROJECT_ROOT;
|
|
53
|
+
|
|
54
|
+
if (!existsSync(join(dockerDir, "Dockerfile"))) {
|
|
55
|
+
await writeFile(
|
|
56
|
+
join(dockerDir, "Dockerfile"),
|
|
57
|
+
`FROM node:22-alpine AS builder
|
|
58
|
+
WORKDIR /app
|
|
59
|
+
COPY package.json package-lock.json ./
|
|
60
|
+
RUN npm ci
|
|
61
|
+
COPY . .
|
|
62
|
+
RUN npm run build
|
|
63
|
+
|
|
64
|
+
FROM nginx:alpine
|
|
65
|
+
COPY --from=builder /app/.docu/dist /usr/share/nginx/html
|
|
66
|
+
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
|
67
|
+
USER nginx
|
|
68
|
+
EXPOSE 80
|
|
69
|
+
CMD ["nginx", "-g", "daemon off;"]
|
|
70
|
+
`
|
|
71
|
+
);
|
|
72
|
+
log.created("📄 Created Dockerfile");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!existsSync(join(dockerDir, "nginx.conf"))) {
|
|
76
|
+
await writeFile(
|
|
77
|
+
join(dockerDir, "nginx.conf"),
|
|
78
|
+
`server {
|
|
79
|
+
listen 80;
|
|
80
|
+
server_name _;
|
|
81
|
+
root /usr/share/nginx/html;
|
|
82
|
+
index index.html;
|
|
83
|
+
|
|
84
|
+
gzip on;
|
|
85
|
+
gzip_types text/html text/css application/javascript image/svg+xml;
|
|
86
|
+
|
|
87
|
+
# Security headers (HSTS effective when HTTPS is terminated upstream)
|
|
88
|
+
add_header X-Frame-Options "DENY" always;
|
|
89
|
+
add_header X-Content-Type-Options "nosniff" always;
|
|
90
|
+
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
|
|
91
|
+
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
92
|
+
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
|
93
|
+
|
|
94
|
+
location /assets/ {
|
|
95
|
+
expires 1y;
|
|
96
|
+
add_header Cache-Control "public, immutable";
|
|
97
|
+
add_header X-Frame-Options "DENY" always;
|
|
98
|
+
add_header X-Content-Type-Options "nosniff" always;
|
|
99
|
+
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
|
|
100
|
+
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
101
|
+
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
location /docs/assets/ {
|
|
105
|
+
expires 7d;
|
|
106
|
+
add_header Cache-Control "public";
|
|
107
|
+
add_header X-Frame-Options "DENY" always;
|
|
108
|
+
add_header X-Content-Type-Options "nosniff" always;
|
|
109
|
+
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
|
|
110
|
+
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
111
|
+
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
location / {
|
|
115
|
+
try_files $uri $uri.html $uri/ =404;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
`
|
|
119
|
+
);
|
|
120
|
+
log.created("📄 Created nginx.conf");
|
|
121
|
+
}
|
|
24
122
|
|
|
25
|
-
|
|
123
|
+
if (!existsSync(join(dockerDir, ".dockerignore"))) {
|
|
124
|
+
await writeFile(
|
|
125
|
+
join(dockerDir, ".dockerignore"),
|
|
126
|
+
`node_modules
|
|
127
|
+
.git
|
|
128
|
+
*.DS_Store
|
|
129
|
+
.docu/dist
|
|
130
|
+
.docu/lib
|
|
131
|
+
.env
|
|
132
|
+
.env.*
|
|
133
|
+
.npmrc
|
|
134
|
+
*.log
|
|
135
|
+
`
|
|
136
|
+
);
|
|
137
|
+
log.created("📄 Created .dockerignore");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function writeGhaWorkflow() {
|
|
26
142
|
if (!existsSync(WORKFLOW_FILE)) {
|
|
27
143
|
await mkdir(WORKFLOW_DIR, { recursive: true });
|
|
28
144
|
await writeFile(WORKFLOW_FILE, GITHUB_ACTIONS_WORKFLOW);
|
|
29
|
-
|
|
145
|
+
log.created("📄 Created .github/workflows/deploy.yml");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function runDeploy(): Promise<void> {
|
|
150
|
+
log.info("📦 Building for production...\n");
|
|
151
|
+
await runBuild();
|
|
152
|
+
|
|
153
|
+
// Common: .nojekyll + _headers
|
|
154
|
+
await writeFile(join(DIST_DIR, ".nojekyll"), "");
|
|
155
|
+
await writeFile(join(DIST_DIR, "_headers"), HEADERS_FILE);
|
|
156
|
+
|
|
157
|
+
if (isDocker) {
|
|
158
|
+
await writeDockerFiles();
|
|
159
|
+
} else {
|
|
160
|
+
await writeGhaWorkflow();
|
|
30
161
|
}
|
|
31
162
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
163
|
+
log.ok();
|
|
164
|
+
log.out(" Output: .docu/dist/");
|
|
165
|
+
if (isDocker) {
|
|
166
|
+
log.out(" Run: docker build -t my-docs . && docker run -p 80:80 my-docs");
|
|
167
|
+
} else {
|
|
168
|
+
log.out(" Push to GitHub and enable Pages (Settings → Pages → Source: GitHub Actions)");
|
|
169
|
+
}
|
|
35
170
|
}
|
|
36
171
|
|
|
37
172
|
const GITHUB_ACTIONS_WORKFLOW = `name: Deploy to GitHub Pages
|
package/.docu/node/deploy.ts
CHANGED
|
@@ -1,56 +1,165 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Deploy script
|
|
2
|
+
* Deploy script — 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
|
|
3
6
|
*
|
|
4
|
-
*
|
|
5
|
-
* Runs build, adds .nojekyll, and generates GitHub Actions workflow.
|
|
7
|
+
* Bun-native path — uses Bun.write() and Bun.spawn().
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
|
-
import {
|
|
10
|
+
import { mkdir } from "node:fs/promises";
|
|
9
11
|
import { existsSync } from "node:fs";
|
|
10
12
|
import { join } from "node:path";
|
|
11
13
|
import { DIST_DIR, PROJECT_ROOT } from "./paths";
|
|
14
|
+
import { HEADERS_FILE } from "./deploy.shared";
|
|
15
|
+
|
|
16
|
+
export { HEADERS_FILE };
|
|
12
17
|
|
|
13
18
|
const WORKFLOW_DIR = join(PROJECT_ROOT, ".github/workflows");
|
|
14
19
|
const WORKFLOW_FILE = join(WORKFLOW_DIR, "deploy.yml");
|
|
15
20
|
|
|
16
|
-
const
|
|
17
|
-
|
|
21
|
+
const isDocker = !!process.env.FLAME_DEPLOY_DOCKER;
|
|
22
|
+
const isSilent = !!process.env.FLAME_DEPLOY_SILENT;
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
/** Logger that no-ops all non-error output in silent mode. */
|
|
25
|
+
const log = isSilent
|
|
26
|
+
? { info: () => {}, ok: () => {}, created: () => {}, out: () => {} }
|
|
27
|
+
: {
|
|
28
|
+
info: (m: string) => console.log(m),
|
|
29
|
+
ok: () => console.log("\n✅ Ready to deploy!"),
|
|
30
|
+
created: (m: string) => console.log(m),
|
|
31
|
+
out: (m: string) => console.log(m),
|
|
32
|
+
};
|
|
22
33
|
|
|
23
|
-
async function
|
|
24
|
-
console.log("📦 Building for production...\n");
|
|
25
|
-
|
|
26
|
-
// Run build
|
|
34
|
+
async function runBuild() {
|
|
27
35
|
const build = Bun.spawn(["bun", "run", "build"], {
|
|
28
|
-
stdout: "inherit",
|
|
29
|
-
stderr: "inherit",
|
|
36
|
+
stdout: isSilent ? "ignore" : "inherit",
|
|
37
|
+
stderr: isSilent ? "ignore" : "inherit",
|
|
30
38
|
});
|
|
31
39
|
const exitCode = await build.exited;
|
|
32
40
|
if (exitCode !== 0) {
|
|
33
41
|
console.error("\n❌ Build failed");
|
|
34
42
|
process.exit(1);
|
|
35
43
|
}
|
|
44
|
+
}
|
|
36
45
|
|
|
37
|
-
|
|
38
|
-
|
|
46
|
+
export const NGINX_CONF = `server {
|
|
47
|
+
listen 80;
|
|
48
|
+
server_name _;
|
|
49
|
+
root /usr/share/nginx/html;
|
|
50
|
+
index index.html;
|
|
51
|
+
|
|
52
|
+
gzip on;
|
|
53
|
+
gzip_types text/html text/css application/javascript image/svg+xml;
|
|
54
|
+
|
|
55
|
+
# Security headers (server-level, inherited by all locations)
|
|
56
|
+
# HSTS effective when HTTPS is terminated upstream (reverse proxy / LB)
|
|
57
|
+
add_header X-Frame-Options "DENY" always;
|
|
58
|
+
add_header X-Content-Type-Options "nosniff" always;
|
|
59
|
+
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
|
|
60
|
+
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
61
|
+
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
|
62
|
+
|
|
63
|
+
location /assets/ {
|
|
64
|
+
expires 1y;
|
|
65
|
+
add_header Cache-Control "public, immutable";
|
|
66
|
+
add_header X-Frame-Options "DENY" always;
|
|
67
|
+
add_header X-Content-Type-Options "nosniff" always;
|
|
68
|
+
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
|
|
69
|
+
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
70
|
+
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
|
71
|
+
}
|
|
39
72
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
73
|
+
location /docs/assets/ {
|
|
74
|
+
expires 7d;
|
|
75
|
+
add_header Cache-Control "public";
|
|
76
|
+
add_header X-Frame-Options "DENY" always;
|
|
77
|
+
add_header X-Content-Type-Options "nosniff" always;
|
|
78
|
+
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
|
|
79
|
+
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
80
|
+
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
|
81
|
+
}
|
|
43
82
|
|
|
44
|
-
|
|
83
|
+
location / {
|
|
84
|
+
try_files $uri $uri.html $uri/ =404;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
`;
|
|
88
|
+
|
|
89
|
+
export const DOCKERFILE_BUN = `FROM oven/bun:1 AS builder
|
|
90
|
+
WORKDIR /app
|
|
91
|
+
COPY package.json bun.lock ./
|
|
92
|
+
RUN bun install --frozen-lockfile
|
|
93
|
+
COPY . .
|
|
94
|
+
RUN bun run build
|
|
95
|
+
|
|
96
|
+
FROM nginx:alpine
|
|
97
|
+
COPY --from=builder /app/.docu/dist /usr/share/nginx/html
|
|
98
|
+
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
|
99
|
+
USER nginx
|
|
100
|
+
EXPOSE 80
|
|
101
|
+
CMD ["nginx", "-g", "daemon off;"]
|
|
102
|
+
`;
|
|
103
|
+
|
|
104
|
+
export const DOCKERIGNORE = `node_modules
|
|
105
|
+
.git
|
|
106
|
+
*.DS_Store
|
|
107
|
+
.docu/dist
|
|
108
|
+
.docu/lib
|
|
109
|
+
.env
|
|
110
|
+
.env.*
|
|
111
|
+
.npmrc
|
|
112
|
+
*.log
|
|
113
|
+
`;
|
|
114
|
+
|
|
115
|
+
async function writeDockerFiles() {
|
|
116
|
+
const dockerDir = PROJECT_ROOT;
|
|
117
|
+
|
|
118
|
+
if (!existsSync(join(dockerDir, "Dockerfile"))) {
|
|
119
|
+
await Bun.write(join(dockerDir, "Dockerfile"), DOCKERFILE_BUN);
|
|
120
|
+
log.created("📄 Created Dockerfile");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!existsSync(join(dockerDir, "nginx.conf"))) {
|
|
124
|
+
await Bun.write(join(dockerDir, "nginx.conf"), NGINX_CONF);
|
|
125
|
+
log.created("📄 Created nginx.conf");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!existsSync(join(dockerDir, ".dockerignore"))) {
|
|
129
|
+
await Bun.write(join(dockerDir, ".dockerignore"), DOCKERIGNORE);
|
|
130
|
+
log.created("📄 Created .dockerignore");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function writeGhaWorkflow() {
|
|
45
135
|
if (!existsSync(WORKFLOW_FILE)) {
|
|
46
136
|
await mkdir(WORKFLOW_DIR, { recursive: true });
|
|
47
|
-
await
|
|
48
|
-
|
|
137
|
+
await Bun.write(WORKFLOW_FILE, GITHUB_ACTIONS_WORKFLOW);
|
|
138
|
+
log.created("📄 Created .github/workflows/deploy.yml");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function deploy() {
|
|
143
|
+
log.info("📦 Building for production...\n");
|
|
144
|
+
await runBuild();
|
|
145
|
+
|
|
146
|
+
// Common: .nojekyll + _headers
|
|
147
|
+
await Bun.write(join(DIST_DIR, ".nojekyll"), "");
|
|
148
|
+
await Bun.write(join(DIST_DIR, "_headers"), HEADERS_FILE);
|
|
149
|
+
|
|
150
|
+
if (isDocker) {
|
|
151
|
+
await writeDockerFiles();
|
|
152
|
+
} else {
|
|
153
|
+
await writeGhaWorkflow();
|
|
49
154
|
}
|
|
50
155
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
156
|
+
log.ok();
|
|
157
|
+
log.out(" Output: .docu/dist/");
|
|
158
|
+
if (isDocker) {
|
|
159
|
+
log.out(" Run: docker build -t my-docs . && docker run -p 80:80 my-docs");
|
|
160
|
+
} else {
|
|
161
|
+
log.out(" Push to GitHub and enable Pages (Settings → Pages → Source: GitHub Actions)");
|
|
162
|
+
}
|
|
54
163
|
}
|
|
55
164
|
|
|
56
165
|
const GITHUB_ACTIONS_WORKFLOW = `name: Deploy to GitHub Pages
|
|
@@ -103,7 +212,9 @@ jobs:
|
|
|
103
212
|
uses: actions/deploy-pages@v4
|
|
104
213
|
`;
|
|
105
214
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
215
|
+
if (import.meta.main) {
|
|
216
|
+
deploy().catch((err) => {
|
|
217
|
+
console.error("Deploy failed:", err);
|
|
218
|
+
process.exit(1);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
@@ -79,6 +79,7 @@ export function htmlShell(opts: HtmlShellOptions): string {
|
|
|
79
79
|
<title>${escapeHtml(title)}</title>
|
|
80
80
|
<meta name="description" content="${escapeHtml(description)}">
|
|
81
81
|
${favicon ? `<link rel="icon" type="image/x-icon" href="${escapeHtml(resolvePath(favicon))}">` : ""}${themeStyle}
|
|
82
|
+
<link rel="preload" href="${escapeHtml(assetPrefix + css)}" as="style">
|
|
82
83
|
<link rel="stylesheet" href="${escapeHtml(assetPrefix + css)}">
|
|
83
84
|
${csp ? `<meta http-equiv="Content-Security-Policy" content="${escapeHtml(csp)}">` : ""}
|
|
84
85
|
${seoTags}
|
|
@@ -86,6 +87,7 @@ export function htmlShell(opts: HtmlShellOptions): string {
|
|
|
86
87
|
</head>
|
|
87
88
|
<body>
|
|
88
89
|
<div id="root">${body}</div>
|
|
90
|
+
<link rel="modulepreload" href="${escapeHtml(assetPrefix + js)}">
|
|
89
91
|
<script type="module"${nonceAttr} src="${escapeHtml(assetPrefix + js)}"></script>${extraScripts ? `\n ${extraScripts}` : ""}${bodyInjection}
|
|
90
92
|
</body>
|
|
91
93
|
</html>`;
|
package/.docu/node/html.ts
CHANGED
|
@@ -1,30 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
export interface HtmlShellOptions {
|
|
4
|
-
title: string;
|
|
5
|
-
description: string;
|
|
6
|
-
body: string;
|
|
7
|
-
favicon: string;
|
|
8
|
-
css: string;
|
|
9
|
-
js: string;
|
|
10
|
-
nonce?: string;
|
|
11
|
-
/**
|
|
12
|
-
* Content-Security-Policy value (from `cspHeader()` in security.ts).
|
|
13
|
-
* When provided, injects `<meta http-equiv="Content-Security-Policy">` in `<head>`.
|
|
14
|
-
* Essential for static deployment where HTTP headers cannot be set.
|
|
15
|
-
*/
|
|
16
|
-
csp?: string;
|
|
17
|
-
extraScripts?: string;
|
|
18
|
-
themeCss?: string;
|
|
19
|
-
/** Depth from document root (0=root, 1=subdir, 2=sub/subdir). Used for relative asset paths. */
|
|
20
|
-
depth?: number;
|
|
21
|
-
/** HTML strings to inject before `</head>` (from plugin `injectHead` hooks). */
|
|
22
|
-
headExtra?: string[];
|
|
23
|
-
/** HTML strings to inject before `</body>`, after the main script (from plugin `injectBody` hooks). */
|
|
24
|
-
bodyExtra?: string[];
|
|
25
|
-
/** SEO meta tags derived from config + frontmatter */
|
|
26
|
-
seo?: SeoMeta;
|
|
27
|
-
}
|
|
1
|
+
import type { HtmlShellOptions } from "./html.shared";
|
|
2
|
+
export type { HtmlShellOptions };
|
|
28
3
|
|
|
29
4
|
export function htmlShell(opts: HtmlShellOptions): string {
|
|
30
5
|
const {
|
|
@@ -69,6 +44,7 @@ export function htmlShell(opts: HtmlShellOptions): string {
|
|
|
69
44
|
<title>${Bun.escapeHTML(title)}</title>
|
|
70
45
|
<meta name="description" content="${Bun.escapeHTML(description)}">
|
|
71
46
|
${favicon ? `<link rel="icon" type="image/x-icon" href="${Bun.escapeHTML(resolvePath(favicon))}">` : ""}${themeStyle}
|
|
47
|
+
<link rel="preload" href="${Bun.escapeHTML(assetPrefix + css)}" as="style">
|
|
72
48
|
<link rel="stylesheet" href="${Bun.escapeHTML(assetPrefix + css)}">
|
|
73
49
|
${csp ? `<meta http-equiv="Content-Security-Policy" content="${Bun.escapeHTML(csp)}">` : ""}
|
|
74
50
|
${seoTags}
|
|
@@ -76,6 +52,7 @@ export function htmlShell(opts: HtmlShellOptions): string {
|
|
|
76
52
|
</head>
|
|
77
53
|
<body>
|
|
78
54
|
<div id="root">${body}</div>
|
|
55
|
+
<link rel="modulepreload" href="${Bun.escapeHTML(assetPrefix + js)}">
|
|
79
56
|
<script type="module"${nonceAttr} src="${Bun.escapeHTML(assetPrefix + js)}"></script>${extraScripts ? `\n ${extraScripts}` : ""}${bodyInjection}
|
|
80
57
|
</body>
|
|
81
58
|
</html>`;
|