checkExamples
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; }