// a page loads the client chunks of the build that rendered it, under content-hashed names,
// and a deploy replaces the build's own copy before the pages render again. so every build's
// chunks are kept under www/_app/immutable, listed per build in state/builds/<build>.json,
// until no page rendered by that build is served
import { constants } from "node:fs";
import { copyFile, mkdir, readdir, rename, rm, stat } from "node:fs/promises";
import path from "node:path";
import { atomicJson, readJson } from "./common.mjs";
const IMMUTABLE = "_app/immutable";
const filesUnder = async (dir, rel = "") => {
const files = [];
for (const entry of await readdir(path.join(dir, rel), { withFileTypes: true })) {
const name = path.join(rel, entry.name);
if (entry.isDirectory()) files.push(...(await filesUnder(dir, name)));
else files.push(name);
}
return files;
};
const buildsDir = (out) => path.join(out, "state", "builds");
// copies the files of a build its pages load that aren't kept yet, before any page renders
export const keepClient = async (client, out, build) => {
const from = path.join(client, IMMUTABLE);
const to = path.join(out, "www", IMMUTABLE);
const files = (await filesUnder(from)).sort();
let copied = 0;
for (const file of files) {
const target = path.join(to, file);
if (await stat(target).then(() => true, () => false)) continue;
await mkdir(path.dirname(target), { recursive: true });
await copyFile(path.join(from, file), target + ".tmp", constants.COPYFILE_FICLONE);
await rename(target + ".tmp", target);
copied++;
}
await atomicJson(path.join(buildsDir(out), build + ".json"), files);
return copied;
};
// removes the files of every build not in `served` that no served build shares
export const collectClients = async (out, served) => {
const builds = (await readdir(buildsDir(out)).catch(() => [])).filter((name) => name.endsWith(".json")).map((name) => name.slice(0, -5));
const kept = new Set(builds.filter((build) => served.has(build)).flatMap((build) => readJson(path.join(buildsDir(out), build + ".json")) ?? []));
let removed = 0;
for (const build of builds.filter((build) => !served.has(build))) {
for (const file of readJson(path.join(buildsDir(out), build + ".json")) ?? []) {
if (kept.has(file)) continue;
await rm(path.join(out, "www", IMMUTABLE, file), { force: true });
removed++;
}
await rm(path.join(buildsDir(out), build + ".json"), { force: true });
}
return removed;
};