@docubook/flame 1.4.3 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.docu/lib/build.deno.js +11 -0
- package/.docu/lib/build.impl-ST63VRTV.js +12 -0
- package/.docu/lib/build.node.js +10 -0
- package/.docu/lib/chunk-2QHMGZIL.js +2419 -0
- package/.docu/lib/chunk-654THQOR.js +461 -0
- package/.docu/lib/chunk-C6RZ2KBH.js +79 -0
- package/.docu/lib/chunk-HH4YXWEF.js +300 -0
- package/.docu/lib/chunk-J5NMYSBJ.js +59 -0
- package/.docu/lib/chunk-RE4NGTMT.js +185 -0
- package/.docu/lib/chunk-X6GYOIYZ.js +383 -0
- package/.docu/lib/chunk-ZOWTASXL.js +92 -0
- package/.docu/lib/clean.js +32 -0
- package/.docu/lib/deploy.deno.js +13 -0
- package/.docu/lib/deploy.node.js +10 -0
- package/.docu/lib/preview.deno.js +10 -0
- package/.docu/lib/preview.node.js +10 -0
- package/.docu/lib/server.deno.js +11 -0
- package/.docu/lib/server.node.js +11 -0
- package/.docu/node/build-summary.ts +126 -0
- package/.docu/node/build.deno.ts +7 -0
- package/.docu/node/build.impl.ts +424 -0
- package/.docu/node/build.node.ts +3 -0
- package/.docu/node/deploy.deno.ts +11 -0
- package/.docu/node/deploy.node.ts +6 -0
- package/.docu/node/deploy.shared.ts +85 -0
- package/.docu/node/deploy.ts +11 -0
- package/.docu/node/escapeHtml.ts +18 -0
- package/.docu/node/git.ts +79 -0
- package/.docu/node/html.shared.ts +110 -0
- package/.docu/node/html.ts +1 -1
- package/.docu/node/hydrate.node.ts +287 -0
- package/.docu/node/hydrate.ts +16 -19
- package/.docu/node/mdx.ts +1 -1
- package/.docu/node/paths.ts +24 -0
- package/.docu/node/plugin-builder.ts +6 -2
- package/.docu/node/plugin.ts +11 -2
- package/.docu/node/preview.deno.ts +4 -0
- package/.docu/node/preview.impl.ts +96 -0
- package/.docu/node/preview.node.ts +4 -0
- package/.docu/node/security.ts +5 -0
- package/.docu/node/server-routes.ts +4 -4
- package/.docu/node/server.deno.ts +4 -0
- package/.docu/node/server.impl.ts +184 -0
- package/.docu/node/server.node.ts +4 -0
- package/.docu/styles/globals.css +20 -5
- package/README.md +57 -506
- package/bin/cli.js +89 -14
- package/bin/compile-lib.mjs +67 -0
- package/package.json +9 -5
- package/template/docs/getting-started/configuration.mdx +18 -0
- package/template/docs/getting-started/overview.mdx +50 -0
- package/template/docs/guide/deployment.mdx +27 -0
- package/template/docs/guide/routing.mdx +25 -0
- package/template/docs/index.mdx +8 -205
- package/template/docu.json +32 -1
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build output summary: walks `dist` after a build, collects generated files
|
|
3
|
+
* and their sizes, groups them by extension, and formats a scannable report
|
|
4
|
+
* for CI logs. Runtime-neutral — uses only `node:fs/promises` and `node:path`
|
|
5
|
+
* so it runs identically under Bun, Node, and Deno.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readdir, stat } from "node:fs/promises";
|
|
9
|
+
import { join, relative, extname } from "node:path";
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
11
|
+
|
|
12
|
+
export interface BuildOutputFile {
|
|
13
|
+
path: string;
|
|
14
|
+
size: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface BuildOutputType {
|
|
18
|
+
ext: string;
|
|
19
|
+
count: number;
|
|
20
|
+
size: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface BuildOutput {
|
|
24
|
+
files: BuildOutputFile[];
|
|
25
|
+
byType: BuildOutputType[];
|
|
26
|
+
totalFiles: number;
|
|
27
|
+
totalSize: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const MAX_FILES_SHOWN = 10;
|
|
31
|
+
const SEP = "─".repeat(60);
|
|
32
|
+
|
|
33
|
+
export function formatSize(bytes: number): string {
|
|
34
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
35
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
|
|
36
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function walkFiles(dir: string): Promise<BuildOutputFile[]> {
|
|
40
|
+
const out: BuildOutputFile[] = [];
|
|
41
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
const full = join(dir, entry.name);
|
|
44
|
+
if (entry.isDirectory()) {
|
|
45
|
+
out.push(...(await walkFiles(full)));
|
|
46
|
+
} else if (entry.isFile()) {
|
|
47
|
+
const s = await stat(full);
|
|
48
|
+
out.push({ path: full, size: s.size });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Collect build output metadata for `distDir`. Returns `null` if the dir is missing or empty. */
|
|
55
|
+
export async function collectBuildOutput(distDir: string): Promise<BuildOutput | null> {
|
|
56
|
+
if (!existsSync(distDir)) return null;
|
|
57
|
+
const all = await walkFiles(distDir);
|
|
58
|
+
if (all.length === 0) return null;
|
|
59
|
+
|
|
60
|
+
const files = all
|
|
61
|
+
.map((f) => ({ path: relative(distDir, f.path), size: f.size }))
|
|
62
|
+
.sort((a, b) => (b.size !== a.size ? b.size - a.size : a.path.localeCompare(b.path)));
|
|
63
|
+
|
|
64
|
+
const byExt = new Map<string, BuildOutputType>();
|
|
65
|
+
for (const f of files) {
|
|
66
|
+
const ext = extname(f.path) || "(none)";
|
|
67
|
+
const entry = byExt.get(ext) ?? { ext, count: 0, size: 0 };
|
|
68
|
+
entry.count++;
|
|
69
|
+
entry.size += f.size;
|
|
70
|
+
byExt.set(ext, entry);
|
|
71
|
+
}
|
|
72
|
+
const byType = [...byExt.values()].sort((a, b) =>
|
|
73
|
+
b.size !== a.size ? b.size - a.size : b.count - a.count
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
files,
|
|
78
|
+
byType,
|
|
79
|
+
totalFiles: files.length,
|
|
80
|
+
totalSize: files.reduce((sum, f) => sum + f.size, 0),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Format `output` as a human-readable, color-free summary suitable for CI logs. */
|
|
85
|
+
export function formatBuildSummary(output: BuildOutput): string {
|
|
86
|
+
const lines: string[] = [];
|
|
87
|
+
lines.push("📦 Build Output Summary:");
|
|
88
|
+
lines.push(SEP);
|
|
89
|
+
|
|
90
|
+
const shown = output.files.slice(0, MAX_FILES_SHOWN);
|
|
91
|
+
const nameWidth = Math.min(Math.max(...shown.map((f) => f.path.length), 1), 50);
|
|
92
|
+
for (const f of shown) {
|
|
93
|
+
lines.push(` ${f.path.padEnd(nameWidth)} ${formatSize(f.size).padStart(10)}`);
|
|
94
|
+
}
|
|
95
|
+
const remaining = output.totalFiles - shown.length;
|
|
96
|
+
if (remaining > 0) {
|
|
97
|
+
lines.push(` ... and ${remaining} more file${remaining === 1 ? "" : "s"}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
lines.push(SEP);
|
|
101
|
+
lines.push("📊 Summary by Type:");
|
|
102
|
+
const extWidth = Math.max(...output.byType.map((t) => t.ext.length), 1);
|
|
103
|
+
for (const t of output.byType) {
|
|
104
|
+
const plural = t.count === 1 ? "file" : "files";
|
|
105
|
+
lines.push(
|
|
106
|
+
` ${t.ext.padEnd(extWidth)} ${String(t.count).padStart(3)} ${plural} ${formatSize(
|
|
107
|
+
t.size
|
|
108
|
+
).padStart(10)}`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
lines.push(SEP);
|
|
113
|
+
const totalPlural = output.totalFiles === 1 ? "file" : "files";
|
|
114
|
+
lines.push(
|
|
115
|
+
`📈 Total: ${output.totalFiles} ${totalPlural} ${formatSize(output.totalSize).padStart(10)}`
|
|
116
|
+
);
|
|
117
|
+
lines.push(SEP);
|
|
118
|
+
|
|
119
|
+
return lines.join("\n");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Collect and format the build summary for `distDir`. Returns `null` when there is nothing to report. */
|
|
123
|
+
export async function generateBuildSummary(distDir: string): Promise<string | null> {
|
|
124
|
+
const output = await collectBuildOutput(distDir);
|
|
125
|
+
return output ? formatBuildSummary(output) : null;
|
|
126
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { runBuildCli } from "./build.impl";
|
|
2
|
+
|
|
3
|
+
await runBuildCli();
|
|
4
|
+
// Deno resolves react-dom/server to server.browser.js (its `deno` export
|
|
5
|
+
// condition), which opens a module-scope MessageChannel that keeps the event
|
|
6
|
+
// loop alive after the build completes — force a clean exit.
|
|
7
|
+
process.exit(0);
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime-neutral static build — mirror of `build.ts` (Bun-only via its
|
|
3
|
+
* transitive imports, protected) with the Bun-coupled modules swapped for
|
|
4
|
+
* their neutral counterparts: `html.shared` (pure escaping) and
|
|
5
|
+
* `hydrate.node` (esbuild client bundling). Everything else is identical.
|
|
6
|
+
* The Node/Deno build entries call `runBuildCli()`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFile, writeFile, mkdir, readdir, copyFile } from "node:fs/promises";
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { join, dirname } from "node:path";
|
|
13
|
+
import React from "react";
|
|
14
|
+
import { renderToString } from "react-dom/server";
|
|
15
|
+
import { compileMdx, getGitLastModifiedBatch } from "./mdx";
|
|
16
|
+
import {
|
|
17
|
+
DOCS_DIR,
|
|
18
|
+
DIST_DIR,
|
|
19
|
+
ASSETS_DIR,
|
|
20
|
+
CACHE_FILE,
|
|
21
|
+
DOCS_ASSETS_DIR,
|
|
22
|
+
PROJECT_ROOT,
|
|
23
|
+
loadDocuConfig,
|
|
24
|
+
} from "./paths";
|
|
25
|
+
import { htmlShell } from "./html.shared";
|
|
26
|
+
import { generateSearchIndex } from "./search-indexer";
|
|
27
|
+
import { buildClientBundle, computeInlineThemeCss } from "./hydrate.node";
|
|
28
|
+
import { logger } from "./logger";
|
|
29
|
+
import { generateBuildSummary } from "./build-summary";
|
|
30
|
+
import { initSentry, captureException } from "./sentry";
|
|
31
|
+
import { loadPlugins } from "./plugin-loader";
|
|
32
|
+
import { BuildPluginBuilder } from "./plugin-builder";
|
|
33
|
+
import { scanMdxFiles } from "./utils";
|
|
34
|
+
import type { BuildCache, CliArgs } from "./types";
|
|
35
|
+
import { generateNonce } from "./security";
|
|
36
|
+
import type { PageMeta, PageContext } from "./plugin";
|
|
37
|
+
import DocsPage from "../pages/docs/[[...slug]]";
|
|
38
|
+
import IndexPage from "../pages/index";
|
|
39
|
+
import NotFoundPage from "../pages/404";
|
|
40
|
+
import { DocsLayout } from "../components/DocsLayout";
|
|
41
|
+
|
|
42
|
+
function parseArgs(): CliArgs {
|
|
43
|
+
const args = process.argv.slice(2);
|
|
44
|
+
return {
|
|
45
|
+
force: args.includes("--force") || args.includes("-f"),
|
|
46
|
+
clean: args.includes("--clean") || args.includes("-c"),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function hashContent(content: string): string {
|
|
51
|
+
return createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function readCache(): Promise<BuildCache> {
|
|
55
|
+
try {
|
|
56
|
+
if (existsSync(CACHE_FILE)) {
|
|
57
|
+
const data = await readFile(CACHE_FILE, "utf-8");
|
|
58
|
+
return JSON.parse(data);
|
|
59
|
+
}
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.error("Failed to load build cache:", (err as Error).message);
|
|
62
|
+
}
|
|
63
|
+
return {};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function writeCache(cache: BuildCache): Promise<void> {
|
|
67
|
+
await writeFile(CACHE_FILE, JSON.stringify(cache, null, 2));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function parseConcurrency(): number {
|
|
71
|
+
return Math.max(1, parseInt(process.env.BUILD_CONCURRENCY || "4", 10) || 4);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type RebuildDecision = "yes" | "hash_check" | "no";
|
|
75
|
+
|
|
76
|
+
function shouldRebuild(path: string, mtime: number, cache: BuildCache): RebuildDecision {
|
|
77
|
+
const cached = cache[path];
|
|
78
|
+
if (!cached) return "yes";
|
|
79
|
+
if (mtime > cached.builtAt) return "hash_check";
|
|
80
|
+
return "no";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let assetManifest = { js: "client.js", css: "client.css" };
|
|
84
|
+
|
|
85
|
+
let inlineThemeCss: string | undefined;
|
|
86
|
+
|
|
87
|
+
async function renderDocsPage(
|
|
88
|
+
docuConfig: ReturnType<typeof loadDocuConfig>,
|
|
89
|
+
slug: string,
|
|
90
|
+
rawMdx: string,
|
|
91
|
+
filePath: string,
|
|
92
|
+
gitDates?: Map<string, string>,
|
|
93
|
+
builder?: BuildPluginBuilder | null,
|
|
94
|
+
nonce?: string
|
|
95
|
+
): Promise<string> {
|
|
96
|
+
let content = rawMdx;
|
|
97
|
+
if (builder) {
|
|
98
|
+
const transformed = await builder.runOnLoad(filePath, content);
|
|
99
|
+
if (transformed?.contents) {
|
|
100
|
+
content = transformed.contents;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let result;
|
|
105
|
+
try {
|
|
106
|
+
const remarkPlugins = builder?.collectRemarkPlugins();
|
|
107
|
+
const rehypePlugins = builder?.collectRehypePlugins();
|
|
108
|
+
result = await compileMdx(content, filePath, gitDates, remarkPlugins, rehypePlugins);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
const msg = err instanceof Error ? err.message : "Unknown MDX error";
|
|
111
|
+
throw new Error(`MDX Error in: docs/${slug}.mdx\n${msg}`, { cause: err });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let frontmatter = result.frontmatter as Record<string, unknown>;
|
|
115
|
+
if (builder) {
|
|
116
|
+
frontmatter = await builder.runTransformFrontmatterChain(frontmatter, {
|
|
117
|
+
slug,
|
|
118
|
+
filePath,
|
|
119
|
+
content,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const title = (typeof frontmatter.title === "string" ? frontmatter.title : "") || slug || "Docs";
|
|
124
|
+
const description = typeof frontmatter.description === "string" ? frontmatter.description : "";
|
|
125
|
+
const slugParts = slug ? slug.split("/") : [];
|
|
126
|
+
|
|
127
|
+
const page = React.createElement(
|
|
128
|
+
DocsLayout,
|
|
129
|
+
{ repoUrl: docuConfig.repo?.url },
|
|
130
|
+
React.createElement(DocsPage, {
|
|
131
|
+
slug: slugParts,
|
|
132
|
+
title,
|
|
133
|
+
description,
|
|
134
|
+
date: (frontmatter.date as string) || undefined,
|
|
135
|
+
content: result.content,
|
|
136
|
+
tocs: result.tocs,
|
|
137
|
+
filePath,
|
|
138
|
+
repoUrl: docuConfig.repo?.url,
|
|
139
|
+
compiledSource: result.compiledSource,
|
|
140
|
+
})
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const body = renderToString(page);
|
|
144
|
+
|
|
145
|
+
const ctx: PageContext = { slug, filePath, frontmatter, content, config: docuConfig };
|
|
146
|
+
const headExtra = builder?.collectHead(ctx);
|
|
147
|
+
const bodyExtra = builder?.collectBody(ctx);
|
|
148
|
+
|
|
149
|
+
const depth = slug ? slug.split("/").length : 1;
|
|
150
|
+
const favicon = docuConfig.meta?.favicon || "/docs/assets/images/favicon.ico";
|
|
151
|
+
let html = htmlShell({
|
|
152
|
+
title,
|
|
153
|
+
description,
|
|
154
|
+
body,
|
|
155
|
+
favicon,
|
|
156
|
+
css: assetManifest.css,
|
|
157
|
+
js: assetManifest.js,
|
|
158
|
+
nonce,
|
|
159
|
+
themeCss: inlineThemeCss,
|
|
160
|
+
depth,
|
|
161
|
+
headExtra,
|
|
162
|
+
bodyExtra,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
if (builder) {
|
|
166
|
+
html = await builder.runTransformHtmlChain(html, ctx);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return html;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function copyDirectoryRecursive(src: string, dest: string): Promise<void> {
|
|
173
|
+
if (!existsSync(src)) return;
|
|
174
|
+
await mkdir(dest, { recursive: true });
|
|
175
|
+
const entries = await readdir(src, { withFileTypes: true });
|
|
176
|
+
for (const entry of entries) {
|
|
177
|
+
const srcPath = join(src, entry.name);
|
|
178
|
+
const destPath = join(dest, entry.name);
|
|
179
|
+
if (entry.isDirectory()) {
|
|
180
|
+
await copyDirectoryRecursive(srcPath, destPath);
|
|
181
|
+
} else {
|
|
182
|
+
await copyFile(srcPath, destPath);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function runBuild(): Promise<void> {
|
|
188
|
+
const docuConfig = loadDocuConfig();
|
|
189
|
+
const args = parseArgs();
|
|
190
|
+
|
|
191
|
+
logger.buildStart();
|
|
192
|
+
|
|
193
|
+
if (args.clean) {
|
|
194
|
+
const { rm } = await import("node:fs/promises");
|
|
195
|
+
try {
|
|
196
|
+
await rm(DIST_DIR, { recursive: true, force: true });
|
|
197
|
+
} catch (err) {
|
|
198
|
+
console.error("Failed to clean dist directory:", (err as Error).message);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
await mkdir(DIST_DIR, { recursive: true });
|
|
203
|
+
await mkdir(ASSETS_DIR, { recursive: true });
|
|
204
|
+
|
|
205
|
+
await copyDirectoryRecursive(DOCS_ASSETS_DIR, join(DIST_DIR, "docs", "assets"));
|
|
206
|
+
|
|
207
|
+
const mdxFiles = await scanMdxFiles(DOCS_DIR);
|
|
208
|
+
const cache = args.force ? {} : await readCache();
|
|
209
|
+
let built = 0;
|
|
210
|
+
let skipped = 0;
|
|
211
|
+
|
|
212
|
+
logger.bundleStart();
|
|
213
|
+
let t = performance.now();
|
|
214
|
+
assetManifest = await buildClientBundle();
|
|
215
|
+
logger.bundleDone(Math.round(performance.now() - t));
|
|
216
|
+
|
|
217
|
+
inlineThemeCss = computeInlineThemeCss();
|
|
218
|
+
|
|
219
|
+
const lastManifest = cache["__assets__"];
|
|
220
|
+
const assetsChanged =
|
|
221
|
+
!lastManifest || lastManifest.hash !== `${assetManifest.js}:${assetManifest.css}`;
|
|
222
|
+
if (assetsChanged) {
|
|
223
|
+
cache["__assets__"] = {
|
|
224
|
+
hash: `${assetManifest.js}:${assetManifest.css}`,
|
|
225
|
+
mtime: 0,
|
|
226
|
+
builtAt: Date.now(),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const pluginsConfig = docuConfig.plugins ?? [];
|
|
231
|
+
const builder = pluginsConfig.length > 0 ? new BuildPluginBuilder(docuConfig) : null;
|
|
232
|
+
if (builder) {
|
|
233
|
+
const plugins = await loadPlugins(pluginsConfig);
|
|
234
|
+
for (const plugin of plugins) {
|
|
235
|
+
await plugin.setup(builder);
|
|
236
|
+
}
|
|
237
|
+
await builder.runOnStart();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
logger.spinner.start("Building pages...");
|
|
241
|
+
t = performance.now();
|
|
242
|
+
|
|
243
|
+
const allRelPaths = mdxFiles.map((f) => f.absPath.replace(PROJECT_ROOT + "/", ""));
|
|
244
|
+
|
|
245
|
+
const indexMdxFull = join(DOCS_DIR, "index.mdx");
|
|
246
|
+
if (existsSync(indexMdxFull)) {
|
|
247
|
+
allRelPaths.push(indexMdxFull.replace(PROJECT_ROOT + "/", ""));
|
|
248
|
+
}
|
|
249
|
+
const gitDates = await getGitLastModifiedBatch(allRelPaths);
|
|
250
|
+
|
|
251
|
+
const CONCURRENCY = parseConcurrency();
|
|
252
|
+
const buildTasks = [];
|
|
253
|
+
const errors: string[] = [];
|
|
254
|
+
|
|
255
|
+
for (const file of mdxFiles) {
|
|
256
|
+
const rebuildDecision = shouldRebuild(file.path, file.mtime, cache);
|
|
257
|
+
|
|
258
|
+
if (rebuildDecision === "no") {
|
|
259
|
+
const outputPath = join(DIST_DIR, "docs", `${file.path}.html`);
|
|
260
|
+
if (existsSync(outputPath) && !assetsChanged) {
|
|
261
|
+
skipped++;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
let rawMdx: string;
|
|
267
|
+
try {
|
|
268
|
+
rawMdx = await readFile(file.absPath, "utf-8");
|
|
269
|
+
} catch (err) {
|
|
270
|
+
if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (rebuildDecision === "hash_check") {
|
|
275
|
+
const contentHash = hashContent(rawMdx);
|
|
276
|
+
const cached = cache[file.path];
|
|
277
|
+
if (cached && cached.hash === contentHash) {
|
|
278
|
+
if (!assetsChanged) {
|
|
279
|
+
const outputPath = join(DIST_DIR, "docs", `${file.path}.html`);
|
|
280
|
+
if (existsSync(outputPath)) {
|
|
281
|
+
cache[file.path] = { ...cached, mtime: file.mtime, builtAt: Date.now() };
|
|
282
|
+
skipped++;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const relPath = file.absPath.replace(PROJECT_ROOT + "/", "");
|
|
290
|
+
const capturedRawMdx = rawMdx;
|
|
291
|
+
const capturedFile = file;
|
|
292
|
+
|
|
293
|
+
buildTasks.push(async () => {
|
|
294
|
+
try {
|
|
295
|
+
const pageNonce = generateNonce();
|
|
296
|
+
const html = await renderDocsPage(
|
|
297
|
+
docuConfig,
|
|
298
|
+
capturedFile.path,
|
|
299
|
+
capturedRawMdx,
|
|
300
|
+
relPath,
|
|
301
|
+
gitDates,
|
|
302
|
+
builder,
|
|
303
|
+
pageNonce
|
|
304
|
+
);
|
|
305
|
+
const outputPath = join(DIST_DIR, "docs", `${capturedFile.path}.html`);
|
|
306
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
307
|
+
await writeFile(outputPath, html);
|
|
308
|
+
cache[capturedFile.path] = {
|
|
309
|
+
hash: hashContent(capturedRawMdx),
|
|
310
|
+
mtime: capturedFile.mtime,
|
|
311
|
+
builtAt: Date.now(),
|
|
312
|
+
};
|
|
313
|
+
built++;
|
|
314
|
+
} catch (err) {
|
|
315
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
316
|
+
errors.push(msg);
|
|
317
|
+
console.error(`\n\u274C ${msg}\n`);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
for (let i = 0; i < buildTasks.length; i += CONCURRENCY) {
|
|
323
|
+
await Promise.all(buildTasks.slice(i, i + CONCURRENCY).map((fn) => fn()));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
const indexMdxPath = join(DOCS_DIR, "index.mdx");
|
|
328
|
+
const indexRaw = await readFile(indexMdxPath, "utf-8");
|
|
329
|
+
const indexRelPath = indexMdxPath.replace(PROJECT_ROOT + "/", "");
|
|
330
|
+
const indexHtml = await renderDocsPage(
|
|
331
|
+
docuConfig,
|
|
332
|
+
"",
|
|
333
|
+
indexRaw,
|
|
334
|
+
indexRelPath,
|
|
335
|
+
gitDates,
|
|
336
|
+
builder,
|
|
337
|
+
generateNonce()
|
|
338
|
+
);
|
|
339
|
+
await mkdir(join(DIST_DIR, "docs"), { recursive: true });
|
|
340
|
+
await writeFile(join(DIST_DIR, "docs", "index.html"), indexHtml);
|
|
341
|
+
} catch (err) {
|
|
342
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
343
|
+
errors.push(`index.mdx: ${msg}`);
|
|
344
|
+
console.error(`\n\u274C Failed to build index: ${msg}\n`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const landingPage = React.createElement(IndexPage);
|
|
348
|
+
const landingFavicon = docuConfig.meta?.favicon || "/docs/assets/images/favicon.ico";
|
|
349
|
+
const landingHtml = htmlShell({
|
|
350
|
+
title: docuConfig.meta?.title || "DocuBook",
|
|
351
|
+
description: docuConfig.meta?.description || "",
|
|
352
|
+
body: renderToString(landingPage),
|
|
353
|
+
favicon: landingFavicon,
|
|
354
|
+
css: assetManifest.css,
|
|
355
|
+
js: assetManifest.js,
|
|
356
|
+
nonce: generateNonce(),
|
|
357
|
+
themeCss: inlineThemeCss,
|
|
358
|
+
});
|
|
359
|
+
await writeFile(join(DIST_DIR, "index.html"), landingHtml);
|
|
360
|
+
|
|
361
|
+
const notFoundPage = React.createElement(
|
|
362
|
+
DocsLayout,
|
|
363
|
+
{ repoUrl: docuConfig.repo?.url },
|
|
364
|
+
React.createElement(NotFoundPage)
|
|
365
|
+
);
|
|
366
|
+
const notFoundFavicon = docuConfig.meta?.favicon || "/docs/assets/images/favicon.ico";
|
|
367
|
+
const notFoundHtml = htmlShell({
|
|
368
|
+
title: "404 - Not Found",
|
|
369
|
+
description: "",
|
|
370
|
+
body: renderToString(notFoundPage),
|
|
371
|
+
favicon: notFoundFavicon,
|
|
372
|
+
css: assetManifest.css,
|
|
373
|
+
js: assetManifest.js,
|
|
374
|
+
nonce: generateNonce(),
|
|
375
|
+
themeCss: inlineThemeCss,
|
|
376
|
+
});
|
|
377
|
+
await writeFile(join(DIST_DIR, "404.html"), notFoundHtml);
|
|
378
|
+
|
|
379
|
+
logger.spinner.stop(
|
|
380
|
+
`Built ${built} pages (${skipped} cached) \x1b[90m(${Math.round(performance.now() - t)}ms)\x1b[0m`
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
if (builder) {
|
|
384
|
+
const pages: PageMeta[] = mdxFiles.map((f) => ({
|
|
385
|
+
slug: f.path,
|
|
386
|
+
title: f.path.split("/").pop() || f.path,
|
|
387
|
+
filePath: join(DOCS_DIR, f.path),
|
|
388
|
+
outputPath: join(DIST_DIR, "docs", `${f.path}.html`),
|
|
389
|
+
}));
|
|
390
|
+
await builder.runOnEnd(pages);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
logger.indexStart();
|
|
394
|
+
t = performance.now();
|
|
395
|
+
const indexCount = await generateSearchIndex();
|
|
396
|
+
logger.indexDone(indexCount, Math.round(performance.now() - t));
|
|
397
|
+
|
|
398
|
+
logger.routes();
|
|
399
|
+
console.log("");
|
|
400
|
+
|
|
401
|
+
const buildSummary = await generateBuildSummary(DIST_DIR);
|
|
402
|
+
if (buildSummary) {
|
|
403
|
+
console.log(buildSummary);
|
|
404
|
+
console.log("");
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
await writeCache(cache);
|
|
408
|
+
|
|
409
|
+
if (errors.length > 0) {
|
|
410
|
+
console.error(`\n\u274C Build completed with ${errors.length} error(s)\n`);
|
|
411
|
+
process.exit(1);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export async function runBuildCli(): Promise<void> {
|
|
416
|
+
try {
|
|
417
|
+
await initSentry();
|
|
418
|
+
await runBuild();
|
|
419
|
+
} catch (err) {
|
|
420
|
+
captureException(err);
|
|
421
|
+
console.error("Build failed:", err);
|
|
422
|
+
process.exit(1);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { runDeploy } from "./deploy.shared";
|
|
2
|
+
|
|
3
|
+
try {
|
|
4
|
+
await runDeploy();
|
|
5
|
+
} catch (err) {
|
|
6
|
+
console.error("Deploy failed:", err);
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
// See build.deno.ts — react-dom/server's browser build keeps Deno's event
|
|
10
|
+
// loop alive after the in-process build; force a clean exit.
|
|
11
|
+
process.exit(0);
|
|
@@ -0,0 +1,85 @@
|
|
|
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.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { writeFile, mkdir } from "node:fs/promises";
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { DIST_DIR, PROJECT_ROOT } from "./paths";
|
|
11
|
+
|
|
12
|
+
const WORKFLOW_DIR = join(PROJECT_ROOT, ".github/workflows");
|
|
13
|
+
const WORKFLOW_FILE = join(WORKFLOW_DIR, "deploy.yml");
|
|
14
|
+
|
|
15
|
+
export async function runDeploy(): Promise<void> {
|
|
16
|
+
console.log("📦 Building for production...\n");
|
|
17
|
+
|
|
18
|
+
process.env.NODE_ENV = "production";
|
|
19
|
+
const { runBuildCli } = await import("./build.impl");
|
|
20
|
+
await runBuildCli();
|
|
21
|
+
|
|
22
|
+
// Add .nojekyll
|
|
23
|
+
await writeFile(join(DIST_DIR, ".nojekyll"), "");
|
|
24
|
+
|
|
25
|
+
// Generate GitHub Actions workflow
|
|
26
|
+
if (!existsSync(WORKFLOW_FILE)) {
|
|
27
|
+
await mkdir(WORKFLOW_DIR, { recursive: true });
|
|
28
|
+
await writeFile(WORKFLOW_FILE, GITHUB_ACTIONS_WORKFLOW);
|
|
29
|
+
console.log("\n📄 Created .github/workflows/deploy.yml");
|
|
30
|
+
}
|
|
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
|
+
}
|
|
36
|
+
|
|
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
|
+
`;
|
package/.docu/node/deploy.ts
CHANGED
|
@@ -13,6 +13,13 @@ import { DIST_DIR, PROJECT_ROOT } from "./paths";
|
|
|
13
13
|
const WORKFLOW_DIR = join(PROJECT_ROOT, ".github/workflows");
|
|
14
14
|
const WORKFLOW_FILE = join(WORKFLOW_DIR, "deploy.yml");
|
|
15
15
|
|
|
16
|
+
const HEADERS_FILE = `/assets/*
|
|
17
|
+
Cache-Control: public, max-age=31536000, immutable
|
|
18
|
+
|
|
19
|
+
/assets/chunks/*
|
|
20
|
+
Cache-Control: public, max-age=31536000, immutable
|
|
21
|
+
`;
|
|
22
|
+
|
|
16
23
|
async function deploy() {
|
|
17
24
|
console.log("📦 Building for production...\n");
|
|
18
25
|
|
|
@@ -30,6 +37,10 @@ async function deploy() {
|
|
|
30
37
|
// Add .nojekyll
|
|
31
38
|
await writeFile(join(DIST_DIR, ".nojekyll"), "");
|
|
32
39
|
|
|
40
|
+
// _headers: long-cache immutable assets for Netlify/Cloudflare Pages.
|
|
41
|
+
// GitHub Pages ignores it (its CDN caches separately) — harmless to emit.
|
|
42
|
+
await writeFile(join(DIST_DIR, "_headers"), HEADERS_FILE);
|
|
43
|
+
|
|
33
44
|
// Generate GitHub Actions workflow
|
|
34
45
|
if (!existsSync(WORKFLOW_FILE)) {
|
|
35
46
|
await mkdir(WORKFLOW_DIR, { recursive: true });
|