buildSite
uses node:path
Build the docs site into out: the site's pages (#zdsdm6da: tcm's docs and the examples page, as the
codebase has them), text-file-looking, ordered by the index's tables, numbered like sections of a text file, with a
numbered menu down the side. They're the docs app (Docs) built as a static site (tcm build --static), every
page rendered as it's built, so each is there before any code runs; then the code browser beside them. Every
relative link and anchor must resolve, and every page be reached from the index: what isn't is a problem, so the
docs can't quietly rot. large builds the large examples' code browser too (Effect's: minutes, the first time
after its files change): what's deployed has it, pnpm verify's check of the site doesn't. env is given to the
docs app's build and the examples': a slot's (withSlot), when this runs in one, so they don't wait for another.
base is the path it's served under (tcm's server has it at /docs), for the docs app's own navigation and the
examples that are static sites; its links are relative.
import { buildCodeBrowser, codebaseDir, repository, sh } from "#codebase/tcmts/dev"; import { finishSite, highlighter, pageList, renderPage, SOURCES, STYLE } from "#codebase/tcmts/site"; import { Store } from "#codebase/tcmts/store"; import { execFileSync } from "node:child_process"; import fs from "node:fs"; import path_1 from "node:path"; export async function buildSite( out: string, { large = false, env = {}, base, }: { large?: boolean; env?: Record<string, string>; base?: string } = {}, ): Promise<{ pages: number; problems: string[] }> { const repo = repository(); // what the codebase has, by name: a link to one of its files (an example's code) is shown as its path const store = new Store(path_1.join(codebaseDir(), ".tcm", "codebase.sqlite")); const names = new Set( store .names(undefined, true) .map((r) => r.path) .filter((p) => p.startsWith("tcmts.")), ); store.close(); const inCodebase = (src: string) => { const name = `tcmts.${src.replace(/\.[^/.]+$/, "").replaceAll("/", ".")}`; return names.has(name) || [...names].some((n) => n.startsWith(`${name}.`)); }; const pages = pageList(SOURCES["docs/README.md"]!); const hl = await highlighter(); const problems: string[] = []; const anchors = new Map<string, Set<string>>(); const links: { from: string; file: string; anchor?: string; text: string }[] = []; const version = (() => { try { return execFileSync("git", ["rev-parse", "--short=8", "HEAD"], { cwd: repo, encoding: "utf8", }).trim(); } catch { return ""; } })(); fs.rmSync(out, { recursive: true, force: true }); fs.mkdirSync(out, { recursive: true }); fs.writeFileSync(path_1.join(out, "style.css"), STYLE); for (const page of pages) { const md = SOURCES[page.src]; if (md === undefined) { problems.push( `${page.src}: the index lists it, and it isn't one of the site's pages (tcmts.site.SOURCES)`, ); continue; } const r = renderPage( page, pages, md, hl, (src) => inCodebase(src) || fs.existsSync(path_1.join(repo, src)), false, ); anchors.set(page.file, new Set(r.ids)); problems.push(...r.problems.map((p) => `${page.src}: ${p}`)); links.push(...r.links.map((l) => ({ from: page.src, ...l }))); } // the pages: the docs app, built as a static site, for the commit it's from const tcm = process.env.TCM_BIN; if (!tcm) problems.push( "the site is built with `tcm run`, which gives it the codebase's own tcm (TCM_BIN)", ); else { const built = sh( process.execPath, [tcm, "build", "tcmts.site.Docs", "--static", out, ...(base ? ["--base", base] : [])], codebaseDir(), { ...env, TCM_SITE_VERSION: version, }, ); if (!built.ok) problems.push( `tcm build tcmts.site.Docs --static:\n${built.out.trim().split("\n").slice(-12).join("\n")}`, ); for (const w of built.out.split("\n").filter((l) => /^\s*warning\b/.test(l))) problems.push(`docs app: ${w.trim()}`); for (const page of pages) if (built.ok && !fs.existsSync(path_1.join(out, page.file))) problems.push(`${page.src}: no page links to it, so the site doesn't have it`); } for (const l of links) { if (l.anchor && !anchors.get(l.file)?.has(l.anchor)) problems.push(`${l.from}: link to ${l.text}: no heading #${l.anchor} there`); } // and the code, to browse: tcm's, and the examples' problems.push(...(await buildCodeBrowser(out, hl, large, env, base))); // a page's links to definitions ({@link}) go to their pages in the code browser: each must be there for (const page of pages) { const file = path_1.join(out, page.file); if (!fs.existsSync(file)) continue; for (const [, to] of fs.readFileSync(file, "utf8").matchAll(/href="(code\/[^"#]+)"/g)) if (!fs.existsSync(path_1.join(out, to!))) problems.push(`${page.src}: link to ${to}: no such page in the code browser`); } // and quick to go through: the stylesheet kept for good, each page prerendered when its link is pointed at finishSite(out); return { pages: pages.length, problems }; }
uses
- tcmts.dev.buildCodeBrowser
- tcmts.dev.codebaseDir
- tcmts.dev.repository
- tcmts.dev.sh
- tcmts.site.SOURCES
- tcmts.site.STYLE
- tcmts.site.finishSite
- tcmts.site.highlighter
- tcmts.site.pageList
- tcmts.site.renderPage
- tcmts.store.Store