@docubook/flame 2.0.2 → 2.1.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/Context.tsx +6 -5
- package/.docu/components/DocsLayout.tsx +7 -4
- package/.docu/components/Menu.tsx +10 -9
- package/.docu/components/Navbar.tsx +24 -22
- package/.docu/components/Pagination.tsx +3 -2
- package/.docu/components/Search.tsx +6 -1
- package/.docu/components/Sidebar.tsx +22 -21
- package/.docu/components/Sublink.tsx +3 -3
- package/.docu/lib/build.deno.js +1 -1
- package/.docu/lib/{build.impl-CA7XPLz0.js → build.impl-DK485Nx1.js} +60 -31
- package/.docu/lib/build.impl-DK485Nx1.js.map +1 -0
- package/.docu/lib/build.impl-Dmybgcj2.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-DjD3qs7t.js → deploy.shared-BW2KXFXs.js} +27 -8
- package/.docu/lib/deploy.shared-BW2KXFXs.js.map +1 -0
- package/.docu/lib/{html.shared-CsEFmy_Y.js → html.shared-RPCM1y5Q.js} +264 -73
- package/.docu/lib/html.shared-RPCM1y5Q.js.map +1 -0
- package/.docu/lib/{logger-_e2TNmEq.js → logger-DYFD47f8.js} +3 -3
- package/.docu/lib/{logger-_e2TNmEq.js.map → logger-DYFD47f8.js.map} +1 -1
- package/.docu/lib/paths-CHXABfXa.js +332 -0
- package/.docu/lib/paths-CHXABfXa.js.map +1 -0
- package/.docu/lib/preview.deno.js +1 -1
- package/.docu/lib/{preview.impl-DiTu2Coo.js → preview.impl-DMXVsGs0.js} +4 -4
- package/.docu/lib/preview.impl-DMXVsGs0.js.map +1 -0
- package/.docu/lib/preview.node.js +1 -1
- package/.docu/lib/{utils-B_CoyKie.js → server-utils-BYWB7A6T.js} +3 -21
- package/.docu/lib/server-utils-BYWB7A6T.js.map +1 -0
- package/.docu/lib/server.deno.js +1 -1
- package/.docu/lib/{server.impl-DPBnKdIh.js → server.impl-DcyZhPA_.js} +24 -19
- package/.docu/lib/server.impl-DcyZhPA_.js.map +1 -0
- package/.docu/lib/server.node.js +1 -1
- package/.docu/node/base-path.ts +306 -0
- package/.docu/node/build.impl.ts +50 -17
- package/.docu/node/build.ts +50 -17
- package/.docu/node/cache-key.ts +92 -4
- package/.docu/node/client-routes.ts +16 -1
- package/.docu/node/deploy.shared.ts +51 -15
- package/.docu/node/deploy.ts +7 -3
- package/.docu/node/html.shared.ts +33 -3
- package/.docu/node/html.ts +18 -3
- package/.docu/node/hydrate.node.ts +6 -0
- package/.docu/node/hydrate.ts +16 -1
- package/.docu/node/mdx.ts +102 -23
- package/.docu/node/paths.ts +115 -0
- package/.docu/node/preview.impl.ts +1 -1
- package/.docu/node/preview.ts +1 -1
- package/.docu/node/search-indexer.ts +20 -3
- package/.docu/node/seo.ts +41 -7
- package/.docu/node/server-routes.ts +34 -11
- package/.docu/node/server-utils.ts +152 -0
- package/.docu/node/server.impl.ts +22 -12
- package/.docu/node/server.ts +23 -27
- package/.docu/node/types.ts +13 -0
- package/.docu/node/utils.ts +56 -149
- package/.docu/pages/404.tsx +2 -1
- package/.docu/pages/index.tsx +6 -13
- package/bin/cli.js +5 -0
- package/docu.schema.json +10 -1
- package/package.json +5 -5
- package/template/docu.json +1 -0
- package/.docu/lib/build.impl-C97QWe1I.js +0 -2
- package/.docu/lib/build.impl-CA7XPLz0.js.map +0 -1
- package/.docu/lib/deploy.shared-DjD3qs7t.js.map +0 -1
- package/.docu/lib/html.shared-CsEFmy_Y.js.map +0 -1
- package/.docu/lib/paths-Cbz-dj4s.js +0 -72
- package/.docu/lib/paths-Cbz-dj4s.js.map +0 -1
- package/.docu/lib/preview.impl-DiTu2Coo.js.map +0 -1
- package/.docu/lib/server.impl-DPBnKdIh.js.map +0 -1
- package/.docu/lib/utils-B_CoyKie.js.map +0 -1
package/.docu/node/build.impl.ts
CHANGED
|
@@ -25,12 +25,17 @@ import {
|
|
|
25
25
|
import {
|
|
26
26
|
DOCS_DIR,
|
|
27
27
|
DIST_DIR,
|
|
28
|
+
DOCS_OUT_DIR,
|
|
28
29
|
ASSETS_DIR,
|
|
29
30
|
CACHE_FILE,
|
|
30
31
|
DOCS_ASSETS_DIR,
|
|
31
32
|
PROJECT_ROOT,
|
|
32
33
|
PAGES_DIR,
|
|
33
34
|
loadDocuConfig,
|
|
35
|
+
servedBasePath,
|
|
36
|
+
docsDepth,
|
|
37
|
+
deployPath,
|
|
38
|
+
assertValidBasePath,
|
|
34
39
|
} from "./paths";
|
|
35
40
|
import { htmlShell } from "./html.shared";
|
|
36
41
|
import { generateSearchIndex } from "./search-indexer";
|
|
@@ -39,7 +44,8 @@ import { logger } from "./logger";
|
|
|
39
44
|
import { initSentry, captureException } from "./sentry";
|
|
40
45
|
import { loadPlugins } from "./plugin-loader";
|
|
41
46
|
import { BuildPluginBuilder } from "./plugin-builder";
|
|
42
|
-
import { scanMdxFiles, resolveDocsIndexSource
|
|
47
|
+
import { scanMdxFiles, resolveDocsIndexSource } from "./server-utils";
|
|
48
|
+
import { defaultFavicon } from "./paths";
|
|
43
49
|
import type { AssetManifest, BuildCache, BuildCacheMeta, CliArgs } from "./types";
|
|
44
50
|
import { isCacheEntry } from "./types";
|
|
45
51
|
import {
|
|
@@ -47,6 +53,7 @@ import {
|
|
|
47
53
|
atomicWriteFile,
|
|
48
54
|
hashMdxSources,
|
|
49
55
|
hookMemoryPressure,
|
|
56
|
+
renderToolchainStamp,
|
|
50
57
|
runtimeStamp,
|
|
51
58
|
} from "./cache-key";
|
|
52
59
|
import { clearDerivedPageCaches } from "./mdx";
|
|
@@ -76,7 +83,15 @@ async function readCache(): Promise<BuildCache> {
|
|
|
76
83
|
const data = await readFile(CACHE_FILE, "utf-8");
|
|
77
84
|
const parsed = JSON.parse(data) as BuildCache;
|
|
78
85
|
const meta = parsed.__meta__ as BuildCacheMeta | undefined;
|
|
79
|
-
|
|
86
|
+
// `render` covers the framework's own rendering sources: a template or
|
|
87
|
+
// meta-tag edit changes output without touching any MDX file, so an
|
|
88
|
+
// older cache must not be trusted after one.
|
|
89
|
+
if (
|
|
90
|
+
!meta ||
|
|
91
|
+
meta.version !== BUILD_CACHE_VERSION ||
|
|
92
|
+
meta.runtime !== runtimeStamp() ||
|
|
93
|
+
meta.render !== renderToolchainStamp()
|
|
94
|
+
) {
|
|
80
95
|
return {};
|
|
81
96
|
}
|
|
82
97
|
return parsed;
|
|
@@ -94,6 +109,7 @@ function stampCache(cache: BuildCache): void {
|
|
|
94
109
|
builtAt: Date.now(),
|
|
95
110
|
version: BUILD_CACHE_VERSION,
|
|
96
111
|
runtime: runtimeStamp(),
|
|
112
|
+
render: renderToolchainStamp(),
|
|
97
113
|
};
|
|
98
114
|
}
|
|
99
115
|
|
|
@@ -245,8 +261,8 @@ async function renderDocsPage(
|
|
|
245
261
|
const headExtra = builder?.collectHead(ctx);
|
|
246
262
|
const bodyExtra = builder?.collectBody(ctx);
|
|
247
263
|
|
|
248
|
-
const depth = slug
|
|
249
|
-
const favicon = docuConfig.meta?.favicon ||
|
|
264
|
+
const depth = docsDepth(slug, servedBasePath());
|
|
265
|
+
const favicon = docuConfig.meta?.favicon || defaultFavicon();
|
|
250
266
|
const seo = buildSeoMeta(docuConfig, frontmatter, slug || "");
|
|
251
267
|
// MDX content hydrates from the bundled ESM module (mdx-hydrate), not
|
|
252
268
|
// new Function — no 'unsafe-eval' needed in the CSP.
|
|
@@ -263,6 +279,7 @@ async function renderDocsPage(
|
|
|
263
279
|
nonce,
|
|
264
280
|
themeCss: inlineThemeCss,
|
|
265
281
|
depth,
|
|
282
|
+
basePath: servedBasePath(),
|
|
266
283
|
headExtra,
|
|
267
284
|
bodyExtra,
|
|
268
285
|
});
|
|
@@ -295,6 +312,11 @@ export async function runBuild(): Promise<void> {
|
|
|
295
312
|
|
|
296
313
|
hookMemoryPressure(clearDerivedPageCaches);
|
|
297
314
|
|
|
315
|
+
// Normalized prefixes (case, whitespace, unservable characters) and values
|
|
316
|
+
// that cannot be honored would otherwise only surface after the whole build —
|
|
317
|
+
// report them, or stop before anything is written.
|
|
318
|
+
for (const warning of assertValidBasePath()) logger.warn(warning.message);
|
|
319
|
+
|
|
298
320
|
logger.buildStart();
|
|
299
321
|
|
|
300
322
|
if (args.clean) {
|
|
@@ -309,7 +331,7 @@ export async function runBuild(): Promise<void> {
|
|
|
309
331
|
await mkdir(DIST_DIR, { recursive: true });
|
|
310
332
|
await mkdir(ASSETS_DIR, { recursive: true });
|
|
311
333
|
|
|
312
|
-
await copyDirectoryRecursive(DOCS_ASSETS_DIR, join(
|
|
334
|
+
await copyDirectoryRecursive(DOCS_ASSETS_DIR, join(DOCS_OUT_DIR, "assets"));
|
|
313
335
|
|
|
314
336
|
const mdxFiles = await scanMdxFiles(DOCS_DIR);
|
|
315
337
|
const cache = args.force ? {} : await readCache();
|
|
@@ -363,6 +385,13 @@ export async function runBuild(): Promise<void> {
|
|
|
363
385
|
// key so the index page hydrates too. Its render has its own try/catch; skip
|
|
364
386
|
// on error.
|
|
365
387
|
const indexSource = resolveDocsIndexSource(DOCS_DIR);
|
|
388
|
+
const docsIndexPath = join(DOCS_OUT_DIR, "index.html");
|
|
389
|
+
// If the source was removed since the previous build, don't ship stale docs
|
|
390
|
+
// output. At the deployment root, this path belongs to the landing page.
|
|
391
|
+
if (!indexSource && DOCS_OUT_DIR !== DIST_DIR) {
|
|
392
|
+
const { rm } = await import("node:fs/promises");
|
|
393
|
+
await rm(docsIndexPath, { force: true });
|
|
394
|
+
}
|
|
366
395
|
if (indexSource) {
|
|
367
396
|
try {
|
|
368
397
|
const indexRaw = await readFile(indexSource, "utf-8");
|
|
@@ -426,7 +455,7 @@ export async function runBuild(): Promise<void> {
|
|
|
426
455
|
const rebuildDecision = shouldRebuild(file.path, file.mtime, cache);
|
|
427
456
|
|
|
428
457
|
if (rebuildDecision === "no" && !builder) {
|
|
429
|
-
const outputPath = join(
|
|
458
|
+
const outputPath = join(DOCS_OUT_DIR, `${file.path}.html`);
|
|
430
459
|
if (existsSync(outputPath) && !assetsChanged) {
|
|
431
460
|
skipped++;
|
|
432
461
|
continue;
|
|
@@ -448,7 +477,7 @@ export async function runBuild(): Promise<void> {
|
|
|
448
477
|
const cached = cache[file.path];
|
|
449
478
|
if (isCacheEntry(cached) && cached.hash === contentHash) {
|
|
450
479
|
if (!assetsChanged) {
|
|
451
|
-
const outputPath = join(
|
|
480
|
+
const outputPath = join(DOCS_OUT_DIR, `${file.path}.html`);
|
|
452
481
|
if (existsSync(outputPath)) {
|
|
453
482
|
cache[file.path] = { ...cached, mtime: file.mtime, builtAt: Date.now() };
|
|
454
483
|
skipped++;
|
|
@@ -474,7 +503,7 @@ export async function runBuild(): Promise<void> {
|
|
|
474
503
|
builder,
|
|
475
504
|
pageNonce
|
|
476
505
|
);
|
|
477
|
-
const outputPath = join(
|
|
506
|
+
const outputPath = join(DOCS_OUT_DIR, `${capturedFile.path}.html`);
|
|
478
507
|
await mkdir(dirname(outputPath), { recursive: true });
|
|
479
508
|
await writeFile(outputPath, html);
|
|
480
509
|
cache[capturedFile.path] = {
|
|
@@ -508,21 +537,19 @@ export async function runBuild(): Promise<void> {
|
|
|
508
537
|
builder,
|
|
509
538
|
generateNonce()
|
|
510
539
|
);
|
|
511
|
-
|
|
512
|
-
|
|
540
|
+
if (DOCS_OUT_DIR !== DIST_DIR) {
|
|
541
|
+
await mkdir(DOCS_OUT_DIR, { recursive: true });
|
|
542
|
+
await writeFile(docsIndexPath, indexHtml);
|
|
543
|
+
}
|
|
513
544
|
} catch (err) {
|
|
514
545
|
const msg = err instanceof Error ? err.message : String(err);
|
|
515
546
|
errors.push(`index: ${msg}`);
|
|
516
547
|
console.error(`\n❌ Failed to build index: ${msg}\n`);
|
|
517
548
|
}
|
|
518
|
-
} else {
|
|
519
|
-
const msg = "docs root index: docs/index.mdx (or docs/index.md) not found";
|
|
520
|
-
errors.push(`index: ${msg}`);
|
|
521
|
-
console.error(`\n❌ Failed to build index: ${msg}\n`);
|
|
522
549
|
}
|
|
523
550
|
|
|
524
551
|
const landingPage = React.createElement(IndexPage);
|
|
525
|
-
const landingFavicon = docuConfig.meta?.favicon ||
|
|
552
|
+
const landingFavicon = docuConfig.meta?.favicon || defaultFavicon();
|
|
526
553
|
const landingSeo = buildSeoMeta(
|
|
527
554
|
docuConfig,
|
|
528
555
|
docuConfig.meta as unknown as Record<string, unknown>,
|
|
@@ -548,14 +575,18 @@ export async function runBuild(): Promise<void> {
|
|
|
548
575
|
js: landingContext.assets.js,
|
|
549
576
|
nonce: landingNonce,
|
|
550
577
|
themeCss: inlineThemeCss,
|
|
578
|
+
basePath: servedBasePath(),
|
|
551
579
|
headExtra: builder?.collectHead(landingContext),
|
|
552
580
|
bodyExtra: builder?.collectBody(landingContext),
|
|
553
581
|
});
|
|
554
582
|
if (builder) landingHtml = await builder.runTransformHtmlChain(landingHtml, landingContext);
|
|
583
|
+
// The landing page always owns `/`. At a root deployment the docs index
|
|
584
|
+
// would collide with it, so the index write above is skipped there — the
|
|
585
|
+
// docs root page simply does not exist.
|
|
555
586
|
await writeFile(join(DIST_DIR, "index.html"), landingHtml);
|
|
556
587
|
|
|
557
588
|
const notFoundPage = React.createElement(NotFoundPage);
|
|
558
|
-
const notFoundFavicon = docuConfig.meta?.favicon ||
|
|
589
|
+
const notFoundFavicon = docuConfig.meta?.favicon || defaultFavicon();
|
|
559
590
|
const notFoundNonce = generateNonce();
|
|
560
591
|
const notFoundContext: PageContext = {
|
|
561
592
|
pageType: "notFound",
|
|
@@ -583,6 +614,8 @@ export async function runBuild(): Promise<void> {
|
|
|
583
614
|
// Served as the static-host fallback at ANY requested path — relative
|
|
584
615
|
// depth can never be right there, so use root-absolute asset URLs.
|
|
585
616
|
absoluteAssets: true,
|
|
617
|
+
basePath: servedBasePath(),
|
|
618
|
+
deployPath: deployPath(),
|
|
586
619
|
});
|
|
587
620
|
if (builder) notFoundHtml = await builder.runTransformHtmlChain(notFoundHtml, notFoundContext);
|
|
588
621
|
await writeFile(join(DIST_DIR, "404.html"), notFoundHtml);
|
|
@@ -596,7 +629,7 @@ export async function runBuild(): Promise<void> {
|
|
|
596
629
|
slug: f.path,
|
|
597
630
|
title: f.path.split("/").pop() || f.path,
|
|
598
631
|
filePath: join(DOCS_DIR, f.path),
|
|
599
|
-
outputPath: join(
|
|
632
|
+
outputPath: join(DOCS_OUT_DIR, `${f.path}.html`),
|
|
600
633
|
}));
|
|
601
634
|
await builder.runOnEnd(pages, { assetManifest, outDir: DIST_DIR });
|
|
602
635
|
}
|
package/.docu/node/build.ts
CHANGED
|
@@ -17,12 +17,17 @@ import {
|
|
|
17
17
|
import {
|
|
18
18
|
DOCS_DIR,
|
|
19
19
|
DIST_DIR,
|
|
20
|
+
DOCS_OUT_DIR,
|
|
20
21
|
ASSETS_DIR,
|
|
21
22
|
CACHE_FILE,
|
|
22
23
|
DOCS_ASSETS_DIR,
|
|
23
24
|
PROJECT_ROOT,
|
|
24
25
|
PAGES_DIR,
|
|
25
26
|
loadDocuConfig,
|
|
27
|
+
servedBasePath,
|
|
28
|
+
docsDepth,
|
|
29
|
+
deployPath,
|
|
30
|
+
assertValidBasePath,
|
|
26
31
|
} from "./paths";
|
|
27
32
|
import { htmlShell } from "./html";
|
|
28
33
|
import { generateSearchIndex } from "./search-indexer";
|
|
@@ -31,7 +36,8 @@ import { logger } from "./logger";
|
|
|
31
36
|
import { initSentry, captureException } from "./sentry";
|
|
32
37
|
import { loadPlugins } from "./plugin-loader";
|
|
33
38
|
import { BuildPluginBuilder } from "./plugin-builder";
|
|
34
|
-
import { scanMdxFiles, resolveDocsIndexSource
|
|
39
|
+
import { scanMdxFiles, resolveDocsIndexSource } from "./server-utils";
|
|
40
|
+
import { defaultFavicon } from "./paths";
|
|
35
41
|
import type { AssetManifest, BuildCache, BuildCacheMeta, CliArgs } from "./types";
|
|
36
42
|
import { isCacheEntry } from "./types";
|
|
37
43
|
import {
|
|
@@ -39,6 +45,7 @@ import {
|
|
|
39
45
|
atomicWriteFile,
|
|
40
46
|
hashMdxSources,
|
|
41
47
|
hookMemoryPressure,
|
|
48
|
+
renderToolchainStamp,
|
|
42
49
|
runtimeStamp,
|
|
43
50
|
} from "./cache-key";
|
|
44
51
|
import { clearDerivedPageCaches } from "./mdx";
|
|
@@ -73,7 +80,15 @@ async function readCache(): Promise<BuildCache> {
|
|
|
73
80
|
// output without changing page content — a stale cache would false-hit.
|
|
74
81
|
// Discard when the version stamp or runtime fingerprint mismatches.
|
|
75
82
|
const meta = parsed.__meta__ as BuildCacheMeta | undefined;
|
|
76
|
-
|
|
83
|
+
// `render` covers the framework's own rendering sources: a template or
|
|
84
|
+
// meta-tag edit changes output without touching any MDX file, so an
|
|
85
|
+
// older cache must not be trusted after one.
|
|
86
|
+
if (
|
|
87
|
+
!meta ||
|
|
88
|
+
meta.version !== BUILD_CACHE_VERSION ||
|
|
89
|
+
meta.runtime !== runtimeStamp() ||
|
|
90
|
+
meta.render !== renderToolchainStamp()
|
|
91
|
+
) {
|
|
77
92
|
return {};
|
|
78
93
|
}
|
|
79
94
|
return parsed;
|
|
@@ -92,6 +107,7 @@ function stampCache(cache: BuildCache): void {
|
|
|
92
107
|
builtAt: Date.now(),
|
|
93
108
|
version: BUILD_CACHE_VERSION,
|
|
94
109
|
runtime: runtimeStamp(),
|
|
110
|
+
render: renderToolchainStamp(),
|
|
95
111
|
};
|
|
96
112
|
}
|
|
97
113
|
|
|
@@ -243,8 +259,8 @@ async function renderDocsPage(
|
|
|
243
259
|
const headExtra = builder?.collectHead(ctx);
|
|
244
260
|
const bodyExtra = builder?.collectBody(ctx);
|
|
245
261
|
|
|
246
|
-
const depth = slug
|
|
247
|
-
const favicon = docuConfig.meta?.favicon ||
|
|
262
|
+
const depth = docsDepth(slug, servedBasePath());
|
|
263
|
+
const favicon = docuConfig.meta?.favicon || defaultFavicon();
|
|
248
264
|
const seo = buildSeoMeta(docuConfig, frontmatter, slug || "");
|
|
249
265
|
// Parity with build.impl.ts: static hosts without header control (GitHub
|
|
250
266
|
// Pages) rely on the <meta> CSP for the per-page script policy.
|
|
@@ -261,6 +277,7 @@ async function renderDocsPage(
|
|
|
261
277
|
nonce,
|
|
262
278
|
themeCss: inlineThemeCss,
|
|
263
279
|
depth,
|
|
280
|
+
basePath: servedBasePath(),
|
|
264
281
|
headExtra,
|
|
265
282
|
bodyExtra,
|
|
266
283
|
});
|
|
@@ -294,6 +311,11 @@ async function build() {
|
|
|
294
311
|
// OS runs low on memory (long CI builds). No-op on older runtimes.
|
|
295
312
|
hookMemoryPressure(clearDerivedPageCaches);
|
|
296
313
|
|
|
314
|
+
// Normalized prefixes (case, whitespace, unservable characters) and values
|
|
315
|
+
// that cannot be honored would otherwise only surface after the whole build —
|
|
316
|
+
// report them, or stop before anything is written.
|
|
317
|
+
for (const warning of assertValidBasePath()) logger.warn(warning.message);
|
|
318
|
+
|
|
297
319
|
logger.buildStart();
|
|
298
320
|
|
|
299
321
|
if (args.clean) {
|
|
@@ -308,7 +330,7 @@ async function build() {
|
|
|
308
330
|
await mkdir(DIST_DIR, { recursive: true });
|
|
309
331
|
await mkdir(ASSETS_DIR, { recursive: true });
|
|
310
332
|
|
|
311
|
-
await copyDirectoryRecursive(DOCS_ASSETS_DIR, join(
|
|
333
|
+
await copyDirectoryRecursive(DOCS_ASSETS_DIR, join(DOCS_OUT_DIR, "assets"));
|
|
312
334
|
|
|
313
335
|
const mdxFiles = await scanMdxFiles(DOCS_DIR);
|
|
314
336
|
const cache = args.force ? {} : await readCache();
|
|
@@ -362,6 +384,13 @@ async function build() {
|
|
|
362
384
|
// key so the index page hydrates too. Its render has its own try/catch; skip
|
|
363
385
|
// on error.
|
|
364
386
|
const indexSource = resolveDocsIndexSource(DOCS_DIR);
|
|
387
|
+
const docsIndexPath = join(DOCS_OUT_DIR, "index.html");
|
|
388
|
+
// If the source was removed since the previous build, don't ship stale docs
|
|
389
|
+
// output. At the deployment root, this path belongs to the landing page.
|
|
390
|
+
if (!indexSource && DOCS_OUT_DIR !== DIST_DIR) {
|
|
391
|
+
const { rm } = await import("node:fs/promises");
|
|
392
|
+
await rm(docsIndexPath, { force: true });
|
|
393
|
+
}
|
|
365
394
|
if (indexSource) {
|
|
366
395
|
try {
|
|
367
396
|
const indexRaw = await readFile(indexSource, "utf-8");
|
|
@@ -425,7 +454,7 @@ async function build() {
|
|
|
425
454
|
const rebuildDecision = shouldRebuild(file.path, file.mtime, cache);
|
|
426
455
|
|
|
427
456
|
if (rebuildDecision === "no" && !builder) {
|
|
428
|
-
const outputPath = join(
|
|
457
|
+
const outputPath = join(DOCS_OUT_DIR, `${file.path}.html`);
|
|
429
458
|
if (existsSync(outputPath) && !assetsChanged) {
|
|
430
459
|
skipped++;
|
|
431
460
|
continue;
|
|
@@ -447,7 +476,7 @@ async function build() {
|
|
|
447
476
|
const cached = cache[file.path];
|
|
448
477
|
if (isCacheEntry(cached) && cached.hash === contentHash) {
|
|
449
478
|
if (!assetsChanged) {
|
|
450
|
-
const outputPath = join(
|
|
479
|
+
const outputPath = join(DOCS_OUT_DIR, `${file.path}.html`);
|
|
451
480
|
if (existsSync(outputPath)) {
|
|
452
481
|
cache[file.path] = { ...cached, mtime: file.mtime, builtAt: Date.now() };
|
|
453
482
|
skipped++;
|
|
@@ -472,7 +501,7 @@ async function build() {
|
|
|
472
501
|
builder,
|
|
473
502
|
pageNonce
|
|
474
503
|
);
|
|
475
|
-
const outputPath = join(
|
|
504
|
+
const outputPath = join(DOCS_OUT_DIR, `${capturedFile.path}.html`);
|
|
476
505
|
await mkdir(dirname(outputPath), { recursive: true });
|
|
477
506
|
await writeFile(outputPath, html);
|
|
478
507
|
cache[capturedFile.path] = {
|
|
@@ -505,21 +534,19 @@ async function build() {
|
|
|
505
534
|
builder,
|
|
506
535
|
generateNonce()
|
|
507
536
|
);
|
|
508
|
-
|
|
509
|
-
|
|
537
|
+
if (DOCS_OUT_DIR !== DIST_DIR) {
|
|
538
|
+
await mkdir(DOCS_OUT_DIR, { recursive: true });
|
|
539
|
+
await writeFile(docsIndexPath, indexHtml);
|
|
540
|
+
}
|
|
510
541
|
} catch (err) {
|
|
511
542
|
const msg = err instanceof Error ? err.message : String(err);
|
|
512
543
|
errors.push(`index: ${msg}`);
|
|
513
544
|
console.error(`\n❌ Failed to build index: ${msg}\n`);
|
|
514
545
|
}
|
|
515
|
-
} else {
|
|
516
|
-
const msg = "docs root index: docs/index.mdx (or docs/index.md) not found";
|
|
517
|
-
errors.push(`index: ${msg}`);
|
|
518
|
-
console.error(`\n❌ Failed to build index: ${msg}\n`);
|
|
519
546
|
}
|
|
520
547
|
|
|
521
548
|
const landingPage = React.createElement(IndexPage);
|
|
522
|
-
const landingFavicon = docuConfig.meta?.favicon ||
|
|
549
|
+
const landingFavicon = docuConfig.meta?.favicon || defaultFavicon();
|
|
523
550
|
const landingSeo = buildSeoMeta(
|
|
524
551
|
docuConfig,
|
|
525
552
|
docuConfig.meta as unknown as Record<string, unknown>,
|
|
@@ -545,14 +572,18 @@ async function build() {
|
|
|
545
572
|
js: landingContext.assets.js,
|
|
546
573
|
nonce: landingNonce,
|
|
547
574
|
themeCss: inlineThemeCss,
|
|
575
|
+
basePath: servedBasePath(),
|
|
548
576
|
headExtra: builder?.collectHead(landingContext),
|
|
549
577
|
bodyExtra: builder?.collectBody(landingContext),
|
|
550
578
|
});
|
|
551
579
|
if (builder) landingHtml = await builder.runTransformHtmlChain(landingHtml, landingContext);
|
|
580
|
+
// The landing page always owns `/`. At a root deployment the docs index
|
|
581
|
+
// would collide with it, so the index write above is skipped there — the
|
|
582
|
+
// docs root page simply does not exist.
|
|
552
583
|
await writeFile(join(DIST_DIR, "index.html"), landingHtml);
|
|
553
584
|
|
|
554
585
|
const notFoundPage = React.createElement(NotFoundPage);
|
|
555
|
-
const notFoundFavicon = docuConfig.meta?.favicon ||
|
|
586
|
+
const notFoundFavicon = docuConfig.meta?.favicon || defaultFavicon();
|
|
556
587
|
const notFoundNonce = generateNonce();
|
|
557
588
|
const notFoundContext: PageContext = {
|
|
558
589
|
pageType: "notFound",
|
|
@@ -580,6 +611,8 @@ async function build() {
|
|
|
580
611
|
// Served as the static-host fallback at ANY requested path — relative
|
|
581
612
|
// depth can never be right there, so use root-absolute asset URLs.
|
|
582
613
|
absoluteAssets: true,
|
|
614
|
+
basePath: servedBasePath(),
|
|
615
|
+
deployPath: deployPath(),
|
|
583
616
|
});
|
|
584
617
|
if (builder) notFoundHtml = await builder.runTransformHtmlChain(notFoundHtml, notFoundContext);
|
|
585
618
|
await writeFile(join(DIST_DIR, "404.html"), notFoundHtml);
|
|
@@ -593,7 +626,7 @@ async function build() {
|
|
|
593
626
|
slug: f.path,
|
|
594
627
|
title: f.path.split("/").pop() || f.path,
|
|
595
628
|
filePath: join(DOCS_DIR, f.path),
|
|
596
|
-
outputPath: join(
|
|
629
|
+
outputPath: join(DOCS_OUT_DIR, `${f.path}.html`),
|
|
597
630
|
}));
|
|
598
631
|
await builder.runOnEnd(pages, { assetManifest, outDir: DIST_DIR });
|
|
599
632
|
}
|
package/.docu/node/cache-key.ts
CHANGED
|
@@ -1,16 +1,47 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
-
import { FRAMEWORK_ROOT, STYLES_DIR, resolveProjectFile } from "./paths";
|
|
4
|
+
import { FRAMEWORK_ROOT, STYLES_DIR, resolveProjectFile, basePath, deployPath } from "./paths";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Build cache version — bump when the toolchain output contract changes
|
|
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
|
+
*
|
|
11
|
+
* v6: pages became base-path aware (`meta.basePath`), and the resolved value
|
|
12
|
+
* is folded into `basePathStamp()` below. Bumping here also invalidates any
|
|
13
|
+
* cache written by an earlier config, so a base-path edit can never be
|
|
14
|
+
* silently skipped as "unchanged".
|
|
10
15
|
*/
|
|
11
|
-
export const BUILD_CACHE_VERSION =
|
|
16
|
+
export const BUILD_CACHE_VERSION = 6;
|
|
12
17
|
|
|
13
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* Resolved docs URL prefix and deployment path, part of every cache key.
|
|
20
|
+
*
|
|
21
|
+
* The page cache is keyed by relative MDX path and the bundle/asset slots are
|
|
22
|
+
* hashed from MDX source alone — neither changes when only `meta.basePath` or
|
|
23
|
+
* `meta.baseURL`'s path changes. Without this stamp, editing either would leave
|
|
24
|
+
* every key untouched and the build would skip every page, emitting HTML that
|
|
25
|
+
* still points at the old prefix.
|
|
26
|
+
*/
|
|
27
|
+
export function basePathStamp(): string {
|
|
28
|
+
return prefixStamp(basePath(), deployPath());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Compose the cache stamp from both prefixes that shape generated URLs: the
|
|
33
|
+
* docs prefix and the host's deployment path (from `meta.baseURL`). Either one
|
|
34
|
+
* changing has to invalidate the cache, or a warm cache keeps serving HTML that
|
|
35
|
+
* points at the old location.
|
|
36
|
+
*/
|
|
37
|
+
export function prefixStamp(resolvedBasePath: string, deploymentPath: string): string {
|
|
38
|
+
if (deploymentPath.length === 0) return resolvedBasePath;
|
|
39
|
+
return `${resolvedBasePath || "/"}@${deploymentPath}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Toolchain fingerprint: Bun version on Bun, Deno version on Deno, Node elsewhere.
|
|
44
|
+
*/
|
|
14
45
|
export function runtimeStamp(): string {
|
|
15
46
|
const g = globalThis as Record<string, unknown>;
|
|
16
47
|
const bun = g.Bun as { version?: string } | undefined;
|
|
@@ -23,6 +54,59 @@ export function runtimeStamp(): string {
|
|
|
23
54
|
return "node-unknown";
|
|
24
55
|
}
|
|
25
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Framework source files that shape rendered HTML.
|
|
59
|
+
*
|
|
60
|
+
* Page cache entries are keyed by MDX source alone, so editing the renderer
|
|
61
|
+
* (a template tweak, a new meta tag, base-path plumbing) would otherwise leave
|
|
62
|
+
* every page a cache hit and keep serving HTML built by the old code. Hashing
|
|
63
|
+
* these sources makes a framework edit invalidate the pages it affects.
|
|
64
|
+
*/
|
|
65
|
+
const RENDER_SOURCE_FILES = [
|
|
66
|
+
".docu/node/html.ts",
|
|
67
|
+
".docu/node/html.shared.ts",
|
|
68
|
+
".docu/node/seo.ts",
|
|
69
|
+
".docu/node/utils.ts",
|
|
70
|
+
".docu/node/paths.ts",
|
|
71
|
+
".docu/node/base-path.ts",
|
|
72
|
+
".docu/node/mdx.ts",
|
|
73
|
+
".docu/node/search-indexer.ts",
|
|
74
|
+
".docu/node/build.ts",
|
|
75
|
+
".docu/node/build.impl.ts",
|
|
76
|
+
".docu/node/server-routes.ts",
|
|
77
|
+
".docu/components/DocsLayout.tsx",
|
|
78
|
+
".docu/components/Navbar.tsx",
|
|
79
|
+
".docu/pages/index.tsx",
|
|
80
|
+
".docu/pages/404.tsx",
|
|
81
|
+
".docu/pages/docs/[[...slug]].tsx",
|
|
82
|
+
] as const;
|
|
83
|
+
|
|
84
|
+
let renderStampCache: string | null = null;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Fingerprint of the framework's rendering sources.
|
|
88
|
+
*
|
|
89
|
+
* Memoized per process — the files cannot change mid-build, and hashing them
|
|
90
|
+
* on every page would dominate build time. A missing file contributes nothing
|
|
91
|
+
* so a partial install still yields a stable (if weaker) stamp.
|
|
92
|
+
*/
|
|
93
|
+
export function renderToolchainStamp(): string {
|
|
94
|
+
if (renderStampCache !== null) return renderStampCache;
|
|
95
|
+
const h = createHash("sha256");
|
|
96
|
+
h.update(`v${BUILD_CACHE_VERSION}`);
|
|
97
|
+
for (const rel of RENDER_SOURCE_FILES) {
|
|
98
|
+
const file = join(FRAMEWORK_ROOT, rel);
|
|
99
|
+
try {
|
|
100
|
+
if (!existsSync(file)) continue;
|
|
101
|
+
h.update(rel).update("\0").update(readFileSync(file, "utf-8"));
|
|
102
|
+
} catch {
|
|
103
|
+
// unreadable source — skip it rather than break the build
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
renderStampCache = h.digest("hex").slice(0, 16);
|
|
107
|
+
return renderStampCache;
|
|
108
|
+
}
|
|
109
|
+
|
|
26
110
|
/**
|
|
27
111
|
* True when a CSS file can change Tailwind v4 output.
|
|
28
112
|
* v4 is CSS-first: `@theme`, `@source`, `@plugin`, `@custom-variant`,
|
|
@@ -230,6 +314,8 @@ export function computeTailwindCacheKey(
|
|
|
230
314
|
h.update("\0");
|
|
231
315
|
h.update(runtimeStamp());
|
|
232
316
|
h.update("\0");
|
|
317
|
+
h.update(basePathStamp());
|
|
318
|
+
h.update("\0");
|
|
233
319
|
h.update(`v${BUILD_CACHE_VERSION}`);
|
|
234
320
|
return h.digest("hex").slice(0, 16);
|
|
235
321
|
}
|
|
@@ -304,7 +390,9 @@ export function hookMemoryPressure(clear: () => void): void {
|
|
|
304
390
|
export function hashMdxSources(mdxSources: Record<string, string>): string {
|
|
305
391
|
const h = createHash("sha256");
|
|
306
392
|
const slugs = Object.keys(mdxSources).sort();
|
|
307
|
-
h.update(
|
|
393
|
+
h.update(
|
|
394
|
+
`v${BUILD_CACHE_VERSION}:${runtimeStamp()}:${basePathStamp()}:${renderToolchainStamp()}:`
|
|
395
|
+
);
|
|
308
396
|
for (const slug of slugs) {
|
|
309
397
|
h.update(slug);
|
|
310
398
|
h.update("\0");
|
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
import type { DocuRoute, DocuConfig } from "./types";
|
|
2
|
-
import { loadDocuConfig } from "./paths";
|
|
2
|
+
import { loadDocuConfig, resolveBasePath, resolveDeployPath } from "./paths";
|
|
3
3
|
import { resolveRoutes } from "./fs-scanner";
|
|
4
4
|
|
|
5
5
|
const docuConfig = loadDocuConfig();
|
|
6
6
|
export const routes: DocuRoute[] = resolveRoutes(docuConfig.routes || []);
|
|
7
7
|
export const config = docuConfig as unknown as DocuConfig;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* URL prefix the site is served under (`""` at the domain root, else `/prefix`).
|
|
11
|
+
* Components use this instead of assuming `/docs`, so a subpath deployment does
|
|
12
|
+
* not request assets from the domain root.
|
|
13
|
+
*/
|
|
14
|
+
export const basePath: string = resolveBasePath(docuConfig);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Path the host serves the dist root under (`""` at an origin root, `/repo` for
|
|
18
|
+
* a GitHub Pages project site — see `resolveDeployPath`). Root-absolute browser
|
|
19
|
+
* requests (the search index) build their URL from it, because the client
|
|
20
|
+
* bundle cannot read `docu.json` at runtime.
|
|
21
|
+
*/
|
|
22
|
+
export const deployPath: string = resolveDeployPath(docuConfig);
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { writeFile, mkdir } from "node:fs/promises";
|
|
12
12
|
import { existsSync, readFileSync } from "node:fs";
|
|
13
13
|
import { join, resolve } from "node:path";
|
|
14
|
-
import { DIST_DIR, PROJECT_ROOT, FRAMEWORK_ROOT } from "./paths";
|
|
14
|
+
import { DIST_DIR, PROJECT_ROOT, FRAMEWORK_ROOT, servedBasePath } from "./paths";
|
|
15
15
|
|
|
16
16
|
const FLAME_VERSION = JSON.parse(
|
|
17
17
|
readFileSync(resolve(FRAMEWORK_ROOT, "package.json"), "utf-8")
|
|
@@ -20,7 +20,49 @@ const FLAME_VERSION = JSON.parse(
|
|
|
20
20
|
const WORKFLOW_DIR = join(PROJECT_ROOT, ".github/workflows");
|
|
21
21
|
const WORKFLOW_FILE = join(WORKFLOW_DIR, "deploy.yml");
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Nginx content-assets block.
|
|
25
|
+
*
|
|
26
|
+
* Two distinct asset trees:
|
|
27
|
+
* - `/assets/` — hash-named client bundles, written to the dist root, immutable.
|
|
28
|
+
* - `<prefix>/assets/` — user content assets copied from `docs/assets`,
|
|
29
|
+
* revalidated after 7d.
|
|
30
|
+
*
|
|
31
|
+
* When the prefix is empty (root deployment) both trees land on `/assets/`, and
|
|
32
|
+
* nginx rejects duplicate location blocks — so the content tree is omitted and
|
|
33
|
+
* the immutable bundle rules win. Non-hashed content assets then inherit the
|
|
34
|
+
* 1y immutable header; that is a deliberate trade for a valid config, and root
|
|
35
|
+
* deployments are the uncommon case.
|
|
36
|
+
*/
|
|
37
|
+
export function buildNginxConf(basePath: string): string {
|
|
38
|
+
// nginx `location` names must be plain URI prefixes; anything else (spaces,
|
|
39
|
+
// braces, semicolons) fails `nginx -t` and the container never starts.
|
|
40
|
+
// Unsafe prefixes simply lose the content-assets cache block — the default
|
|
41
|
+
// location still serves the files, the same trade root deployments make.
|
|
42
|
+
const safePrefix = /^\/[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)*$/.test(basePath);
|
|
43
|
+
if (basePath.length > 0 && !safePrefix) {
|
|
44
|
+
console.warn(
|
|
45
|
+
`[flame] basePath "${basePath}" contains characters nginx cannot use in a location block; ` +
|
|
46
|
+
"omitting the content-assets caching block (assets still served)"
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
const contentAssetsBlock =
|
|
50
|
+
safePrefix && basePath.length > 0
|
|
51
|
+
? `
|
|
52
|
+
location ${basePath}/assets/ {
|
|
53
|
+
expires 7d;
|
|
54
|
+
add_header Cache-Control "public";
|
|
55
|
+
add_header X-Frame-Options "DENY" always;
|
|
56
|
+
add_header X-Content-Type-Options "nosniff" always;
|
|
57
|
+
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
|
|
58
|
+
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
59
|
+
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
|
60
|
+
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;
|
|
61
|
+
}
|
|
62
|
+
`
|
|
63
|
+
: "";
|
|
64
|
+
|
|
65
|
+
return `server {
|
|
24
66
|
listen 80;
|
|
25
67
|
server_name _;
|
|
26
68
|
root /usr/share/nginx/html;
|
|
@@ -58,18 +100,7 @@ export const NGINX_CONF = `server {
|
|
|
58
100
|
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
|
59
101
|
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
102
|
}
|
|
61
|
-
|
|
62
|
-
location /docs/assets/ {
|
|
63
|
-
expires 7d;
|
|
64
|
-
add_header Cache-Control "public";
|
|
65
|
-
add_header X-Frame-Options "DENY" always;
|
|
66
|
-
add_header X-Content-Type-Options "nosniff" always;
|
|
67
|
-
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
|
|
68
|
-
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
69
|
-
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
|
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;
|
|
71
|
-
}
|
|
72
|
-
|
|
103
|
+
${contentAssetsBlock}
|
|
73
104
|
location = /404.html { }
|
|
74
105
|
|
|
75
106
|
location / {
|
|
@@ -77,6 +108,10 @@ export const NGINX_CONF = `server {
|
|
|
77
108
|
}
|
|
78
109
|
}
|
|
79
110
|
`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Backward-compatible default config (root-level `/docs` prefix). */
|
|
114
|
+
export const NGINX_CONF = buildNginxConf("/docs");
|
|
80
115
|
|
|
81
116
|
export const DOCKERIGNORE = `node_modules
|
|
82
117
|
*.DS_Store
|
|
@@ -260,6 +295,7 @@ export const HEADERS_FILE = `/*
|
|
|
260
295
|
`;
|
|
261
296
|
|
|
262
297
|
const isDocker = !!process.env.FLAME_DEPLOY_DOCKER;
|
|
298
|
+
|
|
263
299
|
const isSilent = !!process.env.FLAME_DEPLOY_SILENT;
|
|
264
300
|
const isCi = !!process.env.FLAME_DEPLOY_CI;
|
|
265
301
|
|
|
@@ -300,7 +336,7 @@ async function writeDockerFiles() {
|
|
|
300
336
|
}
|
|
301
337
|
|
|
302
338
|
if (!existsSync(join(dockerDir, "nginx.conf"))) {
|
|
303
|
-
await writeFile(join(dockerDir, "nginx.conf"),
|
|
339
|
+
await writeFile(join(dockerDir, "nginx.conf"), buildNginxConf(servedBasePath()));
|
|
304
340
|
log.created("📄 Created nginx.conf");
|
|
305
341
|
}
|
|
306
342
|
|