checkExamples

tcmts.dev.checkExamples · #gqcsto4j · term · runs on the server

uses node:fs

Check examples (all, or those named) as their files are now, in seconds rather than a fresh build: each has a codebase of its own kept for checking (dist/examples/<name>-check, made once by its setup script), and its files are saved into it in its setup's order, so what doesn't typecheck says so. --tests runs its tests too. A large one (Effect, minutes to import) is checked only when named. Run with pnpm examples:check [name…] [--tests].

import { EXAMPLES, repository, setupOrder, sh, shippedLibrariesDir } from "#codebase/tcmts/dev";
import { createHash } from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";

export async function checkExamples(...args: string[]): Promise<void> {
  const tcm = process.env.TCM_BIN;
  if (!tcm)
    throw new Error("run this with `tcm run`, which gives it the codebase's own tcm (TCM_BIN)");
  const tests = args.includes("--tests");
  const names = args.filter((a) => !a.startsWith("--"));
  const chosen = EXAMPLES.filter((e) =>
    // (the apps, by default; geometry's setup goes through releases, step by step)
    names.length ? names.includes(e.name) : e.app || e.site,
  );
  if (!chosen.length)
    throw new Error(
      `no example named ${names.join(", ")}: ${EXAMPLES.map((e) => e.name).join(", ")}`,
    );
  // the libraries tcm ships (`tcm lib add tcm react`), which a tcm run from its codebase doesn't have beside it
  let shipped: string | undefined;
  const libraries = () => (shipped ??= shippedLibrariesDir());
  let failed = 0;
  for (const ex of chosen) {
    const started = Date.now();
    const files = fs.mkdtempSync(path.join(os.tmpdir(), `tcm check ${ex.name} `));
    for (const [rel, text] of Object.entries(ex.files)) {
      const file = path.join(files, rel);
      fs.mkdirSync(path.dirname(file), { recursive: true });
      fs.writeFileSync(file, text, { mode: rel.endsWith(".sh") ? 0o755 : 0o644 });
    }
    const env = {
      TCM_BIN: tcm,
      TCM_CODEBASE: "",
      TCM_BRANCH: "",
      TCM_SHIPPED_LIBRARIES: libraries(),
    };
    const dir = path.join(repository(), "dist", "examples", `${ex.name}-check`);
    const said: string[] = [];
    let ok = true;
    // (made again when its setup changed: what it sets up may be what changed)
    const seenFile = path.join(dir, ".checked.json");
    const setup = createHash("sha256")
      .update(ex.files["setup.sh"] ?? "")
      .digest("hex");
    if (
      fs.existsSync(seenFile) &&
      (JSON.parse(fs.readFileSync(seenFile, "utf8")) as Record<string, string>)["setup.sh"] !==
        setup
    )
      fs.rmSync(dir, { recursive: true, force: true });
    if (!fs.existsSync(path.join(dir, ".tcm", "codebase.sqlite"))) {
      // made once, as anyone would: by its setup script (which saves every file, and runs its tests)
      fs.rmSync(dir, { recursive: true, force: true });
      const r = sh(path.join(files, "setup.sh"), [dir], files, {
        ...env,
        ...(tests ? {} : { NO_TESTS: "1" }),
      });
      ok = r.ok;
      said.push(
        r.ok ? "made its codebase for checking" : r.out.trim().split("\n").slice(-15).join("\n"),
      );
      // (all of it, as it is now)
      if (r.ok)
        fs.writeFileSync(
          path.join(dir, ".checked.json"),
          JSON.stringify(
            Object.fromEntries(
              Object.entries(ex.files).map(([f, t]) => [
                f,
                createHash("sha256").update(t).digest("hex"),
              ]),
            ),
            null,
            2,
          ),
        );
    } else {
      // only what changed since it was last checked: what uses it is checked with it (dependents are re-derived)
      const seen = (
        fs.existsSync(seenFile) ? JSON.parse(fs.readFileSync(seenFile, "utf8")) : {}
      ) as Record<string, string>;
      const digest = (rel: string) =>
        createHash("sha256")
          .update(ex.files[rel] ?? "")
          .digest("hex");
      const changed = setupOrder(ex.files["setup.sh"] as string).filter(
        ({ file }) => seen[file] !== digest(file),
      );
      if (!changed.length) said.push("nothing changed since it was checked");
      for (const { file, into } of changed) {
        fs.copyFileSync(path.join(files, file), path.join(dir, file));
        const r = sh(
          process.execPath,
          [tcm, "update", file, "--clear", ...(into ? ["--into", into] : [])],
          dir,
          env,
        );
        fs.rmSync(path.join(dir, file), { force: true });
        if (!r.ok) {
          ok = false;
          said.push(`${file}:\n${r.out.trim().split("\n").slice(-20).join("\n")}`);
          break;
        }
        seen[file] = digest(file);
      }
      fs.writeFileSync(seenFile, JSON.stringify(seen, null, 2));
      if (ok && tests) {
        const ns = /\btcm test ([\w.]+)/.exec(ex.files["setup.sh"] as string)?.[1] ?? ex.name;
        const r = sh(process.execPath, [tcm, "test", ns], dir, env);
        ok = r.ok;
        said.push(
          r.out
            .trim()
            .split("\n")
            .filter((l) => /passed|failed|✗/.test(l))
            .join("\n"),
        );
      }
    }
    fs.rmSync(files, { recursive: true, force: true });
    if (!ok) failed++;
    console.log(
      `${ex.name}: ${ok ? "ok" : "FAILED"} (${((Date.now() - started) / 1000).toFixed(1)}s)${said.length ? `\n  ${said.join("\n").replaceAll("\n", "\n  ")}` : ""}`,
    );
  }
  process.exitCode = failed ? 1 : 0;
}

uses