From b3da0209e464dd1ba7ff3c435b3ed6a5e8d37afe Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 26 Feb 2026 16:52:00 -0800 Subject: [PATCH 1/2] Delete empty dirs in baseline-accept task --- Herebyfile.mjs | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/Herebyfile.mjs b/Herebyfile.mjs index 38de8d780e..d299222647 100644 --- a/Herebyfile.mjs +++ b/Herebyfile.mjs @@ -852,6 +852,26 @@ function baselineAcceptTask(localBaseline, refBaseline) { return path.join(refBaseline, relative); } + /** + * Walk up from dir, removing empty directories until hitting root. + * @param {string} dir + * @param {string} root + */ + async function removeEmptyDirsUp(dir, root) { + const resolvedRoot = path.resolve(root); + let current = path.resolve(dir); + while (current !== resolvedRoot) { + try { + await fs.promises.rmdir(current); + } + catch { + // Not empty or doesn't exist; stop walking up. + break; + } + current = path.dirname(current); + } + } + return async () => { const toCopy = await glob(`${localBaseline}/**`, { nodir: true, ignore: `${localBaseline}/**/*.delete` }); for (const p of toCopy) { @@ -865,6 +885,21 @@ function baselineAcceptTask(localBaseline, refBaseline) { await rimraf(out); await rimraf(p); // also delete the .delete file so that it no longer shows up in a diff tool. } + + // Remove empty directories left behind by deletions (walk up from each deleted path). + const cleaned = new Set(); + for (const p of toDelete) { + const refDir = path.dirname(localPathToRefPath(p).replace(/\.delete$/, "")); + const localDir = path.dirname(p); + if (!cleaned.has(refDir)) { + cleaned.add(refDir); + await removeEmptyDirsUp(refDir, refBaseline); + } + if (!cleaned.has(localDir)) { + cleaned.add(localDir); + await removeEmptyDirsUp(localDir, localBaseline); + } + } }; } From d38458a9c8aee4806ea7e580bf9d4e5b49ff75a6 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 4 Mar 2026 09:55:22 -0800 Subject: [PATCH 2/2] Use a top-down approach --- Herebyfile.mjs | 78 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 55 insertions(+), 23 deletions(-) diff --git a/Herebyfile.mjs b/Herebyfile.mjs index d299222647..db64305c1b 100644 --- a/Herebyfile.mjs +++ b/Herebyfile.mjs @@ -853,23 +853,62 @@ function baselineAcceptTask(localBaseline, refBaseline) { } /** - * Walk up from dir, removing empty directories until hitting root. - * @param {string} dir + * Recursively find top-most empty subtrees under root, excluding root itself. + * Returns the highest ancestors whose entire subtrees contain only empty + * directories, so a single rimraf can remove each whole subtree at once. * @param {string} root + * @returns {string[]} */ - async function removeEmptyDirsUp(dir, root) { - const resolvedRoot = path.resolve(root); - let current = path.resolve(dir); - while (current !== resolvedRoot) { + function findEmptySubtrees(root) { + /** @type {string[]} */ + const result = []; + + /** + * @param {string} dir + * @returns {boolean} true if dir is entirely empty + */ + function visit(dir) { + /** @type {import("fs").Dirent[]} */ + let entries; try { - await fs.promises.rmdir(current); + entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { - // Not empty or doesn't exist; stop walking up. - break; + return false; + } + + let empty = true; + /** @type {string[]} */ + const childEmpty = []; + for (const entry of entries) { + if (entry.isDirectory()) { + const childPath = path.join(dir, entry.name); + if (visit(childPath)) { + childEmpty.push(childPath); + } + else { + empty = false; + } + } + else { + empty = false; + } + } + + if (!empty) { + // This dir is not fully empty, so collect the empty children + // we found (they are top-most empty subtrees). + result.push(...childEmpty); } - current = path.dirname(current); + // If empty, don't collect children — the caller will either + // collect this dir or propagate emptiness further up. + return empty; } + + // If root itself is entirely empty, there's nothing to collect + // (we exclude root). Otherwise, visit already pushed the right subtrees. + visit(root); + return result; } return async () => { @@ -886,19 +925,12 @@ function baselineAcceptTask(localBaseline, refBaseline) { await rimraf(p); // also delete the .delete file so that it no longer shows up in a diff tool. } - // Remove empty directories left behind by deletions (walk up from each deleted path). - const cleaned = new Set(); - for (const p of toDelete) { - const refDir = path.dirname(localPathToRefPath(p).replace(/\.delete$/, "")); - const localDir = path.dirname(p); - if (!cleaned.has(refDir)) { - cleaned.add(refDir); - await removeEmptyDirsUp(refDir, refBaseline); - } - if (!cleaned.has(localDir)) { - cleaned.add(localDir); - await removeEmptyDirsUp(localDir, localBaseline); - } + // Remove empty directory trees left behind by deletions. + for (const dir of findEmptySubtrees(refBaseline)) { + await rimraf(dir); + } + for (const dir of findEmptySubtrees(localBaseline)) { + await rimraf(dir); } }; }