@docubook/flame 2.0.0-beta.2 → 2.0.0-beta.4
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/Pagination.tsx +12 -1
- package/.docu/lib/build.deno.js +1 -1
- package/.docu/lib/{build.impl-BJsi28mD.js → build.impl-CT-YLyLn.js} +73 -25
- package/.docu/lib/build.impl-CT-YLyLn.js.map +1 -0
- package/.docu/lib/build.impl-cJPJeQ8r.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-D3UWafa6.js → deploy.shared-CmOzm3b5.js} +3 -3
- package/.docu/lib/{deploy.shared-D3UWafa6.js.map → deploy.shared-CmOzm3b5.js.map} +1 -1
- package/.docu/lib/{html.shared-DSn-7_6t.js → html.shared-WwQnhDty.js} +269 -34
- package/.docu/lib/html.shared-WwQnhDty.js.map +1 -0
- package/.docu/lib/{logger-CycvLCrQ.js → logger-Cz0CPMi8.js} +9 -15
- package/.docu/lib/logger-Cz0CPMi8.js.map +1 -0
- package/.docu/lib/{paths-BtOIPBQ9.js → paths-Bl2cdp9E.js} +27 -3
- package/.docu/lib/paths-Bl2cdp9E.js.map +1 -0
- package/.docu/lib/preview.deno.js +1 -1
- package/.docu/lib/{preview.impl-CXJS2tQS.js → preview.impl-BaYhTRBW.js} +4 -4
- package/.docu/lib/{preview.impl-CXJS2tQS.js.map → preview.impl-BaYhTRBW.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-CJuWimfN.js → server.impl-l8hXfV_0.js} +7 -7
- package/.docu/lib/server.impl-l8hXfV_0.js.map +1 -0
- package/.docu/lib/server.node.js +1 -1
- package/.docu/lib/{utils-DA17MyQG.js → utils-B_CoyKie.js} +28 -6
- package/.docu/lib/utils-B_CoyKie.js.map +1 -0
- package/.docu/node/build.impl.ts +115 -43
- package/.docu/node/build.ts +134 -44
- package/.docu/node/cache-key.ts +311 -0
- package/.docu/node/html.shared.ts +2 -1
- package/.docu/node/html.ts +2 -1
- package/.docu/node/hydrate.node.ts +8 -11
- package/.docu/node/hydrate.ts +13 -14
- package/.docu/node/mdx.ts +14 -2
- package/.docu/node/paths.ts +27 -1
- package/.docu/node/plugin-builder.ts +10 -1
- package/.docu/node/route.ts +19 -21
- package/.docu/node/security.ts +18 -1
- package/.docu/node/server-routes.ts +3 -3
- package/.docu/node/types.ts +23 -5
- package/.docu/node/utils.ts +22 -2
- package/bin/cli.js +56 -18
- package/package.json +11 -17
- package/.docu/lib/build.impl-BJsi28mD.js.map +0 -1
- package/.docu/lib/build.impl-yVLaa1eQ.js +0 -2
- package/.docu/lib/html.shared-DSn-7_6t.js.map +0 -1
- package/.docu/lib/logger-CycvLCrQ.js.map +0 -1
- package/.docu/lib/paths-BtOIPBQ9.js.map +0 -1
- package/.docu/lib/server.impl-CJuWimfN.js.map +0 -1
- package/.docu/lib/utils-DA17MyQG.js.map +0 -1
package/.docu/node/build.impl.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* The Node/Deno build entries call `runBuildCli()`.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { readFile, writeFile, mkdir, readdir, copyFile } from "node:fs/promises";
|
|
9
|
+
import { readFile, writeFile, mkdir, readdir, copyFile, rename, unlink } from "node:fs/promises";
|
|
10
10
|
import { existsSync } from "node:fs";
|
|
11
11
|
import { createHash } from "node:crypto";
|
|
12
12
|
import { join, dirname } from "node:path";
|
|
@@ -38,8 +38,17 @@ import { logger } from "./logger";
|
|
|
38
38
|
import { initSentry, captureException } from "./sentry";
|
|
39
39
|
import { loadPlugins } from "./plugin-loader";
|
|
40
40
|
import { BuildPluginBuilder } from "./plugin-builder";
|
|
41
|
-
import { scanMdxFiles } from "./utils";
|
|
42
|
-
import type { BuildCache, CliArgs } from "./types";
|
|
41
|
+
import { scanMdxFiles, resolveDocsIndexSource, DEFAULT_FAVICON } from "./utils";
|
|
42
|
+
import type { BuildCache, BuildCacheMeta, CliArgs } from "./types";
|
|
43
|
+
import { isCacheEntry } from "./types";
|
|
44
|
+
import {
|
|
45
|
+
BUILD_CACHE_VERSION,
|
|
46
|
+
atomicWriteFile,
|
|
47
|
+
hashMdxSources,
|
|
48
|
+
hookMemoryPressure,
|
|
49
|
+
runtimeStamp,
|
|
50
|
+
} from "./cache-key";
|
|
51
|
+
import { clearDerivedPageCaches } from "./mdx";
|
|
43
52
|
import { generateNonce, cspHeader } from "./security";
|
|
44
53
|
import type { PageMeta, PageContext } from "./plugin";
|
|
45
54
|
import { buildSeoMeta } from "./seo";
|
|
@@ -64,7 +73,12 @@ async function readCache(): Promise<BuildCache> {
|
|
|
64
73
|
try {
|
|
65
74
|
if (existsSync(CACHE_FILE)) {
|
|
66
75
|
const data = await readFile(CACHE_FILE, "utf-8");
|
|
67
|
-
|
|
76
|
+
const parsed = JSON.parse(data) as BuildCache;
|
|
77
|
+
const meta = parsed.__meta__ as BuildCacheMeta | undefined;
|
|
78
|
+
if (!meta || meta.version !== BUILD_CACHE_VERSION || meta.runtime !== runtimeStamp()) {
|
|
79
|
+
return {};
|
|
80
|
+
}
|
|
81
|
+
return parsed;
|
|
68
82
|
}
|
|
69
83
|
} catch (err) {
|
|
70
84
|
console.error("Failed to load build cache:", (err as Error).message);
|
|
@@ -72,8 +86,19 @@ async function readCache(): Promise<BuildCache> {
|
|
|
72
86
|
return {};
|
|
73
87
|
}
|
|
74
88
|
|
|
89
|
+
function stampCache(cache: BuildCache): void {
|
|
90
|
+
cache.__meta__ = {
|
|
91
|
+
hash: `${BUILD_CACHE_VERSION}:${runtimeStamp()}`,
|
|
92
|
+
mtime: 0,
|
|
93
|
+
builtAt: Date.now(),
|
|
94
|
+
version: BUILD_CACHE_VERSION,
|
|
95
|
+
runtime: runtimeStamp(),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
75
99
|
async function writeCache(cache: BuildCache): Promise<void> {
|
|
76
|
-
|
|
100
|
+
stampCache(cache);
|
|
101
|
+
await atomicWriteFile(writeFile, rename, unlink, CACHE_FILE, JSON.stringify(cache, null, 2));
|
|
77
102
|
}
|
|
78
103
|
|
|
79
104
|
function parseConcurrency(): number {
|
|
@@ -84,13 +109,40 @@ type RebuildDecision = "yes" | "hash_check" | "no";
|
|
|
84
109
|
|
|
85
110
|
function shouldRebuild(path: string, mtime: number, cache: BuildCache): RebuildDecision {
|
|
86
111
|
const cached = cache[path];
|
|
87
|
-
if (!cached) return "yes";
|
|
88
|
-
|
|
112
|
+
if (!isCacheEntry(cached)) return "yes";
|
|
113
|
+
// Any mtime drift vs the recorded one — a newer edit, or mtime moving
|
|
114
|
+
// backwards (rsync -a, cp -p, snapshot restore) — falls through to the
|
|
115
|
+
// hash check: hashing an unchanged file is cheap, serving a stale page
|
|
116
|
+
// is not. Only an untouched mtime skips without reading the file.
|
|
117
|
+
if (mtime !== cached.mtime) return "hash_check";
|
|
89
118
|
return "no";
|
|
90
119
|
}
|
|
91
120
|
|
|
92
121
|
let assetManifest = { js: "client.js", css: "client.css" };
|
|
93
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Reuse manifest.json on bundle cache hit; fall back to a full rebuild
|
|
125
|
+
* when the manifest is missing or malformed.
|
|
126
|
+
*/
|
|
127
|
+
async function resolveAssetManifest(
|
|
128
|
+
bundleHit: boolean,
|
|
129
|
+
mdxSources: Record<string, string>
|
|
130
|
+
): Promise<{ js: string; css: string }> {
|
|
131
|
+
if (!bundleHit) return buildClientBundle(mdxSources);
|
|
132
|
+
try {
|
|
133
|
+
const manifest = JSON.parse(await readFile(join(ASSETS_DIR, "manifest.json"), "utf-8")) as {
|
|
134
|
+
js?: string;
|
|
135
|
+
css?: string;
|
|
136
|
+
};
|
|
137
|
+
if (typeof manifest.js === "string" && typeof manifest.css === "string") {
|
|
138
|
+
return { js: manifest.js, css: manifest.css };
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
// corrupt/missing manifest — rebuild below
|
|
142
|
+
}
|
|
143
|
+
return buildClientBundle(mdxSources);
|
|
144
|
+
}
|
|
145
|
+
|
|
94
146
|
let inlineThemeCss: string | undefined;
|
|
95
147
|
|
|
96
148
|
async function renderDocsPage(
|
|
@@ -176,11 +228,11 @@ async function renderDocsPage(
|
|
|
176
228
|
const bodyExtra = builder?.collectBody(ctx);
|
|
177
229
|
|
|
178
230
|
const depth = slug ? slug.split("/").length : 1;
|
|
179
|
-
const favicon = docuConfig.meta?.favicon ||
|
|
231
|
+
const favicon = docuConfig.meta?.favicon || DEFAULT_FAVICON;
|
|
180
232
|
const seo = buildSeoMeta(docuConfig, frontmatter, slug || "");
|
|
181
233
|
// MDX content hydrates from the bundled ESM module (mdx-hydrate), not
|
|
182
234
|
// new Function — no 'unsafe-eval' needed in the CSP.
|
|
183
|
-
const csp = cspHeader(nonce);
|
|
235
|
+
const csp = nonce ? cspHeader(nonce) : undefined;
|
|
184
236
|
let html = htmlShell({
|
|
185
237
|
title,
|
|
186
238
|
description,
|
|
@@ -223,6 +275,8 @@ export async function runBuild(): Promise<void> {
|
|
|
223
275
|
const docuConfig = loadDocuConfig();
|
|
224
276
|
const args = parseArgs();
|
|
225
277
|
|
|
278
|
+
hookMemoryPressure(clearDerivedPageCaches);
|
|
279
|
+
|
|
226
280
|
logger.buildStart();
|
|
227
281
|
|
|
228
282
|
if (args.clean) {
|
|
@@ -287,15 +341,16 @@ export async function runBuild(): Promise<void> {
|
|
|
287
341
|
});
|
|
288
342
|
await Promise.all(prePassTasks);
|
|
289
343
|
|
|
290
|
-
// The docs root (index.mdx) renders with slug "" — mirror that
|
|
291
|
-
// index page hydrates too. Its render has its own try/catch; skip
|
|
292
|
-
|
|
293
|
-
|
|
344
|
+
// The docs root (index.mdx, or index.md) renders with slug "" — mirror that
|
|
345
|
+
// key so the index page hydrates too. Its render has its own try/catch; skip
|
|
346
|
+
// on error.
|
|
347
|
+
const indexSource = resolveDocsIndexSource(DOCS_DIR);
|
|
348
|
+
if (indexSource) {
|
|
294
349
|
try {
|
|
295
|
-
const indexRaw = await readFile(
|
|
350
|
+
const indexRaw = await readFile(indexSource, "utf-8");
|
|
296
351
|
let indexContent = indexRaw;
|
|
297
352
|
if (builder) {
|
|
298
|
-
const relPath =
|
|
353
|
+
const relPath = indexSource.replace(PROJECT_ROOT + "/", "");
|
|
299
354
|
const transformed = await builder.runOnLoad(relPath, indexContent);
|
|
300
355
|
if (transformed?.contents) indexContent = transformed.contents;
|
|
301
356
|
}
|
|
@@ -311,14 +366,20 @@ export async function runBuild(): Promise<void> {
|
|
|
311
366
|
|
|
312
367
|
logger.bundleStart();
|
|
313
368
|
let t = performance.now();
|
|
314
|
-
|
|
369
|
+
const bundleHash = hashMdxSources(mdxSources);
|
|
370
|
+
const lastBundle = cache["__bundle__"];
|
|
371
|
+
const bundleHit =
|
|
372
|
+
isCacheEntry(lastBundle) &&
|
|
373
|
+
lastBundle.hash === bundleHash &&
|
|
374
|
+
existsSync(join(ASSETS_DIR, "manifest.json"));
|
|
375
|
+
assetManifest = await resolveAssetManifest(bundleHit, mdxSources);
|
|
315
376
|
logger.bundleDone(Math.round(performance.now() - t));
|
|
316
377
|
|
|
317
378
|
inlineThemeCss = computeInlineThemeCss();
|
|
318
379
|
|
|
319
380
|
const lastManifest = cache["__assets__"];
|
|
320
381
|
const assetsChanged =
|
|
321
|
-
!lastManifest || lastManifest.hash !== `${assetManifest.js}:${assetManifest.css}`;
|
|
382
|
+
!isCacheEntry(lastManifest) || lastManifest.hash !== `${assetManifest.js}:${assetManifest.css}`;
|
|
322
383
|
if (assetsChanged) {
|
|
323
384
|
cache["__assets__"] = {
|
|
324
385
|
hash: `${assetManifest.js}:${assetManifest.css}`,
|
|
@@ -326,15 +387,17 @@ export async function runBuild(): Promise<void> {
|
|
|
326
387
|
builtAt: Date.now(),
|
|
327
388
|
};
|
|
328
389
|
}
|
|
390
|
+
if (!bundleHit) {
|
|
391
|
+
cache["__bundle__"] = { hash: bundleHash, mtime: 0, builtAt: Date.now() };
|
|
392
|
+
}
|
|
329
393
|
|
|
330
394
|
logger.spinner.start("Building pages...");
|
|
331
395
|
t = performance.now();
|
|
332
396
|
|
|
333
397
|
const allRelPaths = mdxFiles.map((f) => f.absPath.replace(PROJECT_ROOT + "/", ""));
|
|
334
398
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
allRelPaths.push(indexMdxFull.replace(PROJECT_ROOT + "/", ""));
|
|
399
|
+
if (indexSource) {
|
|
400
|
+
allRelPaths.push(indexSource.replace(PROJECT_ROOT + "/", ""));
|
|
338
401
|
}
|
|
339
402
|
const gitDates = await getGitLastModifiedBatch(allRelPaths);
|
|
340
403
|
|
|
@@ -366,7 +429,7 @@ export async function runBuild(): Promise<void> {
|
|
|
366
429
|
if (rebuildDecision === "hash_check") {
|
|
367
430
|
const contentHash = hashContent(rawMdx);
|
|
368
431
|
const cached = cache[file.path];
|
|
369
|
-
if (cached && cached.hash === contentHash) {
|
|
432
|
+
if (isCacheEntry(cached) && cached.hash === contentHash) {
|
|
370
433
|
if (!assetsChanged) {
|
|
371
434
|
const outputPath = join(DIST_DIR, "docs", `${file.path}.html`);
|
|
372
435
|
if (existsSync(outputPath)) {
|
|
@@ -415,30 +478,39 @@ export async function runBuild(): Promise<void> {
|
|
|
415
478
|
await Promise.all(buildTasks.slice(i, i + CONCURRENCY).map((fn) => fn()));
|
|
416
479
|
}
|
|
417
480
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
481
|
+
if (indexSource) {
|
|
482
|
+
try {
|
|
483
|
+
const indexRaw = await readFile(indexSource, "utf-8");
|
|
484
|
+
const indexRelPath = indexSource.replace(PROJECT_ROOT + "/", "");
|
|
485
|
+
const indexHtml = await renderDocsPage(
|
|
486
|
+
docuConfig,
|
|
487
|
+
"",
|
|
488
|
+
indexRaw,
|
|
489
|
+
indexRelPath,
|
|
490
|
+
gitDates,
|
|
491
|
+
builder,
|
|
492
|
+
generateNonce()
|
|
493
|
+
);
|
|
494
|
+
await mkdir(join(DIST_DIR, "docs"), { recursive: true });
|
|
495
|
+
await writeFile(join(DIST_DIR, "docs", "index.html"), indexHtml);
|
|
496
|
+
} catch (err) {
|
|
497
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
498
|
+
errors.push(`index: ${msg}`);
|
|
499
|
+
console.error(`\n❌ Failed to build index: ${msg}\n`);
|
|
500
|
+
}
|
|
501
|
+
} else {
|
|
502
|
+
const msg = "docs root index: docs/index.mdx (or docs/index.md) not found";
|
|
503
|
+
errors.push(`index: ${msg}`);
|
|
504
|
+
console.error(`\n❌ Failed to build index: ${msg}\n`);
|
|
437
505
|
}
|
|
438
506
|
|
|
439
507
|
const landingPage = React.createElement(IndexPage);
|
|
440
|
-
const landingFavicon = docuConfig.meta?.favicon ||
|
|
441
|
-
const landingSeo = buildSeoMeta(
|
|
508
|
+
const landingFavicon = docuConfig.meta?.favicon || DEFAULT_FAVICON;
|
|
509
|
+
const landingSeo = buildSeoMeta(
|
|
510
|
+
docuConfig,
|
|
511
|
+
docuConfig.meta as unknown as Record<string, unknown>,
|
|
512
|
+
""
|
|
513
|
+
);
|
|
442
514
|
const landingNonce = generateNonce();
|
|
443
515
|
const landingHtml = htmlShell({
|
|
444
516
|
title: docuConfig.meta?.title || "DocuBook",
|
|
@@ -459,7 +531,7 @@ export async function runBuild(): Promise<void> {
|
|
|
459
531
|
{ repoUrl: docuConfig.repo?.url },
|
|
460
532
|
React.createElement(NotFoundPage)
|
|
461
533
|
);
|
|
462
|
-
const notFoundFavicon = docuConfig.meta?.favicon ||
|
|
534
|
+
const notFoundFavicon = docuConfig.meta?.favicon || DEFAULT_FAVICON;
|
|
463
535
|
const notFoundNonce = generateNonce();
|
|
464
536
|
const notFoundHtml = htmlShell({
|
|
465
537
|
title: "404 - Not Found",
|
package/.docu/node/build.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFile, writeFile, mkdir, readdir, copyFile } from "node:fs/promises";
|
|
1
|
+
import { readFile, writeFile, mkdir, readdir, copyFile, rename, unlink } from "node:fs/promises";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { join, dirname } from "node:path";
|
|
@@ -30,9 +30,18 @@ import { logger } from "./logger";
|
|
|
30
30
|
import { initSentry, captureException } from "./sentry";
|
|
31
31
|
import { loadPlugins } from "./plugin-loader";
|
|
32
32
|
import { BuildPluginBuilder } from "./plugin-builder";
|
|
33
|
-
import { scanMdxFiles } from "./utils";
|
|
34
|
-
import type { BuildCache, CliArgs } from "./types";
|
|
35
|
-
import {
|
|
33
|
+
import { scanMdxFiles, resolveDocsIndexSource, DEFAULT_FAVICON } from "./utils";
|
|
34
|
+
import type { BuildCache, BuildCacheMeta, CliArgs } from "./types";
|
|
35
|
+
import { isCacheEntry } from "./types";
|
|
36
|
+
import {
|
|
37
|
+
BUILD_CACHE_VERSION,
|
|
38
|
+
atomicWriteFile,
|
|
39
|
+
hashMdxSources,
|
|
40
|
+
hookMemoryPressure,
|
|
41
|
+
runtimeStamp,
|
|
42
|
+
} from "./cache-key";
|
|
43
|
+
import { clearDerivedPageCaches } from "./mdx";
|
|
44
|
+
import { generateNonce, cspHeader } from "./security";
|
|
36
45
|
import type { PageMeta, PageContext } from "./plugin";
|
|
37
46
|
import { buildSeoMeta } from "./seo";
|
|
38
47
|
import DocsPage from "../pages/docs/[[...slug]]";
|
|
@@ -58,7 +67,15 @@ async function readCache(): Promise<BuildCache> {
|
|
|
58
67
|
try {
|
|
59
68
|
if (existsSync(CACHE_FILE)) {
|
|
60
69
|
const data = await readFile(CACHE_FILE, "utf-8");
|
|
61
|
-
|
|
70
|
+
const parsed = JSON.parse(data) as BuildCache;
|
|
71
|
+
// Toolchain upgrade (Bun 1.3 → 1.4, Tailwind CLI bump) changes build
|
|
72
|
+
// output without changing page content — a stale cache would false-hit.
|
|
73
|
+
// Discard when the version stamp or runtime fingerprint mismatches.
|
|
74
|
+
const meta = parsed.__meta__ as BuildCacheMeta | undefined;
|
|
75
|
+
if (!meta || meta.version !== BUILD_CACHE_VERSION || meta.runtime !== runtimeStamp()) {
|
|
76
|
+
return {};
|
|
77
|
+
}
|
|
78
|
+
return parsed;
|
|
62
79
|
}
|
|
63
80
|
} catch (err) {
|
|
64
81
|
console.error("Failed to load build cache:", (err as Error).message);
|
|
@@ -66,8 +83,21 @@ async function readCache(): Promise<BuildCache> {
|
|
|
66
83
|
return {};
|
|
67
84
|
}
|
|
68
85
|
|
|
86
|
+
/** Stamp the cache with the current toolchain fingerprint. */
|
|
87
|
+
function stampCache(cache: BuildCache): void {
|
|
88
|
+
cache.__meta__ = {
|
|
89
|
+
hash: `${BUILD_CACHE_VERSION}:${runtimeStamp()}`,
|
|
90
|
+
mtime: 0,
|
|
91
|
+
builtAt: Date.now(),
|
|
92
|
+
version: BUILD_CACHE_VERSION,
|
|
93
|
+
runtime: runtimeStamp(),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
69
97
|
async function writeCache(cache: BuildCache): Promise<void> {
|
|
70
|
-
|
|
98
|
+
stampCache(cache);
|
|
99
|
+
// Atomic tmp+rename: a crash mid-write never leaves a corrupt cache file.
|
|
100
|
+
await atomicWriteFile(writeFile, rename, unlink, CACHE_FILE, JSON.stringify(cache, null, 2));
|
|
71
101
|
}
|
|
72
102
|
|
|
73
103
|
export function parseConcurrency(): number {
|
|
@@ -78,13 +108,40 @@ type RebuildDecision = "yes" | "hash_check" | "no";
|
|
|
78
108
|
|
|
79
109
|
export function shouldRebuild(path: string, mtime: number, cache: BuildCache): RebuildDecision {
|
|
80
110
|
const cached = cache[path];
|
|
81
|
-
if (!cached) return "yes";
|
|
82
|
-
|
|
111
|
+
if (!isCacheEntry(cached)) return "yes";
|
|
112
|
+
// Any mtime drift vs the recorded one — a newer edit, or mtime moving
|
|
113
|
+
// backwards (rsync -a, cp -p, snapshot restore) — falls through to the
|
|
114
|
+
// hash check: hashing an unchanged file is cheap, serving a stale page
|
|
115
|
+
// is not. Only an untouched mtime skips without reading the file.
|
|
116
|
+
if (mtime !== cached.mtime) return "hash_check";
|
|
83
117
|
return "no";
|
|
84
118
|
}
|
|
85
119
|
|
|
86
120
|
let assetManifest = { js: "client.js", css: "client.css" };
|
|
87
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Reuse manifest.json on bundle cache hit; fall back to a full rebuild
|
|
124
|
+
* when the manifest is missing or malformed.
|
|
125
|
+
*/
|
|
126
|
+
async function resolveAssetManifest(
|
|
127
|
+
bundleHit: boolean,
|
|
128
|
+
mdxSources: Record<string, string>
|
|
129
|
+
): Promise<{ js: string; css: string }> {
|
|
130
|
+
if (!bundleHit) return buildClientBundle(mdxSources);
|
|
131
|
+
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 };
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
// corrupt/missing manifest — rebuild below
|
|
141
|
+
}
|
|
142
|
+
return buildClientBundle(mdxSources);
|
|
143
|
+
}
|
|
144
|
+
|
|
88
145
|
let inlineThemeCss: string | undefined;
|
|
89
146
|
|
|
90
147
|
async function renderDocsPage(
|
|
@@ -169,14 +226,18 @@ async function renderDocsPage(
|
|
|
169
226
|
const bodyExtra = builder?.collectBody(ctx);
|
|
170
227
|
|
|
171
228
|
const depth = slug ? slug.split("/").length : 1;
|
|
172
|
-
const favicon = docuConfig.meta?.favicon ||
|
|
229
|
+
const favicon = docuConfig.meta?.favicon || DEFAULT_FAVICON;
|
|
173
230
|
const seo = buildSeoMeta(docuConfig, frontmatter, slug || "");
|
|
231
|
+
// Parity with build.impl.ts: static hosts without header control (GitHub
|
|
232
|
+
// Pages) rely on the <meta> CSP for the per-page script policy.
|
|
233
|
+
const csp = nonce ? cspHeader(nonce) : undefined;
|
|
174
234
|
let html = htmlShell({
|
|
175
235
|
title,
|
|
176
236
|
description,
|
|
177
237
|
body,
|
|
178
238
|
favicon,
|
|
179
239
|
seo,
|
|
240
|
+
csp,
|
|
180
241
|
css: assetManifest.css,
|
|
181
242
|
js: assetManifest.js,
|
|
182
243
|
nonce,
|
|
@@ -211,6 +272,10 @@ async function copyDirectoryRecursive(src: string, dest: string): Promise<void>
|
|
|
211
272
|
async function build() {
|
|
212
273
|
const args = parseArgs();
|
|
213
274
|
|
|
275
|
+
// Bun 1.4 `process.on("memoryPressure")`: drop parsed page maps when the
|
|
276
|
+
// OS runs low on memory (long CI builds). No-op on older runtimes.
|
|
277
|
+
hookMemoryPressure(clearDerivedPageCaches);
|
|
278
|
+
|
|
214
279
|
logger.buildStart();
|
|
215
280
|
|
|
216
281
|
if (args.clean) {
|
|
@@ -275,15 +340,16 @@ async function build() {
|
|
|
275
340
|
});
|
|
276
341
|
await Promise.all(prePassTasks);
|
|
277
342
|
|
|
278
|
-
// The docs root (index.mdx) renders with slug "" — mirror that
|
|
279
|
-
// index page hydrates too. Its render has its own try/catch; skip
|
|
280
|
-
|
|
281
|
-
|
|
343
|
+
// The docs root (index.mdx, or index.md) renders with slug "" — mirror that
|
|
344
|
+
// key so the index page hydrates too. Its render has its own try/catch; skip
|
|
345
|
+
// on error.
|
|
346
|
+
const indexSource = resolveDocsIndexSource(DOCS_DIR);
|
|
347
|
+
if (indexSource) {
|
|
282
348
|
try {
|
|
283
|
-
const indexRaw = await readFile(
|
|
349
|
+
const indexRaw = await readFile(indexSource, "utf-8");
|
|
284
350
|
let indexContent = indexRaw;
|
|
285
351
|
if (builder) {
|
|
286
|
-
const relPath =
|
|
352
|
+
const relPath = indexSource.replace(PROJECT_ROOT + "/", "");
|
|
287
353
|
const transformed = await builder.runOnLoad(relPath, indexContent);
|
|
288
354
|
if (transformed?.contents) indexContent = transformed.contents;
|
|
289
355
|
}
|
|
@@ -299,14 +365,23 @@ async function build() {
|
|
|
299
365
|
|
|
300
366
|
logger.bundleStart();
|
|
301
367
|
let t = performance.now();
|
|
302
|
-
|
|
368
|
+
// Skip the JS bundle when compiled MDX sources are unchanged: the bundle
|
|
369
|
+
// is shared by every page, so its hash doubles as the content fingerprint.
|
|
370
|
+
// CSS still builds via its own content-keyed cache inside the hydrator.
|
|
371
|
+
const bundleHash = hashMdxSources(mdxSources);
|
|
372
|
+
const lastBundle = cache["__bundle__"];
|
|
373
|
+
const bundleHit =
|
|
374
|
+
isCacheEntry(lastBundle) &&
|
|
375
|
+
lastBundle.hash === bundleHash &&
|
|
376
|
+
existsSync(join(ASSETS_DIR, "manifest.json"));
|
|
377
|
+
assetManifest = await resolveAssetManifest(bundleHit, mdxSources);
|
|
303
378
|
logger.bundleDone(Math.round(performance.now() - t));
|
|
304
379
|
|
|
305
380
|
inlineThemeCss = computeInlineThemeCss();
|
|
306
381
|
|
|
307
382
|
const lastManifest = cache["__assets__"];
|
|
308
383
|
const assetsChanged =
|
|
309
|
-
!lastManifest || lastManifest.hash !== `${assetManifest.js}:${assetManifest.css}`;
|
|
384
|
+
!isCacheEntry(lastManifest) || lastManifest.hash !== `${assetManifest.js}:${assetManifest.css}`;
|
|
310
385
|
if (assetsChanged) {
|
|
311
386
|
cache["__assets__"] = {
|
|
312
387
|
hash: `${assetManifest.js}:${assetManifest.css}`,
|
|
@@ -314,15 +389,17 @@ async function build() {
|
|
|
314
389
|
builtAt: Date.now(),
|
|
315
390
|
};
|
|
316
391
|
}
|
|
392
|
+
if (!bundleHit) {
|
|
393
|
+
cache["__bundle__"] = { hash: bundleHash, mtime: 0, builtAt: Date.now() };
|
|
394
|
+
}
|
|
317
395
|
|
|
318
396
|
logger.spinner.start("Building pages...");
|
|
319
397
|
t = performance.now();
|
|
320
398
|
|
|
321
399
|
const allRelPaths = mdxFiles.map((f) => f.absPath.replace(PROJECT_ROOT + "/", ""));
|
|
322
400
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
allRelPaths.push(indexMdxFull.replace(PROJECT_ROOT + "/", ""));
|
|
401
|
+
if (indexSource) {
|
|
402
|
+
allRelPaths.push(indexSource.replace(PROJECT_ROOT + "/", ""));
|
|
326
403
|
}
|
|
327
404
|
const gitDates = await getGitLastModifiedBatch(allRelPaths);
|
|
328
405
|
|
|
@@ -354,7 +431,7 @@ async function build() {
|
|
|
354
431
|
if (rebuildDecision === "hash_check") {
|
|
355
432
|
const contentHash = hashContent(rawMdx);
|
|
356
433
|
const cached = cache[file.path];
|
|
357
|
-
if (cached && cached.hash === contentHash) {
|
|
434
|
+
if (isCacheEntry(cached) && cached.hash === contentHash) {
|
|
358
435
|
if (!assetsChanged) {
|
|
359
436
|
const outputPath = join(DIST_DIR, "docs", `${file.path}.html`);
|
|
360
437
|
if (existsSync(outputPath)) {
|
|
@@ -402,38 +479,49 @@ async function build() {
|
|
|
402
479
|
await Promise.all(buildTasks.slice(i, i + CONCURRENCY).map((fn) => fn()));
|
|
403
480
|
}
|
|
404
481
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
482
|
+
if (indexSource) {
|
|
483
|
+
try {
|
|
484
|
+
const indexRaw = await readFile(indexSource, "utf-8");
|
|
485
|
+
const indexRelPath = indexSource.replace(PROJECT_ROOT + "/", "");
|
|
486
|
+
const indexHtml = await renderDocsPage(
|
|
487
|
+
"",
|
|
488
|
+
indexRaw,
|
|
489
|
+
indexRelPath,
|
|
490
|
+
gitDates,
|
|
491
|
+
builder,
|
|
492
|
+
generateNonce()
|
|
493
|
+
);
|
|
494
|
+
await mkdir(join(DIST_DIR, "docs"), { recursive: true });
|
|
495
|
+
await writeFile(join(DIST_DIR, "docs", "index.html"), indexHtml);
|
|
496
|
+
} catch (err) {
|
|
497
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
498
|
+
errors.push(`index: ${msg}`);
|
|
499
|
+
console.error(`\n❌ Failed to build index: ${msg}\n`);
|
|
500
|
+
}
|
|
501
|
+
} else {
|
|
502
|
+
const msg = "docs root index: docs/index.mdx (or docs/index.md) not found";
|
|
503
|
+
errors.push(`index: ${msg}`);
|
|
504
|
+
console.error(`\n❌ Failed to build index: ${msg}\n`);
|
|
423
505
|
}
|
|
424
506
|
|
|
425
507
|
const landingPage = React.createElement(IndexPage);
|
|
426
|
-
const landingFavicon = docuConfig.meta?.favicon ||
|
|
427
|
-
const landingSeo = buildSeoMeta(
|
|
508
|
+
const landingFavicon = docuConfig.meta?.favicon || DEFAULT_FAVICON;
|
|
509
|
+
const landingSeo = buildSeoMeta(
|
|
510
|
+
docuConfig,
|
|
511
|
+
docuConfig.meta as unknown as Record<string, unknown>,
|
|
512
|
+
""
|
|
513
|
+
);
|
|
514
|
+
const landingNonce = generateNonce();
|
|
428
515
|
const landingHtml = htmlShell({
|
|
429
516
|
title: docuConfig.meta?.title || "DocuBook",
|
|
430
517
|
description: docuConfig.meta?.description || "",
|
|
431
518
|
body: renderToString(landingPage),
|
|
432
519
|
favicon: landingFavicon,
|
|
433
520
|
seo: landingSeo,
|
|
521
|
+
csp: cspHeader(landingNonce),
|
|
434
522
|
css: assetManifest.css,
|
|
435
523
|
js: assetManifest.js,
|
|
436
|
-
nonce:
|
|
524
|
+
nonce: landingNonce,
|
|
437
525
|
themeCss: inlineThemeCss,
|
|
438
526
|
});
|
|
439
527
|
await writeFile(join(DIST_DIR, "index.html"), landingHtml);
|
|
@@ -443,16 +531,18 @@ async function build() {
|
|
|
443
531
|
{ repoUrl: docuConfig.repo?.url },
|
|
444
532
|
React.createElement(NotFoundPage)
|
|
445
533
|
);
|
|
446
|
-
const notFoundFavicon = docuConfig.meta?.favicon ||
|
|
534
|
+
const notFoundFavicon = docuConfig.meta?.favicon || DEFAULT_FAVICON;
|
|
535
|
+
const notFoundNonce = generateNonce();
|
|
447
536
|
const notFoundHtml = htmlShell({
|
|
448
537
|
title: "404 - Not Found",
|
|
449
538
|
description: "",
|
|
450
539
|
body: renderToString(notFoundPage),
|
|
451
540
|
favicon: notFoundFavicon,
|
|
452
541
|
headExtra: ['<meta name="robots" content="noindex,follow">'],
|
|
542
|
+
csp: cspHeader(notFoundNonce),
|
|
453
543
|
css: assetManifest.css,
|
|
454
544
|
js: assetManifest.js,
|
|
455
|
-
nonce:
|
|
545
|
+
nonce: notFoundNonce,
|
|
456
546
|
themeCss: inlineThemeCss,
|
|
457
547
|
// Served as the static-host fallback at ANY requested path — relative
|
|
458
548
|
// depth can never be right there, so use root-absolute asset URLs.
|