import { log } from "../../utils/log"; import path from "path"; import grabDirNames from "../../utils/grab-dir-names"; import { existsSync, readdirSync, statSync, unlinkSync } from "fs"; const { ROOT_DIR } = grabDirNames(); // esbuild's content hashes are 8-char base64-ish (e.g. `CMSI7VSQ`). Entry // outputs are `/.js` / `/.css`. Shared chunks are // `chunk-*.js` and MUST be kept: any current entry may still reference an // older chunk (esbuild reuses unchanged chunks across rebuilds), and // verifying references would require parsing every entry's imports. const ENTRY_HASH_FILE = /^[A-Za-z0-9]{8}\.(js|css)$/; /** * Deletes orphaned entry outputs left behind by previous rebuilds. * * After a rebuild the page/SSR artifact maps only reference the newest * hashed entry files; every previously hashed entry for the same directory * is provably unreferenced (generated HTML always uses the current map) and * is removed. Chunks, non-hashed fallback module files and `map.json` are * left untouched. */ export default async function cleanupArtifacts({ paradigm }) { const { HYDRATION_DST_DIR, BUNX_CWD_MODULE_CACHE_DIR } = grabDirNames(); const is_hydration = paradigm === "hydration"; const root_dir = is_hydration ? HYDRATION_DST_DIR : BUNX_CWD_MODULE_CACHE_DIR; const map = is_hydration ? global.BUNEXT_BUNDLER_CTX_MAP : global.BUNEXT_SSR_BUNDLER_CTX_MAP; if (!map || !existsSync(root_dir)) { return; } try { const keep_by_dir = new Map(); for (const entry of Object.values(map)) { if (!entry?.path) continue; const full_path = path.join(ROOT_DIR, entry.path); const dir_path = path.dirname(full_path); let keep = keep_by_dir.get(dir_path); if (!keep) { keep = new Set(); keep_by_dir.set(dir_path, keep); } keep.add(path.basename(full_path)); if (entry.css_path) { keep.add(path.basename(entry.css_path)); } } sweepDir(root_dir, keep_by_dir); } catch (error) { log.error(`Cleanup Artifacts Error: ${error?.message || error}`); } } function sweepDir(dir_path, keep_by_dir) { let entries; try { entries = readdirSync(dir_path); } catch (error) { return; } for (const name of entries) { const full_path = path.join(dir_path, name); let is_dir = false; try { is_dir = statSync(full_path).isDirectory(); } catch (error) { continue; } if (is_dir) { sweepDir(full_path, keep_by_dir); continue; } if (!name.match(ENTRY_HASH_FILE)) { continue; } if (name.startsWith("chunk-")) { continue; } const keep = keep_by_dir.get(path.dirname(full_path)); if (keep?.has(name)) { continue; } try { unlinkSync(full_path); } catch (error) { } } }