@dogsbay/format-astro 0.2.0-beta.92 → 0.2.0-beta.98
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/dist/base-path.d.ts +10 -0
- package/dist/base-path.d.ts.map +1 -1
- package/dist/base-path.js +11 -1
- package/dist/base-path.js.map +1 -1
- package/dist/blog.d.ts +134 -0
- package/dist/blog.d.ts.map +1 -0
- package/dist/blog.js +319 -0
- package/dist/blog.js.map +1 -0
- package/dist/diff-decoration.d.ts +79 -0
- package/dist/diff-decoration.d.ts.map +1 -0
- package/dist/diff-decoration.js +541 -0
- package/dist/diff-decoration.js.map +1 -0
- package/dist/granularity.d.ts +83 -0
- package/dist/granularity.d.ts.map +1 -0
- package/dist/granularity.js +247 -0
- package/dist/granularity.js.map +1 -0
- package/dist/index.d.ts +17 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -1
- package/dist/index.js.map +1 -1
- package/dist/lead.d.ts.map +1 -1
- package/dist/lead.js +30 -6
- package/dist/lead.js.map +1 -1
- package/dist/llms-txt.d.ts +19 -0
- package/dist/llms-txt.d.ts.map +1 -1
- package/dist/llms-txt.js +40 -2
- package/dist/llms-txt.js.map +1 -1
- package/dist/project.d.ts +134 -2
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +1149 -137
- package/dist/project.js.map +1 -1
- package/dist/serialize.d.ts +1 -0
- package/dist/serialize.d.ts.map +1 -1
- package/dist/serialize.js +302 -138
- package/dist/serialize.js.map +1 -1
- package/dist/sitemap.d.ts +24 -0
- package/dist/sitemap.d.ts.map +1 -1
- package/dist/sitemap.js +32 -3
- package/dist/sitemap.js.map +1 -1
- package/dist/taxonomy.d.ts +24 -1
- package/dist/taxonomy.d.ts.map +1 -1
- package/dist/taxonomy.js +23 -2
- package/dist/taxonomy.js.map +1 -1
- package/package.json +8 -7
package/dist/project.js
CHANGED
|
@@ -4,14 +4,28 @@
|
|
|
4
4
|
* Takes ExportPage[] + NavItem[] and generates a complete Astro project
|
|
5
5
|
* with static .astro pages using real Dogsbay components.
|
|
6
6
|
*/
|
|
7
|
-
import { existsSync, mkdirSync, writeFileSync, readFileSync, copyFileSync, cpSync, readdirSync, statSync, } from "node:fs";
|
|
7
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, copyFileSync, cpSync, readdirSync, statSync, rmSync, rmdirSync, openSync, readSync, closeSync, } from "node:fs";
|
|
8
8
|
import { join, dirname, relative, resolve } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { treeToDogsbayMd } from "@dogsbay/format-dogsbay-md";
|
|
11
11
|
import { treeToAstro, TONE_CLASSES } from "./serialize.js";
|
|
12
|
+
import { emitPluginRuntime } from "./plugins.js";
|
|
12
13
|
import { buildLlmsTxt, buildSectionLlmsTxt, buildLlmsFullTxt } from "./llms-txt.js";
|
|
13
14
|
import { buildSitemap, buildSitemapIndex } from "./sitemap.js";
|
|
15
|
+
import { assembleBook, estimateTreeBytes } from "./granularity.js";
|
|
16
|
+
/**
|
|
17
|
+
* The per-topic "Read as single page" affordance (granularity book view).
|
|
18
|
+
* Returns `[]` when the topic has no book (granularity off / not in a group).
|
|
19
|
+
* `href` is a build-time-known route (`<bookRoute>#<anchor>`).
|
|
20
|
+
*/
|
|
21
|
+
function singlePageLinkLines(href, indent) {
|
|
22
|
+
if (!href)
|
|
23
|
+
return [];
|
|
24
|
+
const pad = " ".repeat(indent);
|
|
25
|
+
return [`${pad}<a class="dsb-read-single-page" href="${href}">Read as single page →</a>`];
|
|
26
|
+
}
|
|
14
27
|
import { normalizeBasePath, basePathSegments, buildCurrentPath, withBasePath, parseSiteUrl, combinePrefix, } from "./base-path.js";
|
|
28
|
+
import { buildBlogData, blogAdjacency } from "./blog.js";
|
|
15
29
|
/**
|
|
16
30
|
* Combined URL prefix = urlBase (Astro `base` from site.url path) +
|
|
17
31
|
* basePath (filesystem layout prefix). Every URL emitter (nav,
|
|
@@ -194,6 +208,161 @@ function rewriteInlineImageSrcs(nodes, prefix) {
|
|
|
194
208
|
function rewriteHtmlImageSrcs(html, prefix) {
|
|
195
209
|
return html.replace(/(<img\b[^>]*\ssrc=")(\/[^"]+)"/g, (_match, before, src) => `${before}${rewriteHref(src, prefix)}"`);
|
|
196
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Astro `outDir` for a Cloudflare Workers deploy mounted on a host subpath.
|
|
213
|
+
*
|
|
214
|
+
* Workers Static Assets resolves the FULL request pathname against the
|
|
215
|
+
* asset manifest and strips nothing. A site mounted at `dogsbay.ai/blog/*`
|
|
216
|
+
* is therefore asked for `/blog/index.html`, and unless the build output
|
|
217
|
+
* actually contains `blog/`, every request 404s. Astro's `base` does not
|
|
218
|
+
* do this: it prefixes generated URLs (including `/blog/_astro/...`) but
|
|
219
|
+
* leaves the emitted files at `dist/` root. Setting `outDir` to
|
|
220
|
+
* `./dist<urlBase>` is what makes the two agree.
|
|
221
|
+
*
|
|
222
|
+
* GitHub Pages needs the OPPOSITE and must not get this. There the
|
|
223
|
+
* uploaded artifact IS the site root, served at `https://<user>.github.io/
|
|
224
|
+
* <repo>/`, so the host supplies the prefix: URLs must carry it, files
|
|
225
|
+
* must not. Emitting `outDir` for a Pages build would produce
|
|
226
|
+
* `/<repo>/<repo>/`. Hence the deploy-target gate — the same `site.url`
|
|
227
|
+
* deliberately yields different layouts per target.
|
|
228
|
+
*
|
|
229
|
+
* Returns `undefined` when the target is not Workers or the site sits at
|
|
230
|
+
* the host root (no path in `site.url`), which is the common case.
|
|
231
|
+
*/
|
|
232
|
+
export function workersSubpathOutDir(deploy, urlBase) {
|
|
233
|
+
if (deploy !== "cloudflare-workers")
|
|
234
|
+
return undefined;
|
|
235
|
+
if (!urlBase)
|
|
236
|
+
return undefined;
|
|
237
|
+
return `./dist${urlBase}`;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Delete generated content pages whose source markdown is gone.
|
|
241
|
+
*
|
|
242
|
+
* `emitAstroPages` writes a page per source file but never removed one,
|
|
243
|
+
* so deleting a post left its `.astro` and `.md.ts` behind: the page kept
|
|
244
|
+
* building, kept deploying, and kept being linked from tag archives.
|
|
245
|
+
* Measured on the blog — three deleted posts were still live.
|
|
246
|
+
*
|
|
247
|
+
* Deleting files needs a tighter contract than writing them, so this is
|
|
248
|
+
* bounded four ways:
|
|
249
|
+
*
|
|
250
|
+
* ROOT Only the subtree this invocation owns —
|
|
251
|
+
* `src/pages/<basePath>/<section>` — never all of `src/pages`.
|
|
252
|
+
* `--section` exists so several converts land in one project;
|
|
253
|
+
* walking the whole tree meant each run deleted the previous
|
|
254
|
+
* run's section.
|
|
255
|
+
* FLOOR Never prune when this run generated nothing. A mis-set
|
|
256
|
+
* `--section`, a moved source dir or a config typo yields zero
|
|
257
|
+
* pages, and an unguarded prune would delete the entire site
|
|
258
|
+
* and exit 0.
|
|
259
|
+
* WHOLE Never prune when a page failed to generate. The page loop
|
|
260
|
+
* warns and continues, so a failed page is absent from
|
|
261
|
+
* `generatedPaths` and would look exactly like a deleted one —
|
|
262
|
+
* turning a transient serializer throw into a deletion.
|
|
263
|
+
* BANNER Only files carrying the `dogsbay convert` banner. That marks
|
|
264
|
+
* content pages and their `.md` mirrors; taxonomy and blog
|
|
265
|
+
* routes carry a `dogsbay site build` banner and a writer's own
|
|
266
|
+
* page carries none. This runs BEFORE those emitters, when last
|
|
267
|
+
* build's routes are present and unclaimed, so the distinction
|
|
268
|
+
* is what keeps them alive.
|
|
269
|
+
*
|
|
270
|
+
* KNOWN GAP: missing-translation stubs are not covered. They carry
|
|
271
|
+
* `// AUTO-GENERATED missing-translation stub.`, not the convert banner,
|
|
272
|
+
* so deleting `en/foo.md` leaves `/fr/foo` as a live route redirecting to
|
|
273
|
+
* a now-404 `/en/foo` — the same symptom, one hop away. Fixing it needs
|
|
274
|
+
* the stubs tracked in `generatedPaths` first; giving them the convert
|
|
275
|
+
* banner alone would make every build prune the stub it just wrote.
|
|
276
|
+
*/
|
|
277
|
+
function pruneOrphanedPages(outputDir, root, generatedPaths) {
|
|
278
|
+
if (!existsSync(root))
|
|
279
|
+
return [];
|
|
280
|
+
const removed = [];
|
|
281
|
+
const walk = (dir) => {
|
|
282
|
+
let emptied = false;
|
|
283
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
284
|
+
const full = join(dir, entry.name);
|
|
285
|
+
if (entry.isDirectory()) {
|
|
286
|
+
walk(full);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
// Only our two output kinds can be orphans; skip everything else
|
|
290
|
+
// rather than reading it.
|
|
291
|
+
if (!entry.name.endsWith(".astro") && !entry.name.endsWith(".md.ts"))
|
|
292
|
+
continue;
|
|
293
|
+
if (generatedPaths.has(relative(outputDir, full)))
|
|
294
|
+
continue;
|
|
295
|
+
// The banner is always the first line, so read a prefix rather than
|
|
296
|
+
// the whole file — a `.md.ts` mirror embeds an entire page's
|
|
297
|
+
// markdown, and a large corpus is thousands of them per build.
|
|
298
|
+
let head;
|
|
299
|
+
try {
|
|
300
|
+
const fd = openSync(full, "r");
|
|
301
|
+
try {
|
|
302
|
+
const buf = Buffer.alloc(256);
|
|
303
|
+
const n = readSync(fd, buf, 0, 256, 0);
|
|
304
|
+
head = buf.subarray(0, n).toString("utf-8");
|
|
305
|
+
}
|
|
306
|
+
finally {
|
|
307
|
+
closeSync(fd);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (!head.includes("AUTO-GENERATED by `dogsbay convert`"))
|
|
314
|
+
continue;
|
|
315
|
+
rmSync(full, { force: true });
|
|
316
|
+
removed.push(relative(outputDir, full));
|
|
317
|
+
emptied = true;
|
|
318
|
+
}
|
|
319
|
+
// Only directories THIS pass emptied, and only via non-recursive
|
|
320
|
+
// rmdir: it fails atomically if anything landed in the meantime,
|
|
321
|
+
// which matters under `site dev` where the build re-runs while an
|
|
322
|
+
// editor and the dev server are live.
|
|
323
|
+
if (emptied && dir !== root) {
|
|
324
|
+
try {
|
|
325
|
+
rmdirSync(dir);
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
/* not empty, or raced — correct to leave it */
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
walk(root);
|
|
333
|
+
return removed;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Build the `excludeFromRoutes` predicate.
|
|
337
|
+
*
|
|
338
|
+
* Exported because more than one emitter has to agree on which pages
|
|
339
|
+
* become routes. The blog listed excluded pages as posts, producing index
|
|
340
|
+
* cards and Newer/Older links to pages that were never emitted — a 404
|
|
341
|
+
* from the site's own front page.
|
|
342
|
+
*
|
|
343
|
+
* A single-segment pattern matches ANY segment (fragment dirs symlink to
|
|
344
|
+
* many depths in AsciiBinder corpora); a multi-segment pattern matches as
|
|
345
|
+
* a leading path prefix, so `welcome/_internal` excludes only that tree.
|
|
346
|
+
*/
|
|
347
|
+
export function makeSlugExcluder(excludeFromRoutes) {
|
|
348
|
+
const patterns = (excludeFromRoutes ?? []).map((p) => p.replace(/^\/+/, "").replace(/\/+$/, ""));
|
|
349
|
+
return (slug) => {
|
|
350
|
+
if (patterns.length === 0)
|
|
351
|
+
return false;
|
|
352
|
+
const segments = slug.split("/");
|
|
353
|
+
for (const pattern of patterns) {
|
|
354
|
+
if (pattern.includes("/")) {
|
|
355
|
+
if (slug === pattern || slug.startsWith(pattern + "/"))
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
if (segments.includes(pattern))
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return false;
|
|
364
|
+
};
|
|
365
|
+
}
|
|
197
366
|
/**
|
|
198
367
|
* Build a `wrangler.jsonc` for Cloudflare Workers Static Assets.
|
|
199
368
|
*
|
|
@@ -368,11 +537,20 @@ jobs:
|
|
|
368
537
|
* `src/data/site.json`. Backward-compatible: existing fields keep their
|
|
369
538
|
* empty-string defaults; new optional fields are omitted when undefined.
|
|
370
539
|
*/
|
|
371
|
-
function buildSiteConfig(siteName, options) {
|
|
540
|
+
function buildSiteConfig(siteName, options, outputDir) {
|
|
372
541
|
const cfg = {
|
|
373
542
|
siteName,
|
|
374
543
|
repoUrl: options.repoUrl || "",
|
|
375
544
|
};
|
|
545
|
+
// Favicon href, so GENERATED routes can emit <link rel="icon"> too.
|
|
546
|
+
// Content pages get it computed per page; the blog index and the
|
|
547
|
+
// taxonomy routes are emitted by other emitters that never received it,
|
|
548
|
+
// so the site's own front page had no icon while its posts did.
|
|
549
|
+
if (outputDir) {
|
|
550
|
+
const href = faviconHref(outputDir, parseSiteUrl(options.siteUrl).urlBase);
|
|
551
|
+
if (href)
|
|
552
|
+
cfg.favicon = href;
|
|
553
|
+
}
|
|
376
554
|
if (options.siteUrl)
|
|
377
555
|
cfg.siteUrl = options.siteUrl;
|
|
378
556
|
if (options.description)
|
|
@@ -408,6 +586,11 @@ function buildSiteConfig(siteName, options) {
|
|
|
408
586
|
if (options.tagLabels && Object.keys(options.tagLabels).length > 0) {
|
|
409
587
|
cfg.tagLabels = options.tagLabels;
|
|
410
588
|
}
|
|
589
|
+
// Link-icon glyphs — only emitted when configured, so sites without
|
|
590
|
+
// the feature keep byte-identical site.json.
|
|
591
|
+
if (options.linkIcons && (options.linkIcons.external || options.linkIcons.internal)) {
|
|
592
|
+
cfg.linkIcons = options.linkIcons;
|
|
593
|
+
}
|
|
411
594
|
if (options.taxonomyIndexPaths &&
|
|
412
595
|
Object.keys(options.taxonomyIndexPaths).length > 0) {
|
|
413
596
|
// Bake basePath into every emitted indexPath so consumers
|
|
@@ -490,6 +673,21 @@ export async function exportAstroProject(pages, nav, outputDir, options = {}) {
|
|
|
490
673
|
const scaffoldSkipped = emitSiteScaffold(outputDir, siteName, options, writeScaffold);
|
|
491
674
|
const { generated, outputNav } = await emitAstroPages(pages, nav, outputDir, options);
|
|
492
675
|
emitConfigDerivedFiles(outputDir, options);
|
|
676
|
+
// The generated layout unconditionally imports
|
|
677
|
+
// `@/data/switcherMap.json` and the plugin wrapper stacks
|
|
678
|
+
// (`@/components/wrappers/*Stack.astro`); the exporter must
|
|
679
|
+
// guarantee both exist (empty axes / passthrough slots when there
|
|
680
|
+
// is no version data and no plugins) — previously only
|
|
681
|
+
// `dogsbay site build` emitted them, so every direct
|
|
682
|
+
// exportAstroProject consumer produced an unbuildable site
|
|
683
|
+
// (root-caused 2026-07-13 on the first comparison-site build).
|
|
684
|
+
emitSwitcherMap(pages, outputDir, options);
|
|
685
|
+
emitPluginRuntime({
|
|
686
|
+
outputDir,
|
|
687
|
+
clientModules: [],
|
|
688
|
+
styles: [],
|
|
689
|
+
clientConfigs: [],
|
|
690
|
+
});
|
|
493
691
|
emitAgentReadinessFiles(pages, outputNav, outputDir, siteName, options);
|
|
494
692
|
console.log(`Generated ${generated} static .astro pages`);
|
|
495
693
|
if (alreadyScaffolded && !options.force && scaffoldSkipped > 0) {
|
|
@@ -526,7 +724,7 @@ function ensureDirectoryStructure(outputDir, basePath) {
|
|
|
526
724
|
*/
|
|
527
725
|
export function emitSiteConfig(outputDir, siteName, options) {
|
|
528
726
|
mkdirSync(join(outputDir, "src", "data"), { recursive: true });
|
|
529
|
-
writeFileSync(join(outputDir, "src", "data", "site.json"), JSON.stringify(buildSiteConfig(siteName, options), null, 2));
|
|
727
|
+
writeFileSync(join(outputDir, "src", "data", "site.json"), JSON.stringify(buildSiteConfig(siteName, options, outputDir), null, 2));
|
|
530
728
|
// Auto-generated companion to astro.config.mjs. Carries the
|
|
531
729
|
// site/base values derived from dogsbay.config.yml's site.url so
|
|
532
730
|
// changes propagate without --force-rescaffolding the main
|
|
@@ -540,6 +738,11 @@ export function emitSiteConfig(outputDir, siteName, options) {
|
|
|
540
738
|
? JSON.stringify(origin ?? options.siteUrl)
|
|
541
739
|
: "undefined";
|
|
542
740
|
const dogsbayBaseJson = astroBase ? JSON.stringify(astroBase) : "undefined";
|
|
741
|
+
// Workers subpath mounts need the build output to live under the mount
|
|
742
|
+
// path too — see workersSubpathOutDir. Undefined for every other target
|
|
743
|
+
// and for host-root sites, so nothing changes for existing deploys.
|
|
744
|
+
const dogsbayOutDir = workersSubpathOutDir(options.deploy, astroBase);
|
|
745
|
+
const dogsbayOutDirJson = dogsbayOutDir ? JSON.stringify(dogsbayOutDir) : "undefined";
|
|
543
746
|
// build.inlineStylesheets — defaults to "auto" (Astro's own
|
|
544
747
|
// default; matches our docs-first bias since theme.css is ~120KB
|
|
545
748
|
// and externalizing it lets the file cache cross-page). Authors
|
|
@@ -554,8 +757,102 @@ export function emitSiteConfig(outputDir, siteName, options) {
|
|
|
554
757
|
`export const dogsbaySite = ${dogsbaySiteJson};`,
|
|
555
758
|
`export const dogsbayBase = ${dogsbayBaseJson};`,
|
|
556
759
|
`export const dogsbayInlineStylesheets = ${JSON.stringify(dogsbayInline)};`,
|
|
760
|
+
`export const dogsbayOutDir = ${dogsbayOutDirJson};`,
|
|
557
761
|
"",
|
|
558
762
|
].join("\n"));
|
|
763
|
+
// dogsbay-mount.mjs — auto-generated, emitted only for subpath mounts.
|
|
764
|
+
//
|
|
765
|
+
// Two jobs that Astro cannot do itself once outDir moves to
|
|
766
|
+
// dist/<urlBase>/:
|
|
767
|
+
//
|
|
768
|
+
// clean `astro build` clears outDir, which is now the MOUNT dir,
|
|
769
|
+
// not dist/. Stale output therefore survives forever — a
|
|
770
|
+
// site migrating from the pre-mount layout keeps its old
|
|
771
|
+
// root-level index.html and _astro/, and changing site.url
|
|
772
|
+
// from /blog to /news strands the whole old tree. Both keep
|
|
773
|
+
// being uploaded, because `wrangler deploy` ships all of
|
|
774
|
+
// ./dist. dist/ is entirely derived, so clearing it is safe.
|
|
775
|
+
//
|
|
776
|
+
// NOTE robots.txt is deliberately NOT lifted. _headers is a CONFIG
|
|
777
|
+
// file that Workers reads from the assets root; robots.txt is a
|
|
778
|
+
// SERVED URL at /robots.txt, and on a subpath mount that path
|
|
779
|
+
// belongs to whichever worker owns the host root, not to this
|
|
780
|
+
// one. Lifting it would put a file at a path this worker's route
|
|
781
|
+
// never matches. The host-root worker's robots.txt is the
|
|
782
|
+
// authoritative one and must name this mount's sitemap — see
|
|
783
|
+
// plans/dogsbay-ai-site.md. The per-mount copy at
|
|
784
|
+
// <urlBase>/robots.txt is harmless but is NOT what crawlers read.
|
|
785
|
+
//
|
|
786
|
+
// finalize Workers Static Assets reads _headers only from the ROOT of
|
|
787
|
+
// assets.directory (./dist). Astro copies public/ into
|
|
788
|
+
// outDir, so _headers landed at dist/<urlBase>/_headers,
|
|
789
|
+
// where it does nothing — the RFC 8288 Link header pointing
|
|
790
|
+
// agents at llms.txt silently stopped applying, and the raw
|
|
791
|
+
// file was served as an asset. Move it up.
|
|
792
|
+
const mountHelperPath = join(outputDir, "dogsbay-mount.mjs");
|
|
793
|
+
if (!dogsbayOutDir && existsSync(mountHelperPath)) {
|
|
794
|
+
// No longer mounted (deploy target or site.url changed).
|
|
795
|
+
//
|
|
796
|
+
// Deleting it outright looks tidy and is wrong: package.json is
|
|
797
|
+
// scaffold-once, so the build script still runs
|
|
798
|
+
// `node ./dogsbay-mount.mjs clean && …` and the next `pnpm build` dies
|
|
799
|
+
// with ERR_MODULE_NOT_FOUND before Astro even starts. Downgrade it to
|
|
800
|
+
// a no-op instead, so the build keeps working while
|
|
801
|
+
// warnStaleMountBuildScript tells the author to update the script; only
|
|
802
|
+
// remove the file once nothing references it.
|
|
803
|
+
const pkgPath = join(outputDir, "package.json");
|
|
804
|
+
const stillReferenced = existsSync(pkgPath) && readFileSync(pkgPath, "utf-8").includes("dogsbay-mount.mjs");
|
|
805
|
+
if (stillReferenced) {
|
|
806
|
+
writeFileSync(mountHelperPath, [
|
|
807
|
+
"// AUTO-GENERATED by `dogsbay site build` — do not edit.",
|
|
808
|
+
"// This site is no longer mounted on a subpath, but package.json",
|
|
809
|
+
"// still calls this script, so it stays as a no-op rather than",
|
|
810
|
+
"// breaking the build. Update the build script and it goes away.",
|
|
811
|
+
"process.exit(0);",
|
|
812
|
+
"",
|
|
813
|
+
].join("\n"));
|
|
814
|
+
}
|
|
815
|
+
else {
|
|
816
|
+
rmSync(mountHelperPath, { force: true });
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
if (dogsbayOutDir) {
|
|
820
|
+
const mountDir = dogsbayOutDir.replace(/^\.\//, "");
|
|
821
|
+
writeFileSync(mountHelperPath, [
|
|
822
|
+
"// AUTO-GENERATED by `dogsbay site build` — do not edit.",
|
|
823
|
+
"// Subpath-mount build steps. See project.ts (dogsbay-mount.mjs).",
|
|
824
|
+
'import { rmSync, existsSync, mkdirSync, renameSync } from "node:fs";',
|
|
825
|
+
'import { dirname, join, resolve } from "node:path";',
|
|
826
|
+
'import { fileURLToPath } from "node:url";',
|
|
827
|
+
"",
|
|
828
|
+
`const MOUNT_DIR = ${JSON.stringify(mountDir)};`,
|
|
829
|
+
"// Resolve against THIS FILE, never process.cwd(). `pnpm build` runs",
|
|
830
|
+
"// with cwd set to the project, but this is an ordinary script sitting",
|
|
831
|
+
"// in the project root, and the natural CI/debug invocation",
|
|
832
|
+
"// `node path/to/site/dogsbay-mount.mjs clean` would otherwise",
|
|
833
|
+
"// recursively delete the CALLER's dist/.",
|
|
834
|
+
'const ROOT = dirname(fileURLToPath(import.meta.url));',
|
|
835
|
+
'const DIST = join(ROOT, "dist");',
|
|
836
|
+
'const mode = process.argv[2];',
|
|
837
|
+
"",
|
|
838
|
+
'if (mode === "clean") {',
|
|
839
|
+
' // Derived output only — astro build + pagefind regenerate all of it.',
|
|
840
|
+
' rmSync(DIST, { recursive: true, force: true });',
|
|
841
|
+
'} else if (mode === "finalize") {',
|
|
842
|
+
" const from = resolve(ROOT, `${MOUNT_DIR}/_headers`);",
|
|
843
|
+
' if (existsSync(from)) {',
|
|
844
|
+
' const to = join(DIST, "_headers");',
|
|
845
|
+
' mkdirSync(dirname(to), { recursive: true });',
|
|
846
|
+
' renameSync(from, to);',
|
|
847
|
+
" }",
|
|
848
|
+
'} else {',
|
|
849
|
+
' console.error("dogsbay-mount.mjs: expected `clean` or `finalize`");',
|
|
850
|
+
" process.exit(1);",
|
|
851
|
+
"}",
|
|
852
|
+
"",
|
|
853
|
+
].join("\n"));
|
|
854
|
+
}
|
|
855
|
+
warnStaleMountBuildScript(outputDir, dogsbayOutDir, astroBase);
|
|
559
856
|
// Migration check: pre-beta.20 sites have an astro.config.mjs that
|
|
560
857
|
// doesn't import the companion. Without the import, the values
|
|
561
858
|
// emitted above are unused and Astro's `base` stays unset — the
|
|
@@ -580,11 +877,13 @@ export function emitSiteConfig(outputDir, siteName, options) {
|
|
|
580
877
|
' dogsbaySite,',
|
|
581
878
|
' dogsbayBase,',
|
|
582
879
|
' dogsbayInlineStylesheets,',
|
|
880
|
+
' dogsbayOutDir,',
|
|
583
881
|
' } from "./astro.config.dogsbay.mjs";',
|
|
584
882
|
"",
|
|
585
883
|
" export default defineConfig({",
|
|
586
884
|
" ...(dogsbaySite ? { site: dogsbaySite } : {}),",
|
|
587
885
|
" ...(dogsbayBase ? { base: dogsbayBase } : {}),",
|
|
886
|
+
" ...(dogsbayOutDir ? { outDir: dogsbayOutDir } : {}),",
|
|
588
887
|
" build: { inlineStylesheets: dogsbayInlineStylesheets },",
|
|
589
888
|
" // ...your existing config...",
|
|
590
889
|
" });",
|
|
@@ -594,6 +893,30 @@ export function emitSiteConfig(outputDir, siteName, options) {
|
|
|
594
893
|
"",
|
|
595
894
|
].join("\n"));
|
|
596
895
|
}
|
|
896
|
+
else if (dogsbayOutDir && !astroConfigSrc.includes("dogsbayOutDir")) {
|
|
897
|
+
// The companion is imported, but this scaffold predates outDir
|
|
898
|
+
// support. astro.config.mjs is scaffold-once, so an existing site
|
|
899
|
+
// that moves to a Workers subpath mount keeps a config that emits
|
|
900
|
+
// to dist/ root while its wrangler route asks for dist<urlBase>/.
|
|
901
|
+
// Every request 404s and the HTML looks fine locally, so warn.
|
|
902
|
+
console.warn([
|
|
903
|
+
"",
|
|
904
|
+
" ⚠ astro.config.mjs does not apply `dogsbayOutDir`.",
|
|
905
|
+
` This site deploys to Cloudflare Workers under "${dogsbayOutDir.replace("./dist", "")}",`,
|
|
906
|
+
" so the build output must live under that path or every",
|
|
907
|
+
" request 404s — Workers matches the full request pathname",
|
|
908
|
+
" and strips no prefix.",
|
|
909
|
+
"",
|
|
910
|
+
" Add to the companion import in astro.config.mjs:",
|
|
911
|
+
"",
|
|
912
|
+
" dogsbayOutDir,",
|
|
913
|
+
"",
|
|
914
|
+
" and to defineConfig:",
|
|
915
|
+
"",
|
|
916
|
+
" ...(dogsbayOutDir ? { outDir: dogsbayOutDir } : {}),",
|
|
917
|
+
"",
|
|
918
|
+
].join("\n"));
|
|
919
|
+
}
|
|
597
920
|
}
|
|
598
921
|
}
|
|
599
922
|
/**
|
|
@@ -628,6 +951,74 @@ function syncDogsbayDepVersions(pkgPath, version) {
|
|
|
628
951
|
}
|
|
629
952
|
return changed;
|
|
630
953
|
}
|
|
954
|
+
/**
|
|
955
|
+
* Warn when the scaffold-once build script disagrees with the site's
|
|
956
|
+
* current mount.
|
|
957
|
+
*
|
|
958
|
+
* Lives here, not in `emitSiteScaffold`, because that function only runs
|
|
959
|
+
* on `site init` — a migration warning there would never fire for the
|
|
960
|
+
* sites that need it. Called from `emitSiteConfig`, which runs on every
|
|
961
|
+
* build.
|
|
962
|
+
*
|
|
963
|
+
* Both directions fail the same way: silently. Search stops working and
|
|
964
|
+
* nothing else about the build looks wrong.
|
|
965
|
+
*/
|
|
966
|
+
function warnStaleMountBuildScript(outputDir, outDir, urlBase) {
|
|
967
|
+
const pkgJsonPath = join(outputDir, "package.json");
|
|
968
|
+
if (!existsSync(pkgJsonPath))
|
|
969
|
+
return;
|
|
970
|
+
const pkgSrc = readFileSync(pkgJsonPath, "utf-8");
|
|
971
|
+
if (!outDir) {
|
|
972
|
+
// Moved OFF a mount: the script still clears dist/ and writes the
|
|
973
|
+
// Pagefind bundle to the old mount dir, while the page asks elsewhere.
|
|
974
|
+
if (!pkgSrc.includes("dogsbay-mount.mjs"))
|
|
975
|
+
return;
|
|
976
|
+
console.warn([
|
|
977
|
+
"",
|
|
978
|
+
" ⚠ package.json's build script still runs the subpath-mount steps,",
|
|
979
|
+
" but this site is no longer mounted on a subpath. dogsbay-mount.mjs",
|
|
980
|
+
" has been reduced to a no-op so the build keeps working, and the",
|
|
981
|
+
" Pagefind bundle is still being written to the old mount directory,",
|
|
982
|
+
" so Cmd+K will 404 until the script is updated.",
|
|
983
|
+
"",
|
|
984
|
+
" Change the build script back to:",
|
|
985
|
+
"",
|
|
986
|
+
" astro build && pagefind --site dist",
|
|
987
|
+
"",
|
|
988
|
+
].join("\n"));
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
// Moved ONTO a mount, or BETWEEN mounts.
|
|
992
|
+
//
|
|
993
|
+
// Gating on the mere presence of "dogsbay-mount.mjs" missed the second
|
|
994
|
+
// case: a site moving from /docs to /news regenerates MOUNT_DIR and
|
|
995
|
+
// astro build writes dist/news, but the scaffold-once script still runs
|
|
996
|
+
// `--output-path dist/docs/pagefind`. The bundle lands in a directory no
|
|
997
|
+
// page references and Cmd+K silently dies — the exact failure this
|
|
998
|
+
// warning exists for. Compare against the CURRENT mount instead.
|
|
999
|
+
if (!pkgSrc.includes("pagefind --site dist"))
|
|
1000
|
+
return;
|
|
1001
|
+
const mountDir = outDir.replace(/^\.\//, "");
|
|
1002
|
+
if (pkgSrc.includes(`--output-path ${mountDir}/pagefind`))
|
|
1003
|
+
return;
|
|
1004
|
+
const wanted = `node ./dogsbay-mount.mjs clean && astro build && ` +
|
|
1005
|
+
`pagefind --site dist --output-path ${mountDir}/pagefind && ` +
|
|
1006
|
+
`node ./dogsbay-mount.mjs finalize`;
|
|
1007
|
+
console.warn([
|
|
1008
|
+
"",
|
|
1009
|
+
" ⚠ package.json's build script predates subpath-mount support.",
|
|
1010
|
+
` This site is mounted at "${urlBase}", which needs three things`,
|
|
1011
|
+
" the old script does not do: clear dist/ (astro build only clears",
|
|
1012
|
+
" the mount dir, so stale output ships forever), write the Pagefind",
|
|
1013
|
+
" bundle under the mount (or Cmd+K 404s), and lift _headers to the",
|
|
1014
|
+
" assets root (or the llms.txt Link header stops applying).",
|
|
1015
|
+
"",
|
|
1016
|
+
" Change the build script to:",
|
|
1017
|
+
"",
|
|
1018
|
+
` ${wanted}`,
|
|
1019
|
+
"",
|
|
1020
|
+
].join("\n"));
|
|
1021
|
+
}
|
|
631
1022
|
export function emitSiteScaffold(outputDir, siteName, options, writeScaffold) {
|
|
632
1023
|
let scaffoldFilesSkipped = 0;
|
|
633
1024
|
// Ensure dirs exist when called standalone (not via the orchestrator).
|
|
@@ -651,10 +1042,32 @@ export function emitSiteScaffold(outputDir, siteName, options, writeScaffold) {
|
|
|
651
1042
|
try {
|
|
652
1043
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
653
1044
|
const pkg = JSON.parse(readFileSync(join(here, "..", "package.json"), "utf-8"));
|
|
654
|
-
// Caret on a stable version, exact pin on a prerelease
|
|
655
|
-
//
|
|
656
|
-
//
|
|
657
|
-
//
|
|
1045
|
+
// Caret on a stable version, exact pin on a prerelease.
|
|
1046
|
+
//
|
|
1047
|
+
// The reason given here used to be that "npm treats
|
|
1048
|
+
// `^0.2.0-beta.2` as NOT matching `0.2.0-beta.3`". That is FALSE.
|
|
1049
|
+
// Checked against semver 7:
|
|
1050
|
+
//
|
|
1051
|
+
// ^0.2.0-beta.96 matches 0.2.0-beta.97 -> true
|
|
1052
|
+
// ^0.2.0-beta.96 matches 0.2.0 -> true
|
|
1053
|
+
// ^0.2.0-beta.96 matches 0.2.1-beta.1 -> false
|
|
1054
|
+
//
|
|
1055
|
+
// A caret DOES pick up later prereleases of the same
|
|
1056
|
+
// major.minor.patch tuple. What it will not do is cross to a
|
|
1057
|
+
// prerelease of a DIFFERENT tuple — probably what the original
|
|
1058
|
+
// note was reaching for.
|
|
1059
|
+
//
|
|
1060
|
+
// The pin stays, for a reason that is actually true: a scaffolded
|
|
1061
|
+
// site commits its generated `astro/` output, so an exact version
|
|
1062
|
+
// guarantees the packages that produced the committed source are
|
|
1063
|
+
// the ones CI installs. A floating range lets a lockfile refresh
|
|
1064
|
+
// change a deployed site with nothing in the diff to show it.
|
|
1065
|
+
//
|
|
1066
|
+
// Our own site repos (dogsbay-ai-blog, dogsbay-ai-site) opt OUT
|
|
1067
|
+
// of that and track the `beta` dist-tag instead, because we are
|
|
1068
|
+
// the only consumers before release and the per-publish version
|
|
1069
|
+
// bump is not worth the commit. Their .gitignore explains the
|
|
1070
|
+
// trade and what to reverse at release.
|
|
658
1071
|
return /-/.test(pkg.version) ? pkg.version : `^${pkg.version}`;
|
|
659
1072
|
}
|
|
660
1073
|
catch {
|
|
@@ -677,6 +1090,21 @@ export function emitSiteScaffold(outputDir, siteName, options, writeScaffold) {
|
|
|
677
1090
|
const deployDevDeps = isCloudflare
|
|
678
1091
|
? { wrangler: "^4.0.0" }
|
|
679
1092
|
: {};
|
|
1093
|
+
// `--local` outside a workspace also needs a hoisted node_modules.
|
|
1094
|
+
//
|
|
1095
|
+
// With pnpm's default isolated layout, `file:`-linked packages resolve their
|
|
1096
|
+
// own dependencies from their own store path, and Astro's transitives then
|
|
1097
|
+
// fail to resolve from the SITE (measured: "Rolldown failed to resolve import
|
|
1098
|
+
// @astrojs/internal-helpers/path"). Hoisting is what the linked-package case
|
|
1099
|
+
// wants and costs nothing here — the site is disposable derived output, not a
|
|
1100
|
+
// library whose dependency isolation anyone relies on.
|
|
1101
|
+
if (options.local && !insideWs && writeScaffold) {
|
|
1102
|
+
writeFileSync(join(outputDir, ".npmrc"), "node-linker=hoisted\n");
|
|
1103
|
+
}
|
|
1104
|
+
// Subpath mounts move the Astro output to dist/<urlBase>/, so Pagefind
|
|
1105
|
+
// must write its bundle there too — see the build script below.
|
|
1106
|
+
const scaffoldUrlBase = parseSiteUrl(options.siteUrl).urlBase;
|
|
1107
|
+
const scaffoldOutDir = workersSubpathOutDir(options.deploy, scaffoldUrlBase);
|
|
680
1108
|
// package.json — scaffold-once. Maintainers add their own deps; the
|
|
681
1109
|
// detection of "already scaffolded" actually keys off this file's
|
|
682
1110
|
// existence, so it's also our sentinel.
|
|
@@ -690,12 +1118,27 @@ export function emitSiteScaffold(outputDir, siteName, options, writeScaffold) {
|
|
|
690
1118
|
dev: "astro dev",
|
|
691
1119
|
// Pagefind runs after astro build; indexes the static dist/ output and
|
|
692
1120
|
// writes `dist/pagefind/` for the search UI to load lazily on Cmd+K.
|
|
693
|
-
|
|
1121
|
+
//
|
|
1122
|
+
// Under a subpath mount the Astro output moves to dist/<urlBase>/,
|
|
1123
|
+
// but `--site` stays `dist` on purpose: Pagefind derives result
|
|
1124
|
+
// URLs from the indexed root, so indexing dist/ keeps them as
|
|
1125
|
+
// `/<urlBase>/page/`. Only the BUNDLE has to move, or the page
|
|
1126
|
+
// requests `/<urlBase>/pagefind/pagefind.js` (the pagefindUrl prop
|
|
1127
|
+
// is built from the combined prefix) and gets a 404 — a path that
|
|
1128
|
+
// is not even matched by the emitted `<host>/<urlBase>/*` route,
|
|
1129
|
+
// so Cmd+K would be unreachable by any URL.
|
|
1130
|
+
// Subpath mounts bracket the build with dogsbay-mount.mjs:
|
|
1131
|
+
// `clean` clears dist/ (astro build only clears the MOUNT dir,
|
|
1132
|
+
// so stale layouts would ship forever), and `finalize` lifts
|
|
1133
|
+
// _headers to the assets root, where Workers actually reads it.
|
|
1134
|
+
build: scaffoldOutDir
|
|
1135
|
+
? `node ./dogsbay-mount.mjs clean && astro build && pagefind --site dist --output-path ${scaffoldOutDir.replace(/^\.\//, "")}/pagefind && node ./dogsbay-mount.mjs finalize`
|
|
1136
|
+
: "astro build && pagefind --site dist",
|
|
694
1137
|
preview: "astro preview",
|
|
695
1138
|
...deployScripts,
|
|
696
1139
|
},
|
|
697
1140
|
dependencies: {
|
|
698
|
-
astro: "^
|
|
1141
|
+
astro: "^7.0.4",
|
|
699
1142
|
// Sitemap is emitted directly by Dogsbay into
|
|
700
1143
|
// public/<basePath>/sitemap-{index,0}.xml so multi-mount
|
|
701
1144
|
// deploys don't collide at the host root. We deliberately
|
|
@@ -706,15 +1149,14 @@ export function emitSiteScaffold(outputDir, siteName, options, writeScaffold) {
|
|
|
706
1149
|
// include it; the produced search index is shipped statically and
|
|
707
1150
|
// doesn't load this dep at runtime.
|
|
708
1151
|
pagefind: "^1.4.0",
|
|
709
|
-
tailwindcss: "^4.
|
|
710
|
-
//
|
|
711
|
-
//
|
|
712
|
-
//
|
|
713
|
-
//
|
|
714
|
-
//
|
|
715
|
-
//
|
|
716
|
-
|
|
717
|
-
"@tailwindcss/vite": "~4.2.2",
|
|
1152
|
+
tailwindcss: "^4.3.2",
|
|
1153
|
+
// 4.3.x required on Astro 7: 4.2.x fails against vite 7.3
|
|
1154
|
+
// ("rollupOptions.input should not be an html file when
|
|
1155
|
+
// building for SSR" — surfaced on a fresh comparison-site
|
|
1156
|
+
// build 2026-07-13). The OLD ~4.2.2 pin guarded an Astro 6
|
|
1157
|
+
// rolldown incompatibility that no longer applies; the
|
|
1158
|
+
// monorepo apps build on 4.3.2.
|
|
1159
|
+
"@tailwindcss/vite": "^4.3.2",
|
|
718
1160
|
"tailwind-variants": "^0.3.0",
|
|
719
1161
|
shiki: "^4.0.0",
|
|
720
1162
|
"@shikijs/transformers": "^4.0.0",
|
|
@@ -740,13 +1182,22 @@ export function emitSiteScaffold(outputDir, siteName, options, writeScaffold) {
|
|
|
740
1182
|
// @floating-ui/core" at astro build time.
|
|
741
1183
|
"@floating-ui/core": "^1.7.0",
|
|
742
1184
|
},
|
|
743
|
-
//
|
|
744
|
-
//
|
|
745
|
-
//
|
|
746
|
-
//
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
1185
|
+
// `--local` outside a workspace needs these.
|
|
1186
|
+
//
|
|
1187
|
+
// A `file:` dependency is installed from the package's OWN manifest,
|
|
1188
|
+
// and every @dogsbay/* manifest declares its siblings as `workspace:*`
|
|
1189
|
+
// — a protocol that only resolves inside the workspace. So `file:`
|
|
1190
|
+
// alone fails at install with ERR_PNPM_WORKSPACE_PKG_NOT_FOUND, naming
|
|
1191
|
+
// a transitive dependency the caller never wrote down. Overrides map
|
|
1192
|
+
// every sibling to the same checkout, which is what makes `--local`
|
|
1193
|
+
// installable rather than merely emitted.
|
|
1194
|
+
...(options.local && !insideWs ? { pnpm: { overrides: monorepoOverrides() } } : {}),
|
|
1195
|
+
// NOTE deliberately NO vite override: Astro 7 pairs with
|
|
1196
|
+
// vite 8. The old `overrides: { vite: "^7" }` (an Astro 6
|
|
1197
|
+
// era guard) FORCED vite 7 under astro 7.0.x and broke every
|
|
1198
|
+
// fresh scaffold build ("rollupOptions.input should not be
|
|
1199
|
+
// an html file when building for SSR") — root-caused
|
|
1200
|
+
// 2026-07-13 on the first standalone comparison-site build.
|
|
750
1201
|
...(Object.keys(deployDevDeps).length > 0
|
|
751
1202
|
? { devDependencies: deployDevDeps }
|
|
752
1203
|
: {}),
|
|
@@ -835,11 +1286,13 @@ import {
|
|
|
835
1286
|
dogsbaySite,
|
|
836
1287
|
dogsbayBase,
|
|
837
1288
|
dogsbayInlineStylesheets,
|
|
1289
|
+
dogsbayOutDir,
|
|
838
1290
|
} from "./astro.config.dogsbay.mjs";
|
|
839
1291
|
|
|
840
1292
|
export default defineConfig({
|
|
841
1293
|
...(dogsbaySite ? { site: dogsbaySite } : {}),
|
|
842
1294
|
...(dogsbayBase ? { base: dogsbayBase } : {}),
|
|
1295
|
+
...(dogsbayOutDir ? { outDir: dogsbayOutDir } : {}),
|
|
843
1296
|
output: "static",
|
|
844
1297
|
build: {
|
|
845
1298
|
inlineStylesheets: dogsbayInlineStylesheets,
|
|
@@ -991,25 +1444,49 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
991
1444
|
// same shape); the duplicate is cheap (one file) and keeps the
|
|
992
1445
|
// build-time and runtime worlds cleanly separated. See
|
|
993
1446
|
// plans/client-rendered-nav.md.
|
|
994
|
-
|
|
1447
|
+
// …UNDER the basePath, because that is where the client fetches it
|
|
1448
|
+
// from (`${basePath}/_dogsbay/nav.json`) and because two sites mounted
|
|
1449
|
+
// on one origin — e.g. two release-comparison pairs — would otherwise
|
|
1450
|
+
// both claim `/_dogsbay/nav.json` and collide. Found by the comparison
|
|
1451
|
+
// acceptance suite: the nav silently never hydrated (404), so no page
|
|
1452
|
+
// marks rendered at all.
|
|
1453
|
+
const publicNavDir = join(outputDir, "public", ...basePathSegments(normalizeBasePath(options.basePath)), "_dogsbay");
|
|
995
1454
|
mkdirSync(publicNavDir, { recursive: true });
|
|
996
1455
|
writeFileSync(join(publicNavDir, "nav.json"), JSON.stringify(outputNav));
|
|
997
1456
|
// Static assets (images etc.) — content-tier; always copy from the
|
|
998
1457
|
// user's source dir. If they removed an asset, we want it gone here
|
|
999
1458
|
// too. Skipped when sourceDir isn't supplied (programmatic callers
|
|
1000
1459
|
// that want pure page emission).
|
|
1001
|
-
if (options.
|
|
1460
|
+
if (options.assetSourceDirs && options.assetSourceDirs.length > 0) {
|
|
1461
|
+
// Multi-source: copy EACH source's assets under its version/locale
|
|
1462
|
+
// URL prefix (public/<prefix>/...), matching the per-source ref
|
|
1463
|
+
// rewrite. Replaces the single-sourceDir copy, which only handled
|
|
1464
|
+
// the first source and never version-namespaced.
|
|
1465
|
+
for (const { dir, urlPrefix } of options.assetSourceDirs) {
|
|
1466
|
+
copyAssets(dir, outputDir, options.imageOptimization, urlPrefix);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
else if (options.sourceDir) {
|
|
1002
1470
|
copyAssets(options.sourceDir, outputDir, options.imageOptimization);
|
|
1003
1471
|
}
|
|
1004
1472
|
// External asset mounts (e.g. a Docusaurus `static/` dir) → public/_assets/,
|
|
1005
1473
|
// preserving internal structure so the importer's `/_assets/...` image refs
|
|
1006
1474
|
// resolve. Used by the convert --to astro path; the dogsbay-md → site build
|
|
1007
1475
|
// path instead lands these under content/_assets and copyAssets picks them up.
|
|
1476
|
+
// A mount may target an `_assets` subdir (`into`) and restrict itself to
|
|
1477
|
+
// media files (`mediaOnly`) — used for docs-co-located images where the
|
|
1478
|
+
// mounted dir is the docs tree itself.
|
|
1008
1479
|
if (options.assetMounts) {
|
|
1009
1480
|
for (const mount of options.assetMounts) {
|
|
1010
|
-
if (existsSync(mount.dir))
|
|
1011
|
-
|
|
1012
|
-
|
|
1481
|
+
if (!existsSync(mount.dir))
|
|
1482
|
+
continue;
|
|
1483
|
+
const dest = mount.into
|
|
1484
|
+
? join(outputDir, "public", "_assets", mount.into)
|
|
1485
|
+
: join(outputDir, "public", "_assets");
|
|
1486
|
+
if (mount.mediaOnly)
|
|
1487
|
+
copyMediaTree(mount.dir, dest);
|
|
1488
|
+
else
|
|
1489
|
+
cpSync(mount.dir, dest, { recursive: true });
|
|
1013
1490
|
}
|
|
1014
1491
|
}
|
|
1015
1492
|
let generated = 0;
|
|
@@ -1020,6 +1497,7 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
1020
1497
|
// plans/dir-index-slug-nav-drop.md.
|
|
1021
1498
|
let rootIndexEmitted = false;
|
|
1022
1499
|
const generatedPaths = new Set();
|
|
1500
|
+
let pageFailures = 0;
|
|
1023
1501
|
const pagesDir = join(outputDir, "src", "pages", ...baseSegments);
|
|
1024
1502
|
const useImageOpt = options.imageOptimization ?? false;
|
|
1025
1503
|
// hrefPrefix is the COMBINED prefix (urlBase + basePath) — what
|
|
@@ -1050,30 +1528,92 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
1050
1528
|
// prefix (exact path-prefix). `welcome/_internal` excludes
|
|
1051
1529
|
// only that specific path tree, not arbitrary `_internal/`
|
|
1052
1530
|
// elsewhere.
|
|
1053
|
-
const
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1531
|
+
const isExcludedSlug = makeSlugExcluder(options.excludeFromRoutes);
|
|
1532
|
+
// Granularity (additive book view): roll each top-level nav group into one
|
|
1533
|
+
// `_book` page emitted alongside the topics. Kept out of nav; quarantined via
|
|
1534
|
+
// noindex + excludeFromSearch frontmatter so robots/sitemap/pagefind exclude
|
|
1535
|
+
// it, and — being local to this emitter — it never reaches the llms.txt builder
|
|
1536
|
+
// either. The `_book` page renders through the same path as any other page.
|
|
1537
|
+
// See plans/granularity-views.md.
|
|
1538
|
+
const bookPages = [];
|
|
1539
|
+
// topic slug → "<bookRoute>#<anchor>" — the per-topic "Read as single page"
|
|
1540
|
+
// button target. Empty unless granularity.book is on.
|
|
1541
|
+
const singlePageBySlug = new Map();
|
|
1542
|
+
// Emit a single-page (book) per nav node whose EFFECTIVE granularity resolves
|
|
1543
|
+
// to "yes". The annotation lives on each nav node (`singlePage`):
|
|
1544
|
+
// - "yes" → roll up this node's subtree into one page.
|
|
1545
|
+
// - "children" → make each direct child default to "yes" (chapters).
|
|
1546
|
+
// - "no" → no page (the default).
|
|
1547
|
+
// `granularity.book: true` is the legacy all-on switch: it seeds the top level
|
|
1548
|
+
// as "yes", so every top-level group rolls up — a node's own annotation still
|
|
1549
|
+
// wins. (This comment previously said "children", which is the same OUTCOME
|
|
1550
|
+
// but the wrong value, and disagreed with the config schema's wording.) A rollup whose estimated source exceeds maxSinglePageMB is SKIPPED with
|
|
1551
|
+
// a warning (no silent multi-minute page). See plans/granularity-views.md.
|
|
1552
|
+
const maxBookBytes = Math.round((options.granularity?.maxSinglePageMB ?? 3) * 1024 * 1024);
|
|
1553
|
+
const rootInheritYes = options.granularity?.book === true;
|
|
1554
|
+
const emitBook = (nav, label) => {
|
|
1555
|
+
const book = assembleBook(nav, pages, { hrefPrefix: combined, title: label });
|
|
1556
|
+
if (!book)
|
|
1557
|
+
return;
|
|
1558
|
+
const bytes = estimateTreeBytes(book.page.tree);
|
|
1559
|
+
if (bytes > maxBookBytes) {
|
|
1560
|
+
console.warn(`[dogsbay] granularity: skipping single-page "${label}" — ~${(bytes / 1048576).toFixed(1)} MB ` +
|
|
1561
|
+
`exceeds maxSinglePageMB (${options.granularity?.maxSinglePageMB ?? 3}). ` +
|
|
1562
|
+
`Mark it "single-page: children" in the nav to split it into chapters.`);
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
book.page.frontmatter = { ...book.page.frontmatter, excludeFromSearch: true };
|
|
1566
|
+
bookPages.push(book.page);
|
|
1567
|
+
const bookRoute = combined ? `${combined}/${book.page.slug}` : `/${book.page.slug}`;
|
|
1568
|
+
for (const [slug, anchor] of book.topicAnchors) {
|
|
1569
|
+
singlePageBySlug.set(slug, `${bookRoute}#${anchor}`);
|
|
1570
|
+
}
|
|
1571
|
+
};
|
|
1572
|
+
const walkBooks = (items, inheritYes) => {
|
|
1573
|
+
for (const item of items) {
|
|
1574
|
+
const effective = item.singlePage ?? (inheritYes ? "yes" : "no");
|
|
1575
|
+
if (effective === "yes" && item.children?.length) {
|
|
1576
|
+
emitBook(item.children, item.label);
|
|
1068
1577
|
}
|
|
1578
|
+
if (item.children?.length)
|
|
1579
|
+
walkBooks(item.children, effective === "children");
|
|
1069
1580
|
}
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1581
|
+
};
|
|
1582
|
+
const hasAnnotation = (items) => items.some((i) => i.singlePage !== undefined || (i.children && hasAnnotation(i.children)));
|
|
1583
|
+
if (rootInheritYes || hasAnnotation(outputNav)) {
|
|
1584
|
+
walkBooks(outputNav, rootInheritYes);
|
|
1585
|
+
}
|
|
1586
|
+
const emitPages = bookPages.length ? [...pages, ...bookPages] : pages;
|
|
1587
|
+
// Blog mode: compute the post list ONCE, then read adjacency and
|
|
1588
|
+
// reading time per page out of it. Both derive from the same sorted
|
|
1589
|
+
// list, so a post's "next" and its neighbour's "prev" cannot disagree.
|
|
1590
|
+
// Book rollups are excluded — they are quarantined aggregates, not posts.
|
|
1591
|
+
const blogData = options.blog
|
|
1592
|
+
? buildBlogData(pages, {
|
|
1593
|
+
basePath,
|
|
1594
|
+
section: options.section,
|
|
1595
|
+
excludeSlug: isExcludedSlug,
|
|
1596
|
+
// Post links must carry the COMBINED prefix — the path the host
|
|
1597
|
+
// serves under. With basePath alone, a site mounted at /blog
|
|
1598
|
+
// emitted /second-post/, which is outside its own route.
|
|
1599
|
+
urlPrefix: combinedPrefix(options),
|
|
1600
|
+
config: options.blog,
|
|
1601
|
+
})
|
|
1602
|
+
: undefined;
|
|
1603
|
+
const blogAdj = blogData ? blogAdjacency(blogData) : undefined;
|
|
1604
|
+
const blogMinutes = new Map(blogData ? blogData.posts.map((post) => [post.slug, post.readingMinutes]) : []);
|
|
1605
|
+
for (const page of emitPages) {
|
|
1073
1606
|
try {
|
|
1074
1607
|
// Skip excluded pages before any expensive work (tree rewrite,
|
|
1075
1608
|
// serialize, IO).
|
|
1076
|
-
|
|
1609
|
+
// BOTH signals. `ExportPage.fragment` is the typed field
|
|
1610
|
+
// (`@dogsbay/types` format.ts) that format-docusaurus sets in structural
|
|
1611
|
+
// mode; `frontmatter._fragment` is the loader-stamped one. Checking only
|
|
1612
|
+
// the second emitted every Docusaurus partial as its own routable,
|
|
1613
|
+
// empty-titled, orphan page — duplicating content already inlined into
|
|
1614
|
+
// the pages that included it, and landing in sitemap and search.
|
|
1615
|
+
const isFragment = page.fragment === true ||
|
|
1616
|
+
(page.frontmatter && page.frontmatter._fragment === true);
|
|
1077
1617
|
if (isFragment || isExcludedSlug(page.slug)) {
|
|
1078
1618
|
continue;
|
|
1079
1619
|
}
|
|
@@ -1145,6 +1685,20 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
1145
1685
|
const pageCategory = Array.isArray(pageMeta?.category)
|
|
1146
1686
|
? pageMeta.category
|
|
1147
1687
|
: undefined;
|
|
1688
|
+
// "Last updated" date → DocsFooter. `updated` is the canonical PageMeta
|
|
1689
|
+
// field (parseMeta); also accept `lastUpdated` / `lastmod` for sitemap
|
|
1690
|
+
// parity. Plain string, rendered as-is.
|
|
1691
|
+
const rawLastUpdated = (typeof pageMeta?.updated === "string" && pageMeta.updated) ||
|
|
1692
|
+
(typeof fm.updated === "string" && fm.updated) ||
|
|
1693
|
+
(typeof fm.lastUpdated === "string" && fm.lastUpdated) ||
|
|
1694
|
+
(typeof fm.lastmod === "string" && fm.lastmod) ||
|
|
1695
|
+
null;
|
|
1696
|
+
// Canonical meta normalises to a full ISO datetime ("…T00:00:00.000Z").
|
|
1697
|
+
// Drop the time for display — these are dates, not timestamps. The footer
|
|
1698
|
+
// prettifies further; non-ISO strings pass through untouched.
|
|
1699
|
+
const pageLastUpdated = typeof rawLastUpdated === "string" && /^\d{4}-\d{2}-\d{2}T/.test(rawLastUpdated)
|
|
1700
|
+
? rawLastUpdated.slice(0, 10)
|
|
1701
|
+
: rawLastUpdated;
|
|
1148
1702
|
// Custom-taxonomy values lifted from frontmatter into
|
|
1149
1703
|
// `meta.taxonomies` by the importer (see `parseMeta` in
|
|
1150
1704
|
// `@dogsbay/types`). Surfaced to DocsLayout so it can emit one
|
|
@@ -1277,11 +1831,26 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
1277
1831
|
// this keeps prev/next aligned with what the reader
|
|
1278
1832
|
// sees.
|
|
1279
1833
|
`const _navForPagination = filterNavByAxis(nav as any[], {`,
|
|
1280
|
-
|
|
1834
|
+
// Pass the REAL basePath ("" for a root-served site) — coercing ""
|
|
1835
|
+
// → "/docs" makes the filter look for `/docs/…` bucket hrefs that
|
|
1836
|
+
// don't exist, emptying the pagination nav so prev/next vanish on
|
|
1837
|
+
// every page of a root-served multi-source site.
|
|
1838
|
+
` basePath: ${JSON.stringify(basePath ?? "")},`,
|
|
1839
|
+
` namespace: ${JSON.stringify(page.multiSource?.namespace ?? null)} ?? undefined,`,
|
|
1281
1840
|
` version: ${JSON.stringify(page.multiSource?.version ?? null)} ?? undefined,`,
|
|
1282
1841
|
` locale: ${JSON.stringify(page.multiSource?.locale ?? null)} ?? undefined,`,
|
|
1283
1842
|
`});`,
|
|
1284
|
-
|
|
1843
|
+
// Blog adjacency is CHRONOLOGICAL, not navigational. getPagination
|
|
1844
|
+
// walks NavItem[] — sidebar order — and a blog has no sidebar; its
|
|
1845
|
+
// neighbours are "the post before/after this one in time". Emitting
|
|
1846
|
+
// the pair as literals also means the page does no adjacency work at
|
|
1847
|
+
// runtime.
|
|
1848
|
+
...(blogAdj
|
|
1849
|
+
? [
|
|
1850
|
+
`const prev = ${JSON.stringify(blogAdj.get(page.slug)?.prev ?? null)} ?? undefined;`,
|
|
1851
|
+
`const next = ${JSON.stringify(blogAdj.get(page.slug)?.next ?? null)} ?? undefined;`,
|
|
1852
|
+
]
|
|
1853
|
+
: [`const { prev, next } = getPagination(currentPath, _navForPagination);`]),
|
|
1285
1854
|
`const title = ${JSON.stringify(page.title)};`,
|
|
1286
1855
|
`const description = ${JSON.stringify(pageDescription)} || undefined;`,
|
|
1287
1856
|
`const ogImage = ${JSON.stringify(pageOgImage)} || undefined;`,
|
|
@@ -1295,6 +1864,7 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
1295
1864
|
`const pageCategory = ${JSON.stringify(pageCategory ?? null)};`,
|
|
1296
1865
|
`const pageTaxonomies = ${JSON.stringify(pageTaxonomies ?? null)};`,
|
|
1297
1866
|
`const tagsIndexPath = ${JSON.stringify(tagsIndexPath)};`,
|
|
1867
|
+
`const lastUpdated = ${JSON.stringify(pageLastUpdated)} || undefined;`,
|
|
1298
1868
|
`const llmActionsProps = ${JSON.stringify(llmActionsEnabled
|
|
1299
1869
|
? {
|
|
1300
1870
|
enabled: true,
|
|
@@ -1340,10 +1910,12 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
1340
1910
|
` pageType={pageType ?? undefined}`,
|
|
1341
1911
|
` audience={pageAudience ?? undefined}`,
|
|
1342
1912
|
` category={pageCategory ?? undefined}`,
|
|
1913
|
+
` lastUpdated={lastUpdated}`,
|
|
1343
1914
|
` taxonomies={pageTaxonomies ?? undefined}`,
|
|
1344
1915
|
` autoH1={${autoH1}}`,
|
|
1345
1916
|
` autoLede={${autoLede}}`,
|
|
1346
1917
|
` llmActions={llmActionsProps}`,
|
|
1918
|
+
` linkIcons={siteConfig.linkIcons}`,
|
|
1347
1919
|
` multiSource={${JSON.stringify(page.multiSource ?? null)} ?? undefined}`,
|
|
1348
1920
|
` switcherMap={switcherMapData}`,
|
|
1349
1921
|
// basePath here is the COMBINED URL prefix (urlBase from
|
|
@@ -1368,12 +1940,37 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
1368
1940
|
` pagefindUrl={${JSON.stringify(combined ? `${combined}/pagefind/` : "/pagefind/")}}`,
|
|
1369
1941
|
// Favicon — composed with combined prefix so the
|
|
1370
1942
|
// <link rel="icon"> resolves on subpath-mounted deploys.
|
|
1371
|
-
//
|
|
1372
|
-
//
|
|
1373
|
-
//
|
|
1374
|
-
//
|
|
1375
|
-
`
|
|
1943
|
+
//
|
|
1944
|
+
// Emitted ONLY when the file actually exists in public/. The
|
|
1945
|
+
// scaffold ships no favicon, so an unconditional link meant every
|
|
1946
|
+
// generated site advertised an icon that 404s. Authors drop
|
|
1947
|
+
// `public/favicon.ico` (or `.svg`) in their Astro project and the
|
|
1948
|
+
// link appears on the next build; DocsLayout takes `false` to mean
|
|
1949
|
+
// "emit no <link rel=icon>".
|
|
1950
|
+
` favicon={${JSON.stringify(faviconHref(outputDir, parseSiteUrl(options.siteUrl).urlBase))}}`,
|
|
1376
1951
|
` wideLayout={${wideLayout}}`,
|
|
1952
|
+
// Blog chrome — sidebar dropped, byline on. Every other prop above
|
|
1953
|
+
// is unchanged, which is what keeps the blog visually continuous
|
|
1954
|
+
// with the docs rather than a lookalike.
|
|
1955
|
+
...(options.blog
|
|
1956
|
+
? [
|
|
1957
|
+
` chrome="blog"`,
|
|
1958
|
+
` author={${JSON.stringify(page.meta?.author ?? null)} ?? undefined}`,
|
|
1959
|
+
` publishedDate={${JSON.stringify(page.meta?.[options.blog.dateField] ?? null)} ?? undefined}`,
|
|
1960
|
+
` updatedDate={${JSON.stringify(page.meta?.updated ?? null)} ?? undefined}`,
|
|
1961
|
+
` readingMinutes={${blogMinutes.get(page.slug) ?? 1}}`,
|
|
1962
|
+
// Series position, derived in buildBlogData from publication
|
|
1963
|
+
// order — never hand-typed in prose, which goes stale the
|
|
1964
|
+
// moment another part lands.
|
|
1965
|
+
// String-guarded like buildBlogData's firstString: a
|
|
1966
|
+
// `heroImage: {src, alt}` object would otherwise reach
|
|
1967
|
+
// DocsLayout's `heroImage?: string` prop and render as
|
|
1968
|
+
// src="[object Object]".
|
|
1969
|
+
` heroImage={${JSON.stringify(typeof (page.frontmatter ?? {}).heroImage === "string"
|
|
1970
|
+
? (page.frontmatter ?? {}).heroImage
|
|
1971
|
+
: null)} ?? undefined}`,
|
|
1972
|
+
]
|
|
1973
|
+
: []),
|
|
1377
1974
|
` toc={${JSON.stringify(tocMode)}}`,
|
|
1378
1975
|
`>`,
|
|
1379
1976
|
// RightRail region — assigned to DocsLayout's `right-rail` named
|
|
@@ -1383,15 +1980,16 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
1383
1980
|
? [` <RightRailStack slot="right-rail" />`]
|
|
1384
1981
|
: []),
|
|
1385
1982
|
` <MarkdownContentStack>`,
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1983
|
+
// Every page body gets the docs-prose typography wrapper — wide
|
|
1984
|
+
// (endpoint) pages included. Mixed prose+endpoint pages (MDX
|
|
1985
|
+
// imports) previously lost ALL typography because the wrapper
|
|
1986
|
+
// was skipped; API-region internals opt out via the `dba-api`
|
|
1987
|
+
// marker on ApiLayout + the `:not(.dba-api *)` guards in the
|
|
1988
|
+
// generated .docs-prose rules.
|
|
1989
|
+
` <article class="docs-prose">`,
|
|
1990
|
+
...singlePageLinkLines(singlePageBySlug.get(page.slug), 6),
|
|
1991
|
+
result.body.split("\n").map((l) => ` ${l}`).join("\n"),
|
|
1992
|
+
` </article>`,
|
|
1395
1993
|
` </MarkdownContentStack>`,
|
|
1396
1994
|
`</DocsLayout>`,
|
|
1397
1995
|
];
|
|
@@ -1434,6 +2032,7 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
1434
2032
|
mkdirSync(dirname(mdEndpointPath), { recursive: true });
|
|
1435
2033
|
const endpointBody = buildMdEndpoint(page, sourceRel);
|
|
1436
2034
|
writeFileSync(mdEndpointPath, endpointBody);
|
|
2035
|
+
generatedPaths.add(relative(outputDir, mdEndpointPath));
|
|
1437
2036
|
// Sibling-level mirror for the index page under a non-empty
|
|
1438
2037
|
// basePath. baseSegments is empty when basePath is empty
|
|
1439
2038
|
// (root-served sites); in that case the index slug is just
|
|
@@ -1446,11 +2045,16 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
1446
2045
|
const siblingPath = join(outputDir, "src", "pages", ...parentSegments, `${lastSeg}.md.ts`);
|
|
1447
2046
|
mkdirSync(dirname(siblingPath), { recursive: true });
|
|
1448
2047
|
writeFileSync(siblingPath, endpointBody);
|
|
2048
|
+
generatedPaths.add(relative(outputDir, siblingPath));
|
|
1449
2049
|
}
|
|
1450
2050
|
}
|
|
1451
2051
|
}
|
|
1452
2052
|
catch (err) {
|
|
1453
2053
|
console.warn(`Warning: failed to generate ${page.slug}: ${err.message}`);
|
|
2054
|
+
// A failed page is absent from generatedPaths, which is
|
|
2055
|
+
// indistinguishable from a deleted one. Pruning is suppressed for
|
|
2056
|
+
// the whole run rather than deleting last build's still-good copy.
|
|
2057
|
+
pageFailures++;
|
|
1454
2058
|
}
|
|
1455
2059
|
}
|
|
1456
2060
|
// Generate index redirect at src/pages/index.astro — sends `/` to the
|
|
@@ -1466,14 +2070,67 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
1466
2070
|
// raw input `nav` — otherwise importers whose nav starts root-relative (e.g.
|
|
1467
2071
|
// the docusaurus convert path) emit a redirect that drops the URL base and
|
|
1468
2072
|
// sends `/` to the host root instead of into the site's subpath.
|
|
1469
|
-
|
|
2073
|
+
// ─── Per-version subtree index redirects ─────────────────────────
|
|
2074
|
+
// A version subtree (`/3.29/…`, `/latest/…`) owns no page of its
|
|
2075
|
+
// own, so `/latest/` 404s while `/latest/about` resolves — and
|
|
2076
|
+
// `/latest/` is exactly what a reader types, what a switcher link
|
|
2077
|
+
// truncates to, and what a "latest" alias is FOR. Give each bucket
|
|
2078
|
+
// the same contract the site root gets: redirect to its first nav
|
|
2079
|
+
// entry.
|
|
2080
|
+
const subtreeRoots = axisSubtreeRoots(pages);
|
|
2081
|
+
for (const slugPrefix of subtreeRoots.keys()) {
|
|
2082
|
+
const target = findFirstNavHrefUnder(outputNav, [`${combined}/${slugPrefix}`]);
|
|
2083
|
+
if (!target)
|
|
2084
|
+
continue;
|
|
2085
|
+
const subtreeIndexPath = join(pagesDir, section ? section : "", slugPrefix, "index.astro");
|
|
2086
|
+
const subtreeIndexRel = relative(outputDir, subtreeIndexPath);
|
|
2087
|
+
// A real page at this route wins — never clobber content.
|
|
2088
|
+
if (generatedPaths.has(subtreeIndexRel))
|
|
2089
|
+
continue;
|
|
2090
|
+
mkdirSync(dirname(subtreeIndexPath), { recursive: true });
|
|
2091
|
+
writeFileSync(subtreeIndexPath,
|
|
2092
|
+
// Marked as ours so a later emitter (the blog index claims this
|
|
2093
|
+
// exact route) can tell a generated stub from a page a writer
|
|
2094
|
+
// wrote. Without the marker the stub looked hand-authored and
|
|
2095
|
+
// silently blocked the blog index from ever being emitted.
|
|
2096
|
+
`---\n// AUTO-GENERATED by dogsbay site build — safe to replace.\nreturn Astro.redirect("${target}");\n---\n`);
|
|
2097
|
+
generatedPaths.add(subtreeIndexRel);
|
|
2098
|
+
}
|
|
2099
|
+
// The site root targets the DEFAULT version's first entry, not the
|
|
2100
|
+
// first entry overall. Nav is assembled oldest-version-first, so
|
|
2101
|
+
// `findFirstNavHref` alone sends every reader arriving at `/` into
|
|
2102
|
+
// the OLDEST docs — the opposite of what `defaultVersion` declares.
|
|
2103
|
+
const defaultVersionPrefixes = options.defaultVersion
|
|
2104
|
+
? [...subtreeRoots]
|
|
2105
|
+
.filter(([, version]) => version === options.defaultVersion)
|
|
2106
|
+
.map(([slugPrefix]) => `${combined}/${slugPrefix}`)
|
|
2107
|
+
: [];
|
|
2108
|
+
const firstHref = (defaultVersionPrefixes.length > 0
|
|
2109
|
+
? findFirstNavHrefUnder(outputNav, defaultVersionPrefixes)
|
|
2110
|
+
: undefined) ?? findFirstNavHref(outputNav, basePath);
|
|
1470
2111
|
const needsRootRedirect = basePath !== "" ||
|
|
1471
2112
|
(!section && !rootIndexEmitted && firstHref !== "");
|
|
1472
2113
|
if (needsRootRedirect) {
|
|
1473
2114
|
const indexPath = join(outputDir, "src", "pages", "index.astro");
|
|
1474
|
-
writeFileSync(indexPath, `---\nreturn Astro.redirect("${firstHref}");\n---\n`);
|
|
2115
|
+
writeFileSync(indexPath, `---\n// AUTO-GENERATED by dogsbay site build — safe to replace.\nreturn Astro.redirect("${firstHref}");\n---\n`);
|
|
1475
2116
|
generatedPaths.add(relative(outputDir, indexPath));
|
|
1476
2117
|
}
|
|
2118
|
+
// See pruneOrphanedPages for why each of these guards exists. They are
|
|
2119
|
+
// cheap; the failure they prevent is deleting a site.
|
|
2120
|
+
if (generatedPaths.size === 0) {
|
|
2121
|
+
// Zero pages is a broken input, not an empty site.
|
|
2122
|
+
}
|
|
2123
|
+
else if (pageFailures > 0) {
|
|
2124
|
+
console.warn(` Skipped pruning: ${pageFailures} page(s) failed to generate, so an ` +
|
|
2125
|
+
`orphan cannot be told from a failure.`);
|
|
2126
|
+
}
|
|
2127
|
+
else {
|
|
2128
|
+
const pruned = pruneOrphanedPages(outputDir, join(pagesDir, section ?? ""), generatedPaths);
|
|
2129
|
+
if (pruned.length > 0) {
|
|
2130
|
+
console.log(` Pruned ${pruned.length} page(s) whose source was deleted: ` +
|
|
2131
|
+
`${pruned.slice(0, 3).join(", ")}${pruned.length > 3 ? ", …" : ""}`);
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
1477
2134
|
return { generated, outputNav, generatedPaths };
|
|
1478
2135
|
}
|
|
1479
2136
|
/**
|
|
@@ -1649,11 +2306,22 @@ function composeAxisHeader(declared, seen, defaultId, allowEol) {
|
|
|
1649
2306
|
const out = [];
|
|
1650
2307
|
const seenInDeclared = new Set();
|
|
1651
2308
|
for (const d of declared) {
|
|
1652
|
-
|
|
2309
|
+
// A `hidden` version is the NUMBER an alias stands in for — the
|
|
2310
|
+
// Docusaurus `lastVersion` + `path: "latest"` pair. Its pages are
|
|
2311
|
+
// served under the alias, so no page ever carries its id and `seen`
|
|
2312
|
+
// will never contain it. Admit it anyway: it is label-only metadata
|
|
2313
|
+
// (the switcher filters hidden rows out of the dropdown), and it is
|
|
2314
|
+
// the sole source of the "3.32 (latest)" label. Gating it on `seen`
|
|
2315
|
+
// silently degrades that row to a bare "latest".
|
|
2316
|
+
const aliasedNumber = allowEol && d.hidden === true;
|
|
2317
|
+
if (seen.has(d.id) || aliasedNumber) {
|
|
1653
2318
|
out.push({
|
|
1654
2319
|
id: d.id,
|
|
1655
2320
|
...(d.label !== undefined ? { label: d.label } : {}),
|
|
2321
|
+
// eol + prerelease + hidden are version-only marks (allowEol gates them).
|
|
1656
2322
|
...(allowEol && d.eol === true ? { eol: true } : {}),
|
|
2323
|
+
...(allowEol && d.prerelease === true ? { prerelease: true } : {}),
|
|
2324
|
+
...(allowEol && d.hidden === true ? { hidden: true } : {}),
|
|
1657
2325
|
...(defaultId === d.id ? { default: true } : {}),
|
|
1658
2326
|
});
|
|
1659
2327
|
seenInDeclared.add(d.id);
|
|
@@ -1834,6 +2502,10 @@ export function emitAgentReadinessFiles(pages, outputNav, outputDir, siteName, o
|
|
|
1834
2502
|
mkdirSync(join(outputDir, "src"), { recursive: true });
|
|
1835
2503
|
writeFileSync(join(outputDir, "src", "middleware.ts"), buildMiddlewareSource({
|
|
1836
2504
|
mdMirror: mdMirrorOn,
|
|
2505
|
+
// Same combined prefix the axis redirect uses — the middleware
|
|
2506
|
+
// compares against the request URL, which carries the served
|
|
2507
|
+
// subpath.
|
|
2508
|
+
mdMirrorBasePath: combinedPrefix(options),
|
|
1837
2509
|
axisRedirect: axisRedirectOn
|
|
1838
2510
|
? {
|
|
1839
2511
|
// Middleware compares paths against the request URL,
|
|
@@ -1936,10 +2608,77 @@ function buildRobotsTxt(options, hasSiteUrl) {
|
|
|
1936
2608
|
// standards-compliant parsers. Emitted alongside Sitemap when
|
|
1937
2609
|
// siteUrl is set; absolute URLs only (relative paths would be
|
|
1938
2610
|
// ambiguous without a base).
|
|
1939
|
-
|
|
2611
|
+
// OPT-IN (`agent.llmsTxtDirective`), default off. RFC 9309 permits
|
|
2612
|
+
// unknown directives, but Lighthouse's validator reports this one as
|
|
2613
|
+
// `Unknown directive` and fails the "robots.txt is valid" audit —
|
|
2614
|
+
// measured: SEO 100 -> 92 on every page. Nothing consumes the line
|
|
2615
|
+
// either; `Llms-Txt:` is not part of the llms.txt proposal, and
|
|
2616
|
+
// agents find the file at its well-known path. A measured penalty
|
|
2617
|
+
// for a speculative benefit is not a sensible default.
|
|
2618
|
+
//
|
|
2619
|
+
// `llms.txt` itself is still emitted; this controls only the pointer.
|
|
2620
|
+
const llmsTxt = options.llmsTxtDirective === true &&
|
|
2621
|
+
options.llmsTxt !== false &&
|
|
2622
|
+
hasSiteUrl &&
|
|
2623
|
+
origin
|
|
1940
2624
|
? `Llms-Txt: ${origin}${withBasePath(combined, "/llms.txt")}\n`
|
|
1941
2625
|
: "";
|
|
1942
|
-
return
|
|
2626
|
+
return `${contentSignalPreamble(options)}User-agent: *\nAllow: /\n${contentSignal}${sitemap}${llmsTxt}`;
|
|
2627
|
+
}
|
|
2628
|
+
/**
|
|
2629
|
+
* The Article 4 reservation-of-rights preamble, as robots.txt comments.
|
|
2630
|
+
*
|
|
2631
|
+
* Opt-in via `agent.contentSignal.preamble`. Default off: it is 25
|
|
2632
|
+
* lines of legal text on every site, and it asserts a legal position
|
|
2633
|
+
* that is the operator's to take, not ours to take for them.
|
|
2634
|
+
*
|
|
2635
|
+
* Why it is worth having at all — `Content-Signal: ai-train=no` on its
|
|
2636
|
+
* own is a preference. Article 4(3) of EU Directive 2019/790 makes the
|
|
2637
|
+
* text-and-data-mining exception conditional on rights not having been
|
|
2638
|
+
* "expressly reserved in an appropriate manner", including
|
|
2639
|
+
* machine-readable means. The preamble is what turns the signal into
|
|
2640
|
+
* that express reservation, which is the difference between a request
|
|
2641
|
+
* and a reservation with legal effect in the EU.
|
|
2642
|
+
*
|
|
2643
|
+
* The wording is Cloudflare's Content Signals Policy text, which is
|
|
2644
|
+
* published under CC0 precisely so it can be reproduced. It is
|
|
2645
|
+
* reproduced rather than linked because a reservation has to be present
|
|
2646
|
+
* where the machine reads it.
|
|
2647
|
+
*
|
|
2648
|
+
* Kept BYTE-IDENTICAL to the published policy on purpose. It is a legal
|
|
2649
|
+
* instrument, not prose we own: reword it and a site is asserting
|
|
2650
|
+
* something subtly different from what its operator believes it is
|
|
2651
|
+
* asserting. `tests/robots-preamble.test.ts` pins the text.
|
|
2652
|
+
*/
|
|
2653
|
+
function contentSignalPreamble(options) {
|
|
2654
|
+
if (options.contentSignalPreamble !== true)
|
|
2655
|
+
return "";
|
|
2656
|
+
return `# As a condition of accessing this website, you agree to abide by the following
|
|
2657
|
+
# content signals:
|
|
2658
|
+
|
|
2659
|
+
# (a) If a Content-Signal = yes, you may collect content for the corresponding
|
|
2660
|
+
# use.
|
|
2661
|
+
# (b) If a Content-Signal = no, you may not collect content for the
|
|
2662
|
+
# corresponding use.
|
|
2663
|
+
# (c) If the website operator does not include a Content-Signal for a
|
|
2664
|
+
# corresponding use, the website operator neither grants nor restricts
|
|
2665
|
+
# permission via Content-Signal with respect to the corresponding use.
|
|
2666
|
+
|
|
2667
|
+
# The content signals and their meanings are:
|
|
2668
|
+
|
|
2669
|
+
# search: building a search index and providing search results (e.g., returning
|
|
2670
|
+
# hyperlinks and short excerpts from your website's contents). Search does not
|
|
2671
|
+
# include providing AI-generated search summaries.
|
|
2672
|
+
# ai-input: inputting content into one or more AI models (e.g., retrieval
|
|
2673
|
+
# augmented generation, grounding, or other real-time taking of content for
|
|
2674
|
+
# generative AI search answers).
|
|
2675
|
+
# ai-train: training or fine-tuning AI models.
|
|
2676
|
+
|
|
2677
|
+
# ANY RESTRICTIONS EXPRESSED VIA CONTENT SIGNALS ARE EXPRESS RESERVATIONS OF
|
|
2678
|
+
# RIGHTS UNDER ARTICLE 4 OF THE EUROPEAN UNION DIRECTIVE 2019/790 ON COPYRIGHT
|
|
2679
|
+
# AND RELATED RIGHTS IN THE DIGITAL SINGLE MARKET.
|
|
2680
|
+
|
|
2681
|
+
`;
|
|
1943
2682
|
}
|
|
1944
2683
|
/**
|
|
1945
2684
|
* Build the contents of `public/_headers` (Cloudflare Pages / Workers
|
|
@@ -1951,6 +2690,37 @@ function buildRobotsTxt(options, hasSiteUrl) {
|
|
|
1951
2690
|
* `/docs` mount) — matches where the platform actually emits
|
|
1952
2691
|
* llms.txt under the per-mount layout.
|
|
1953
2692
|
*/
|
|
2693
|
+
/**
|
|
2694
|
+
* URL for `<link rel="icon">`, or `false` when the project ships no
|
|
2695
|
+
* favicon.
|
|
2696
|
+
*
|
|
2697
|
+
* The scaffold deliberately ships none — authors drop their own into
|
|
2698
|
+
* `public/`. Emitting the link unconditionally therefore advertised a
|
|
2699
|
+
* 404 on every generated site. Checks `.ico` then `.svg`, matching the
|
|
2700
|
+
* two the scaffold documents.
|
|
2701
|
+
*/
|
|
2702
|
+
export function faviconHref(outputDir, urlBase) {
|
|
2703
|
+
// urlBase, NOT the combined prefix. `public/` is copied to the ROOT of
|
|
2704
|
+
// outDir, so a favicon serves from <urlBase>/favicon.ico on all three
|
|
2705
|
+
// layouts — mounted (outDir = dist/<urlBase>), GitHub Pages (host
|
|
2706
|
+
// supplies <urlBase>), and host-root. Composing with basePath as well
|
|
2707
|
+
// produced /blog/docs/favicon.ico for a file that serves from
|
|
2708
|
+
// /blog/favicon.ico: a 404 with a passing existence check in front of
|
|
2709
|
+
// it. Per-mount files (sitemap, llms.txt, nav.json) DO live under
|
|
2710
|
+
// public/<basePath>/ and legitimately use the combined prefix; the
|
|
2711
|
+
// root-level ones (favicon, robots.txt, _headers) do not.
|
|
2712
|
+
//
|
|
2713
|
+
// .png is in the list because it is the most common form — both
|
|
2714
|
+
// packages/cli/templates and apps/material-docs hardcode a .png — and
|
|
2715
|
+
// omitting it would silently emit no icon for authors who did the right
|
|
2716
|
+
// thing.
|
|
2717
|
+
for (const name of ["favicon.ico", "favicon.svg", "favicon.png"]) {
|
|
2718
|
+
if (existsSync(join(outputDir, "public", name))) {
|
|
2719
|
+
return urlBase ? `${urlBase}/${name}` : `/${name}`;
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
return false;
|
|
2723
|
+
}
|
|
1954
2724
|
function buildHeadersFile(basePath) {
|
|
1955
2725
|
const llmsHref = withBasePath(basePath, "/llms.txt");
|
|
1956
2726
|
return [
|
|
@@ -2006,7 +2776,7 @@ function buildMiddlewareSource(config) {
|
|
|
2006
2776
|
lines.push(" if (context.isPrerendered) return next();");
|
|
2007
2777
|
lines.push(" const url = new URL(context.request.url);");
|
|
2008
2778
|
if (config.mdMirror) {
|
|
2009
|
-
lines.push(' const accept = context.request.headers.get("accept");',
|
|
2779
|
+
lines.push(' const accept = context.request.headers.get("accept");', ` const mdTarget = shouldRewriteToMarkdown(accept, url.pathname, ${JSON.stringify(config.mdMirrorBasePath ?? "")});`, " if (mdTarget) return context.rewrite(mdTarget);");
|
|
2010
2780
|
}
|
|
2011
2781
|
if (config.axisRedirect) {
|
|
2012
2782
|
lines.push(" const axisTarget = shouldRedirectToDefaultVersion(", " url.pathname,", " AXIS_REDIRECT_CONFIG,", " );", " if (axisTarget) {", " // 302 (not 301) — the version + locale switchers let readers", " // navigate away from defaults, so we don't want browsers", " // permanently caching the unprefixed URL as default content.", " return Response.redirect(new URL(axisTarget, url.origin), 302);", " }");
|
|
@@ -2068,7 +2838,10 @@ function emitLlmsTxtFiles(outputDir, siteName, options, nav, pages) {
|
|
|
2068
2838
|
const baseSegments = basePathSegments(basePath);
|
|
2069
2839
|
const mountDir = join(outputDir, "public", ...baseSegments);
|
|
2070
2840
|
mkdirSync(mountDir, { recursive: true });
|
|
2071
|
-
writeFileSync(join(mountDir, "llms.txt"), buildLlmsTxt(siteConfig, nav, pages, {
|
|
2841
|
+
writeFileSync(join(mountDir, "llms.txt"), buildLlmsTxt(siteConfig, nav, pages, {
|
|
2842
|
+
hrefPrefix,
|
|
2843
|
+
aggregates: options.aggregates,
|
|
2844
|
+
}));
|
|
2072
2845
|
writeFileSync(join(mountDir, "llms-full.txt"), buildLlmsFullTxt(siteConfig, nav, pages, {
|
|
2073
2846
|
summary: "body",
|
|
2074
2847
|
serializePage: serializePageMd,
|
|
@@ -2142,7 +2915,13 @@ function emitSitemapFiles(outputDir, options, pages) {
|
|
|
2142
2915
|
basePath: combined,
|
|
2143
2916
|
siteNoindex: options.noindex === true,
|
|
2144
2917
|
}));
|
|
2145
|
-
writeFileSync(join(mountDir, "sitemap-index.xml"), buildSitemapIndex({
|
|
2918
|
+
writeFileSync(join(mountDir, "sitemap-index.xml"), buildSitemapIndex({
|
|
2919
|
+
siteUrl: options.siteUrl,
|
|
2920
|
+
basePath: combined,
|
|
2921
|
+
extraSitemaps: (options.aggregates ?? [])
|
|
2922
|
+
.map((a) => a.sitemap)
|
|
2923
|
+
.filter((u) => !!u),
|
|
2924
|
+
}));
|
|
2146
2925
|
}
|
|
2147
2926
|
/**
|
|
2148
2927
|
* Pick a directory under `public/` for a top-level nav group. Prefers
|
|
@@ -2196,6 +2975,55 @@ function findFirstNavHref(items, fallback) {
|
|
|
2196
2975
|
}
|
|
2197
2976
|
return fallback;
|
|
2198
2977
|
}
|
|
2978
|
+
/**
|
|
2979
|
+
* URL subtree root of every multi-source axis bucket, mapped to the
|
|
2980
|
+
* version that bucket carries. Keys are slug prefixes (no leading or
|
|
2981
|
+
* trailing slash), e.g. `3.29` or `calico/en/3.29`.
|
|
2982
|
+
*
|
|
2983
|
+
* `multiSource.originalSlug` is the slug as the importer produced it,
|
|
2984
|
+
* BEFORE the loader applied the axis prefix — so stripping it off the
|
|
2985
|
+
* emitted slug yields that bucket's prefix exactly. Deriving it that
|
|
2986
|
+
* way rather than by segment index matters because how many segments
|
|
2987
|
+
* precede the version depends on which axes are active
|
|
2988
|
+
* (`/<namespace>/<locale>/<version>/<slug>`), and a longest-common-
|
|
2989
|
+
* prefix guess collapses to nothing when two products share a version
|
|
2990
|
+
* id.
|
|
2991
|
+
*/
|
|
2992
|
+
function axisSubtreeRoots(pages) {
|
|
2993
|
+
const roots = new Map();
|
|
2994
|
+
for (const page of pages) {
|
|
2995
|
+
const meta = page.multiSource;
|
|
2996
|
+
if (!meta?.version || meta.originalSlug === undefined)
|
|
2997
|
+
continue;
|
|
2998
|
+
if (!page.slug.endsWith(meta.originalSlug))
|
|
2999
|
+
continue;
|
|
3000
|
+
const prefix = page.slug
|
|
3001
|
+
.slice(0, page.slug.length - meta.originalSlug.length)
|
|
3002
|
+
.replace(/^\/+|\/+$/g, "");
|
|
3003
|
+
if (prefix)
|
|
3004
|
+
roots.set(prefix, meta.version);
|
|
3005
|
+
}
|
|
3006
|
+
return roots;
|
|
3007
|
+
}
|
|
3008
|
+
/**
|
|
3009
|
+
* First nav href (in nav order) living under any of `prefixes`, which
|
|
3010
|
+
* are URL paths without a trailing slash. Used to pick a redirect
|
|
3011
|
+
* target for a subtree root that has no page of its own.
|
|
3012
|
+
*/
|
|
3013
|
+
function findFirstNavHrefUnder(items, prefixes) {
|
|
3014
|
+
for (const item of items) {
|
|
3015
|
+
const href = item.href;
|
|
3016
|
+
if (href && prefixes.some((prefix) => href.startsWith(`${prefix}/`))) {
|
|
3017
|
+
return href;
|
|
3018
|
+
}
|
|
3019
|
+
if (item.children) {
|
|
3020
|
+
const found = findFirstNavHrefUnder(item.children, prefixes);
|
|
3021
|
+
if (found)
|
|
3022
|
+
return found;
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
return undefined;
|
|
3026
|
+
}
|
|
2199
3027
|
function copyComponents(outputDir) {
|
|
2200
3028
|
const componentsSource = resolveComponentsSource();
|
|
2201
3029
|
if (!componentsSource)
|
|
@@ -2235,9 +3063,38 @@ function copyComponents(outputDir) {
|
|
|
2235
3063
|
}
|
|
2236
3064
|
}
|
|
2237
3065
|
}
|
|
2238
|
-
|
|
3066
|
+
/** Media files worth carrying from a `mediaOnly` asset mount. */
|
|
3067
|
+
const MOUNT_MEDIA_EXTS = new Set([
|
|
3068
|
+
".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif",
|
|
3069
|
+
".svg", ".ico", ".pdf", ".mp4", ".webm",
|
|
3070
|
+
]);
|
|
3071
|
+
/** Recursively copy only media files, preserving relative structure. */
|
|
3072
|
+
function copyMediaTree(srcDir, destDir) {
|
|
3073
|
+
for (const entry of readdirSync(srcDir)) {
|
|
3074
|
+
const full = join(srcDir, entry);
|
|
3075
|
+
if (statSync(full).isDirectory()) {
|
|
3076
|
+
copyMediaTree(full, join(destDir, entry));
|
|
3077
|
+
continue;
|
|
3078
|
+
}
|
|
3079
|
+
const dot = entry.lastIndexOf(".");
|
|
3080
|
+
if (dot === -1 || !MOUNT_MEDIA_EXTS.has(entry.slice(dot).toLowerCase()))
|
|
3081
|
+
continue;
|
|
3082
|
+
mkdirSync(destDir, { recursive: true });
|
|
3083
|
+
cpSync(full, join(destDir, entry));
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
function copyAssets(sourceDir, outputDir, imageOptimization, urlPrefix) {
|
|
2239
3087
|
// sourceDir is already the docs dir (e.g. .../fastapi/docs/en/docs)
|
|
2240
3088
|
const searchDir = sourceDir;
|
|
3089
|
+
// A version/locale/namespace URL segment (e.g. "8.8") the content's
|
|
3090
|
+
// asset refs were rewritten with — so each source's assets land under
|
|
3091
|
+
// public/<prefix>/... and same-path images across versions never
|
|
3092
|
+
// collide. Empty for single-source builds (today's flat behaviour).
|
|
3093
|
+
const prefixParts = (urlPrefix ?? "")
|
|
3094
|
+
.split("/")
|
|
3095
|
+
.map((p) => p.trim())
|
|
3096
|
+
.filter(Boolean);
|
|
3097
|
+
const withPrefix = (rel) => prefixParts.length > 0 ? join(...prefixParts, rel) : rel;
|
|
2241
3098
|
// Raster images benefit from Astro optimization (WebP, dimensions)
|
|
2242
3099
|
const optimizableExts = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
|
|
2243
3100
|
// SVGs, icons, PDFs always go to public/ (no optimization needed)
|
|
@@ -2251,7 +3108,7 @@ function copyAssets(sourceDir, outputDir, imageOptimization) {
|
|
|
2251
3108
|
else {
|
|
2252
3109
|
const ext = entry.substring(entry.lastIndexOf(".")).toLowerCase();
|
|
2253
3110
|
if (optimizableExts.has(ext)) {
|
|
2254
|
-
const rel = relative(searchDir, full);
|
|
3111
|
+
const rel = withPrefix(relative(searchDir, full));
|
|
2255
3112
|
// Always copy to public/ so inline <img src="/..."> works
|
|
2256
3113
|
const pubDest = join(outputDir, "public", rel);
|
|
2257
3114
|
mkdirSync(dirname(pubDest), { recursive: true });
|
|
@@ -2266,7 +3123,7 @@ function copyAssets(sourceDir, outputDir, imageOptimization) {
|
|
|
2266
3123
|
}
|
|
2267
3124
|
else if (passthroughExts.has(ext)) {
|
|
2268
3125
|
// SVGs, icons, PDFs always go to public/
|
|
2269
|
-
const rel = relative(searchDir, full);
|
|
3126
|
+
const rel = withPrefix(relative(searchDir, full));
|
|
2270
3127
|
const dest = join(outputDir, "public", rel);
|
|
2271
3128
|
mkdirSync(dirname(dest), { recursive: true });
|
|
2272
3129
|
cpSync(full, dest);
|
|
@@ -2310,6 +3167,22 @@ function generateGlobalCss() {
|
|
|
2310
3167
|
return `@import "tailwindcss";
|
|
2311
3168
|
@import "./theme.css";
|
|
2312
3169
|
|
|
3170
|
+
/* Custom elements have no user-agent style, so an unstyled one is
|
|
3171
|
+
display:inline — on which w-full does nothing and a border paints
|
|
3172
|
+
as detached fragments rather than around the content. @dogsbay/elements
|
|
3173
|
+
injects these same defaults at runtime, which is what reaches EXISTING
|
|
3174
|
+
sites (global.css is scaffolded once and never rewritten). Having them
|
|
3175
|
+
here too means a new site is correct at CSS time, with no flash before
|
|
3176
|
+
the module script runs.
|
|
3177
|
+
|
|
3178
|
+
:where() keeps specificity at zero, so any utility class wins. */
|
|
3179
|
+
:where(db-tabs, db-accordion, db-collapsible, db-card, db-steps, db-code-block) {
|
|
3180
|
+
display: block;
|
|
3181
|
+
}
|
|
3182
|
+
:where(db-link-button) {
|
|
3183
|
+
display: inline-block;
|
|
3184
|
+
}
|
|
3185
|
+
|
|
2313
3186
|
/* Scan @dogsbay packages for Tailwind classes */
|
|
2314
3187
|
@source "../../node_modules/@dogsbay/ui/src";
|
|
2315
3188
|
@source "../../node_modules/@dogsbay/docs-layout/src";
|
|
@@ -2322,52 +3195,120 @@ function generateGlobalCss() {
|
|
|
2322
3195
|
demo cells with no visible background. */
|
|
2323
3196
|
@source inline("${buildToneSafelist()}");
|
|
2324
3197
|
|
|
2325
|
-
/* Prose typography for rendered content
|
|
3198
|
+
/* Prose typography for rendered content.
|
|
3199
|
+
Every element rule is guarded with :not(.dba-api *): API-reference
|
|
3200
|
+
regions (ApiLayout root carries the dba-api marker) compose their own
|
|
3201
|
+
typography from utility classes, and unguarded .docs-prose rules would
|
|
3202
|
+
out-specify them. The guard lets mixed prose+endpoint pages (MDX
|
|
3203
|
+
imports) keep full typography outside the cards. */
|
|
2326
3204
|
.docs-prose {
|
|
2327
3205
|
line-height: 1.7;
|
|
2328
3206
|
|
|
2329
|
-
& h1 { font-family: var(--font-heading); font-size: 2rem; font-weight: 700; margin-top: 0; margin-bottom: 1rem; line-height: 1.2; letter-spacing: -0.025em; }
|
|
2330
|
-
& h2 { font-family: var(--font-heading); font-size: 1.5rem; font-weight: 600; margin-top: 2.5rem; margin-bottom: 0.75rem; line-height: 1.3; border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; letter-spacing: -0.015em; }
|
|
2331
|
-
& h3 { font-family: var(--font-heading); font-size: 1.25rem; font-weight: 600; margin-top: 2rem; margin-bottom: 0.5rem; line-height: 1.4; }
|
|
2332
|
-
& h4 { font-family: var(--font-heading); font-size: 1.1rem; font-weight: 600; margin-top: 1.5rem; margin-bottom: 0.5rem; }
|
|
3207
|
+
& h1:not(.dba-api *) { font-family: var(--font-heading); font-size: 2rem; font-weight: 700; margin-top: 0; margin-bottom: 1rem; line-height: 1.2; letter-spacing: -0.025em; }
|
|
3208
|
+
& h2:not(.dba-api *) { font-family: var(--font-heading); font-size: 1.5rem; font-weight: 600; margin-top: 2.5rem; margin-bottom: 0.75rem; line-height: 1.3; border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; letter-spacing: -0.015em; }
|
|
3209
|
+
& h3:not(.dba-api *) { font-family: var(--font-heading); font-size: 1.25rem; font-weight: 600; margin-top: 2rem; margin-bottom: 0.5rem; line-height: 1.4; }
|
|
3210
|
+
& h4:not(.dba-api *) { font-family: var(--font-heading); font-size: 1.1rem; font-weight: 600; margin-top: 1.5rem; margin-bottom: 0.5rem; }
|
|
2333
3211
|
|
|
2334
|
-
& p { margin-top: 0.75rem; margin-bottom: 0.75rem; }
|
|
3212
|
+
& p:not(.dba-api *) { margin-top: 0.75rem; margin-bottom: 0.75rem; }
|
|
2335
3213
|
|
|
2336
|
-
|
|
2337
|
-
|
|
3214
|
+
/* The :not([data-variant]) guard exempts anchors that ARE components. A
|
|
3215
|
+
link-button renders as an <a> carrying bg-primary + text-primary-foreground
|
|
3216
|
+
and a data-variant attribute, and this rule repainted its text with
|
|
3217
|
+
var(--primary) — primary on primary: an unreadable solid block in light
|
|
3218
|
+
mode, a blank one in dark. The secondary variant survived only because its
|
|
3219
|
+
background is light enough to read primary-coloured text, which is why it
|
|
3220
|
+
looked like a colour bug in one button rather than a cascade bug in both.
|
|
3221
|
+
(No backticks in this comment: the whole stylesheet is a template
|
|
3222
|
+
literal.) */
|
|
3223
|
+
& a:not(.dba-api *):not([data-variant]) { color: var(--primary); text-decoration: underline; text-underline-offset: 2px; }
|
|
3224
|
+
& a:hover:not(.dba-api *):not([data-variant]) { opacity: 0.8; }
|
|
2338
3225
|
|
|
2339
|
-
& strong { font-weight: 600; }
|
|
2340
|
-
& code { font-size: 0.875em; padding: 0.15em 0.35em; border-radius: 0.25rem; background: var(--muted); font-family: var(--font-code, ui-monospace, monospace); }
|
|
2341
|
-
& pre code { padding: 0; background: none; font-size: 1em; }
|
|
3226
|
+
& strong:not(.dba-api *) { font-weight: 600; }
|
|
3227
|
+
& code:not(.dba-api *) { font-size: 0.875em; padding: 0.15em 0.35em; border-radius: 0.25rem; background: var(--muted); font-family: var(--font-code, ui-monospace, monospace); }
|
|
3228
|
+
& pre code:not(.dba-api *) { padding: 0; background: none; font-size: 1em; }
|
|
2342
3229
|
|
|
2343
3230
|
/* Spacing between consecutive block elements (code blocks, alerts, etc.) */
|
|
2344
3231
|
& > * + * { margin-top: 0.75rem; }
|
|
2345
|
-
& li > * + * { margin-top: 0.75rem; }
|
|
3232
|
+
& li > * + *:not(.dba-api *) { margin-top: 0.75rem; }
|
|
2346
3233
|
|
|
2347
|
-
& ul { list-style: disc; padding-left: 1.5rem; margin: 0.75rem 0; }
|
|
2348
|
-
& ol { list-style: decimal; padding-left: 1.5rem; margin: 0.75rem 0; }
|
|
2349
|
-
& li { margin: 0.25rem 0; }
|
|
2350
|
-
& li > ul, & li > ol { margin: 0.25rem 0; }
|
|
3234
|
+
& ul:not(.dba-api *) { list-style: disc; padding-left: 1.5rem; margin: 0.75rem 0; }
|
|
3235
|
+
& ol:not(.dba-api *) { list-style: decimal; padding-left: 1.5rem; margin: 0.75rem 0; }
|
|
3236
|
+
& li:not(.dba-api *) { margin: 0.25rem 0; }
|
|
3237
|
+
& li > ul:not(.dba-api *), & li > ol:not(.dba-api *) { margin: 0.25rem 0; }
|
|
2351
3238
|
|
|
2352
|
-
& blockquote { border-left: 4px solid var(--border); padding-left: 1rem; color: var(--muted-foreground); font-style: italic; margin: 1rem 0; }
|
|
3239
|
+
& blockquote:not(.dba-api *) { border-left: 4px solid var(--border); padding-left: 1rem; color: var(--muted-foreground); font-style: italic; margin: 1rem 0; }
|
|
2353
3240
|
|
|
2354
|
-
& hr { border: none; border-top: 1px solid var(--border); margin: 2rem 0; }
|
|
3241
|
+
& hr:not(.dba-api *) { border: none; border-top: 1px solid var(--border); margin: 2rem 0; }
|
|
2355
3242
|
|
|
2356
|
-
& table { width: 100%; border-collapse: collapse; margin: 1rem 0; font-size: 0.875rem; }
|
|
2357
|
-
& th { text-align: left; font-weight: 600; padding: 0.5rem; border-bottom: 2px solid var(--border); }
|
|
2358
|
-
& td { padding: 0.5rem; border-bottom: 1px solid var(--border); }
|
|
3243
|
+
& table:not(.dba-api *) { width: 100%; border-collapse: collapse; margin: 1rem 0; font-size: 0.875rem; }
|
|
3244
|
+
& th:not(.dba-api *) { text-align: left; vertical-align: bottom; font-weight: 600; padding: 0.5rem; border-bottom: 2px solid var(--border); }
|
|
3245
|
+
& td:not(.dba-api *) { padding: 0.5rem; vertical-align: top; border-bottom: 1px solid var(--border); }
|
|
2359
3246
|
|
|
2360
|
-
& img { max-width: 100%; border-radius: 0.5rem; }
|
|
3247
|
+
& img:not(.dba-api *) { max-width: 100%; border-radius: 0.5rem; }
|
|
2361
3248
|
|
|
2362
3249
|
& .heading-anchor { text-decoration: none; opacity: 0; margin-right: 0.25rem; transition: opacity 0.2s; }
|
|
2363
3250
|
& h1:hover .heading-anchor, & h2:hover .heading-anchor, & h3:hover .heading-anchor, & h4:hover .heading-anchor { opacity: 0.4; }
|
|
2364
3251
|
|
|
2365
|
-
& details { border: 1px solid var(--border); border-radius: 0.5rem; padding: 1rem; margin: 1rem 0; }
|
|
2366
|
-
& summary { cursor: pointer; font-weight: 600; }
|
|
3252
|
+
& details:not(.dba-api *) { border: 1px solid var(--border); border-radius: 0.5rem; padding: 1rem; margin: 1rem 0; }
|
|
3253
|
+
& summary:not(.dba-api *) { cursor: pointer; font-weight: 600; }
|
|
3254
|
+
|
|
3255
|
+
/* mark is emitted by the highlight directive AND can arrive as raw HTML
|
|
3256
|
+
from an importer. Without a rule here it renders as the browser default
|
|
3257
|
+
yellow, which clashes in light mode and is unreadable on a dark page
|
|
3258
|
+
(nothing declares color-scheme). Token-based so it follows the theme. */
|
|
3259
|
+
& mark:not(.dba-api *) { background: var(--dsb-mark-bg, rgb(254 240 138 / 0.6)); color: inherit; border-radius: 0.125rem; padding: 0 0.125rem; }
|
|
3260
|
+
&:where(.dark *) mark:not(.dba-api *), .dark & mark:not(.dba-api *) { background: var(--dsb-mark-bg-dark, rgb(234 179 8 / 0.3)); }
|
|
3261
|
+
|
|
3262
|
+
& dl:not(.dba-api *) { margin: 1rem 0; }
|
|
3263
|
+
& dt:not(.dba-api *) { font-weight: 600; margin-top: 0.75rem; }
|
|
3264
|
+
& dd:not(.dba-api *) { margin-left: 1.5rem; color: var(--muted-foreground); }
|
|
3265
|
+
|
|
3266
|
+
/* In-cell admonitions. Inside a raw HTML table cell nothing
|
|
3267
|
+
markdown-native works, so the AsciiDoc engine emits a neutral
|
|
3268
|
+
dl.db-admonition shape (DD-010) and this rule renders it as a compact
|
|
3269
|
+
callout box — same contract the Obsidian plugin styles. Per-type
|
|
3270
|
+
accent from theme tokens; overrides the generic dl/dt/dd rules above. */
|
|
3271
|
+
& dl.db-admonition {
|
|
3272
|
+
margin: 0.75rem 0;
|
|
3273
|
+
padding: 0.5rem 0.75rem;
|
|
3274
|
+
border-left: 3px solid var(--info);
|
|
3275
|
+
border-radius: 0.25rem;
|
|
3276
|
+
background: color-mix(in oklab, var(--info) 8%, transparent);
|
|
3277
|
+
}
|
|
3278
|
+
& dl.db-admonition > dt {
|
|
3279
|
+
margin-top: 0;
|
|
3280
|
+
font-weight: 600;
|
|
3281
|
+
font-size: 0.8125rem;
|
|
3282
|
+
text-transform: uppercase;
|
|
3283
|
+
letter-spacing: 0.03em;
|
|
3284
|
+
color: var(--info);
|
|
3285
|
+
}
|
|
3286
|
+
& dl.db-admonition > dd { margin-left: 0; margin-top: 0.25rem; color: var(--foreground); }
|
|
3287
|
+
& dl.db-admonition-warning, & dl.db-admonition-caution {
|
|
3288
|
+
border-left-color: var(--warning);
|
|
3289
|
+
background: color-mix(in oklab, var(--warning) 8%, transparent);
|
|
3290
|
+
}
|
|
3291
|
+
& dl.db-admonition-warning > dt, & dl.db-admonition-caution > dt { color: var(--warning); }
|
|
3292
|
+
& dl.db-admonition-important {
|
|
3293
|
+
border-left-color: var(--destructive);
|
|
3294
|
+
background: color-mix(in oklab, var(--destructive) 8%, transparent);
|
|
3295
|
+
}
|
|
3296
|
+
& dl.db-admonition-important > dt { color: var(--destructive); }
|
|
2367
3297
|
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
3298
|
+
/* Granularity book-view affordances (plans/granularity-views.md):
|
|
3299
|
+
"Read as single page →" on topics, "Open as page ↗" on book sections.
|
|
3300
|
+
Deliberately subtle/muted — the book has one per section. */
|
|
3301
|
+
& .dsb-read-single-page {
|
|
3302
|
+
display: inline-block;
|
|
3303
|
+
margin: 0 0 1.25rem;
|
|
3304
|
+
font-size: 0.8125rem;
|
|
3305
|
+
color: var(--muted-foreground);
|
|
3306
|
+
text-decoration: none;
|
|
3307
|
+
}
|
|
3308
|
+
& .dsb-read-single-page:hover { color: var(--foreground); text-decoration: underline; }
|
|
3309
|
+
& .dsb-open-as-page { margin: -0.25rem 0 0.5rem; font-size: 0.75rem; }
|
|
3310
|
+
& .dsb-open-as-page a { color: var(--muted-foreground); text-decoration: none; }
|
|
3311
|
+
& .dsb-open-as-page a:hover { color: var(--foreground); text-decoration: underline; }
|
|
2371
3312
|
|
|
2372
3313
|
/* AsciiDoc block roles ([role="x"] / [.x]) preserved as classes (#384).
|
|
2373
3314
|
_abstract marks a module's lead/intro paragraph; .lead/.small are
|
|
@@ -2458,23 +3399,24 @@ const THEME_DEFAULT = {
|
|
|
2458
3399
|
--api-panel-fg: oklch(0.926 0.013 253.833);
|
|
2459
3400
|
|
|
2460
3401
|
/* API semantic colors — HTTP methods, types, status codes */
|
|
2461
|
-
|
|
2462
|
-
--api-
|
|
2463
|
-
--api-
|
|
2464
|
-
--api-
|
|
2465
|
-
--api-
|
|
2466
|
-
--api-
|
|
2467
|
-
--api-
|
|
2468
|
-
--api-
|
|
2469
|
-
--api-
|
|
2470
|
-
--api-type-
|
|
2471
|
-
--api-type-
|
|
2472
|
-
--api-type-
|
|
2473
|
-
--api-type-
|
|
2474
|
-
--api-
|
|
2475
|
-
--api-status-
|
|
2476
|
-
--api-status-
|
|
2477
|
-
--api-status-
|
|
3402
|
+
/* Light mode: darkened for WCAG AA on the 20%/10% tints. See .dark. */
|
|
3403
|
+
--api-get: oklch(0.435 0.095 162);
|
|
3404
|
+
--api-post: oklch(0.45 0.174 259);
|
|
3405
|
+
--api-put: oklch(0.455 0.105 63);
|
|
3406
|
+
--api-patch: oklch(0.455 0.13 45);
|
|
3407
|
+
--api-delete: oklch(0.45 0.179 22);
|
|
3408
|
+
--api-head: oklch(0.47 0.222 286);
|
|
3409
|
+
--api-required: oklch(0.45 0.179 22);
|
|
3410
|
+
--api-deprecated: oklch(0.455 0.105 63);
|
|
3411
|
+
--api-type-string: oklch(0.435 0.095 162);
|
|
3412
|
+
--api-type-number: oklch(0.45 0.174 259);
|
|
3413
|
+
--api-type-boolean: oklch(0.455 0.105 63);
|
|
3414
|
+
--api-type-object: oklch(0.47 0.222 286);
|
|
3415
|
+
--api-type-array: oklch(0.455 0.13 45);
|
|
3416
|
+
--api-status-2xx: oklch(0.435 0.095 162);
|
|
3417
|
+
--api-status-3xx: oklch(0.45 0.174 259);
|
|
3418
|
+
--api-status-4xx: oklch(0.455 0.105 63);
|
|
3419
|
+
--api-status-5xx: oklch(0.45 0.179 22);
|
|
2478
3420
|
|
|
2479
3421
|
--font-sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
2480
3422
|
--font-heading: var(--font-sans);
|
|
@@ -2511,6 +3453,25 @@ const THEME_DEFAULT = {
|
|
|
2511
3453
|
--sidebar-ring: oklch(0.439 0.01 286.375);
|
|
2512
3454
|
--code-background: oklch(0.18 0.014 253.833);
|
|
2513
3455
|
--code-foreground: oklch(0.926 0.013 253.833);
|
|
3456
|
+
|
|
3457
|
+
/* API semantic colors — lightened for dark mode (AA on 20%/10% tints). */
|
|
3458
|
+
--api-get: oklch(0.720 0.135 162);
|
|
3459
|
+
--api-post: oklch(0.755 0.174 259);
|
|
3460
|
+
--api-put: oklch(0.745 0.15 63);
|
|
3461
|
+
--api-patch: oklch(0.725 0.18 45);
|
|
3462
|
+
--api-delete: oklch(0.805 0.194 22);
|
|
3463
|
+
--api-head: oklch(0.740 0.182 286);
|
|
3464
|
+
--api-required: oklch(0.805 0.194 22);
|
|
3465
|
+
--api-deprecated: oklch(0.745 0.15 63);
|
|
3466
|
+
--api-type-string: oklch(0.690 0.135 162);
|
|
3467
|
+
--api-type-number: oklch(0.725 0.174 259);
|
|
3468
|
+
--api-type-boolean: oklch(0.720 0.15 63);
|
|
3469
|
+
--api-type-object: oklch(0.740 0.182 286);
|
|
3470
|
+
--api-type-array: oklch(0.725 0.18 45);
|
|
3471
|
+
--api-status-2xx: oklch(0.720 0.135 162);
|
|
3472
|
+
--api-status-3xx: oklch(0.755 0.174 259);
|
|
3473
|
+
--api-status-4xx: oklch(0.770 0.15 63);
|
|
3474
|
+
--api-status-5xx: oklch(0.805 0.194 22);
|
|
2514
3475
|
}`,
|
|
2515
3476
|
};
|
|
2516
3477
|
/**
|
|
@@ -2577,23 +3538,24 @@ const THEME_MINTLIFY = {
|
|
|
2577
3538
|
--api-panel-fg: oklch(0.926 0.013 253.833);
|
|
2578
3539
|
|
|
2579
3540
|
/* API semantic colors — HTTP methods, types, status codes */
|
|
2580
|
-
|
|
2581
|
-
--api-
|
|
2582
|
-
--api-
|
|
2583
|
-
--api-
|
|
2584
|
-
--api-
|
|
2585
|
-
--api-
|
|
2586
|
-
--api-
|
|
2587
|
-
--api-
|
|
2588
|
-
--api-
|
|
2589
|
-
--api-type-
|
|
2590
|
-
--api-type-
|
|
2591
|
-
--api-type-
|
|
2592
|
-
--api-type-
|
|
2593
|
-
--api-
|
|
2594
|
-
--api-status-
|
|
2595
|
-
--api-status-
|
|
2596
|
-
--api-status-
|
|
3541
|
+
/* Light mode: darkened for WCAG AA on the 20%/10% tints. See .dark. */
|
|
3542
|
+
--api-get: oklch(0.435 0.095 162);
|
|
3543
|
+
--api-post: oklch(0.45 0.174 259);
|
|
3544
|
+
--api-put: oklch(0.455 0.105 63);
|
|
3545
|
+
--api-patch: oklch(0.455 0.13 45);
|
|
3546
|
+
--api-delete: oklch(0.45 0.179 22);
|
|
3547
|
+
--api-head: oklch(0.47 0.222 286);
|
|
3548
|
+
--api-required: oklch(0.45 0.179 22);
|
|
3549
|
+
--api-deprecated: oklch(0.455 0.105 63);
|
|
3550
|
+
--api-type-string: oklch(0.435 0.095 162);
|
|
3551
|
+
--api-type-number: oklch(0.45 0.174 259);
|
|
3552
|
+
--api-type-boolean: oklch(0.455 0.105 63);
|
|
3553
|
+
--api-type-object: oklch(0.47 0.222 286);
|
|
3554
|
+
--api-type-array: oklch(0.455 0.13 45);
|
|
3555
|
+
--api-status-2xx: oklch(0.435 0.095 162);
|
|
3556
|
+
--api-status-3xx: oklch(0.45 0.174 259);
|
|
3557
|
+
--api-status-4xx: oklch(0.455 0.105 63);
|
|
3558
|
+
--api-status-5xx: oklch(0.45 0.179 22);
|
|
2597
3559
|
|
|
2598
3560
|
/* Fonts — system sans for body, JetBrains Mono for code */
|
|
2599
3561
|
--font-sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
@@ -2631,6 +3593,25 @@ const THEME_MINTLIFY = {
|
|
|
2631
3593
|
--sidebar-ring: oklch(0.45 0.01 260);
|
|
2632
3594
|
--code-background: oklch(0.2 0.01 260);
|
|
2633
3595
|
--code-foreground: oklch(0.9 0.01 260);
|
|
3596
|
+
|
|
3597
|
+
/* API semantic colors — lightened for dark mode (AA on 20%/10% tints). */
|
|
3598
|
+
--api-get: oklch(0.720 0.135 162);
|
|
3599
|
+
--api-post: oklch(0.755 0.174 259);
|
|
3600
|
+
--api-put: oklch(0.745 0.15 63);
|
|
3601
|
+
--api-patch: oklch(0.725 0.18 45);
|
|
3602
|
+
--api-delete: oklch(0.805 0.194 22);
|
|
3603
|
+
--api-head: oklch(0.740 0.182 286);
|
|
3604
|
+
--api-required: oklch(0.805 0.194 22);
|
|
3605
|
+
--api-deprecated: oklch(0.745 0.15 63);
|
|
3606
|
+
--api-type-string: oklch(0.690 0.135 162);
|
|
3607
|
+
--api-type-number: oklch(0.725 0.174 259);
|
|
3608
|
+
--api-type-boolean: oklch(0.720 0.15 63);
|
|
3609
|
+
--api-type-object: oklch(0.740 0.182 286);
|
|
3610
|
+
--api-type-array: oklch(0.725 0.18 45);
|
|
3611
|
+
--api-status-2xx: oklch(0.720 0.135 162);
|
|
3612
|
+
--api-status-3xx: oklch(0.755 0.174 259);
|
|
3613
|
+
--api-status-4xx: oklch(0.770 0.15 63);
|
|
3614
|
+
--api-status-5xx: oklch(0.805 0.194 22);
|
|
2634
3615
|
}`,
|
|
2635
3616
|
};
|
|
2636
3617
|
function writeThemeFile(path, themeName) {
|
|
@@ -2781,6 +3762,37 @@ function isInsideWorkspace(outputDir) {
|
|
|
2781
3762
|
}
|
|
2782
3763
|
return false;
|
|
2783
3764
|
}
|
|
3765
|
+
/**
|
|
3766
|
+
* Every @dogsbay/* package in this checkout, mapped to its `file:` path.
|
|
3767
|
+
*
|
|
3768
|
+
* Enumerated from disk rather than hard-coded: the set of packages changes, and
|
|
3769
|
+
* a stale list fails as a confusing install error naming a package nobody
|
|
3770
|
+
* mentioned.
|
|
3771
|
+
*/
|
|
3772
|
+
function monorepoOverrides() {
|
|
3773
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
3774
|
+
const overrides = {};
|
|
3775
|
+
if (!existsSync(root))
|
|
3776
|
+
return overrides;
|
|
3777
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
3778
|
+
if (!entry.isDirectory())
|
|
3779
|
+
continue;
|
|
3780
|
+
const manifest = join(root, entry.name, "package.json");
|
|
3781
|
+
if (!existsSync(manifest))
|
|
3782
|
+
continue;
|
|
3783
|
+
try {
|
|
3784
|
+
const { name } = JSON.parse(readFileSync(manifest, "utf-8"));
|
|
3785
|
+
if (name?.startsWith("@dogsbay/"))
|
|
3786
|
+
overrides[name] = `file:${join(root, entry.name)}`;
|
|
3787
|
+
}
|
|
3788
|
+
catch {
|
|
3789
|
+
// A malformed manifest in the checkout is not this function's problem;
|
|
3790
|
+
// skipping it degrades to the previous behaviour for that one package
|
|
3791
|
+
// rather than failing the whole export.
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3794
|
+
return overrides;
|
|
3795
|
+
}
|
|
2784
3796
|
function resolveMonorepoPkg(name) {
|
|
2785
3797
|
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
2786
3798
|
// From packages/format-astro/src/ → ../../{name}
|