buildSite

tcmts.dev.buildSite · #ve3skzhc · term · runs on the server

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

used by