pruneBuilds
uses node:path
Delete old builds: per app, keep the build its name points to now plus the newest keep others. Tabs still
on a deleted build reload themselves onto the current one at their next server call. What's leased
(leaseBuilds) stays: a build being made, and its assets, and the build a tcm start serves. What goes is
moved aside first, and what was taken hold of meanwhile is put back.
import type { Codebase } from "#codebase/tcmts/cli/codebase"; import { dim } from "#codebase/tcmts/cli/style"; import { appsDir, assetsDir, buildWorkDir, clearLeftovers, currentBuild, heldBuilds, listBuilds, ownId, } from "#codebase/tcmts/cli/web"; import type { BuildManifest } from "#codebase/tcmts/cli/web"; import { shortHash } from "#codebase/tcmts/core/hash"; import fs from "node:fs"; import path from "node:path"; export function pruneBuilds(cb: Codebase, keep: number, quiet = false): BuildManifest[] { const work = buildWorkDir(cb); clearLeftovers(work); // (read first: what's taken hold of after this is looked at again before anything goes) const held = heldBuilds(work); const current = new Set<string>(held.builds); const byApp = new Map<string, BuildManifest[]>(); for (const b of listBuilds(cb)) { byApp.set(b.app, [...(byApp.get(b.app) ?? []), b]); try { current.add(currentBuild(cb, b.app)); } catch { // the app's name is gone: nothing of it is current } } const trash = path.join(work, `pruned.${ownId()}`); const moveAside = (from: string, to: string) => { fs.mkdirSync(path.dirname(to), { recursive: true }); try { fs.renameSync(from, to); return true; } catch { return false; } }; let removed: BuildManifest[] = []; for (const builds of byApp.values()) { const others = builds.filter((b) => !current.has(b.hash)); for (const b of others.slice(keep)) if (moveAside(path.join(appsDir(cb), b.hash), path.join(trash, "apps", b.hash))) removed.push(b); } // the shared assets only the removed builds had if (removed.length) { const stem = (f: string) => f.replace(/\.(br|gz)$/, ""); const used = new Set([...listBuilds(cb).flatMap((b) => b.assets ?? []), ...held.assets]); const dir = assetsDir(cb); for (const f of fs.existsSync(dir) ? fs.readdirSync(dir) : []) if (!used.has(stem(f))) moveAside(path.join(dir, f), path.join(trash, "assets", f)); // what was taken hold of meanwhile goes back: builds, then the assets of what's kept const now = heldBuilds(work); removed = removed.filter( (b) => !now.builds.has(b.hash) || !moveAside(path.join(trash, "apps", b.hash), path.join(appsDir(cb), b.hash)), ); const kept = new Set([...listBuilds(cb).flatMap((b) => b.assets ?? []), ...now.assets]); const aside = path.join(trash, "assets"); for (const f of fs.existsSync(aside) ? fs.readdirSync(aside) : []) if (kept.has(stem(f))) moveAside(path.join(aside, f), path.join(dir, f)); fs.rmSync(trash, { recursive: true, force: true }); // and the differences between chunks that aren't both kept const deltas = path.join(appsDir(cb), "deltas"); for (const f of fs.existsSync(deltas) ? fs.readdirSync(deltas) : []) { const [from, to] = f.replace(/\.dcb(\.none)?$/, "").split("--"); if (!kept.has(from!) || !kept.has(to!)) fs.rmSync(path.join(deltas, f), { force: true }); } } if (!quiet) console.log( removed.length ? `removed ${removed.length} build(s): ${removed.map((b) => `${b.app} ${shortHash(b.hash)}`).join(", ")}` : dim("nothing to prune"), ); return removed; }
uses
- tcmts.cli.codebase.Codebase
- tcmts.cli.style.dim
- tcmts.cli.web.BuildManifest
- tcmts.cli.web.appsDir
- tcmts.cli.web.assetsDir
- tcmts.cli.web.buildWorkDir
- tcmts.cli.web.clearLeftovers
- tcmts.cli.web.currentBuild
- tcmts.cli.web.heldBuilds
- tcmts.cli.web.listBuilds
- tcmts.cli.web.ownId
- tcmts.core.hash.shortHash