How to empty folder pruner in developer workflows
- Step 1Grab the artifact — Download the build ZIP from CI or the PR's artifacts. No checkout or rebuild needed — the tool works on the ZIP directly.
- Step 2Prune in a tab — Open /archive-tools/empty-folder-pruner, drop the ZIP, click Process. The tool computes referenced dirs and rebuilds without the empties.
- Step 3Read the metrics — Note 'Empty dirs removed' and the entry count — concrete numbers for a review comment, not a vague 'cleaned it up'.
- Step 4Diff to verify — Run Archive Diff on original vs pruned to confirm only directory entries were removed (plus the expected timestamp reset).
- Step 5Normalise for reproducibility — Send the pruned ZIP through Timestamp Normaliser with a fixed date so the artifact is byte-stable across machines.
- Step 6Attest with a checksum — Generate a SHA-256 manifest via Checksum Generator and commit it next to the artifact for CI to verify later.
Local equivalents for what the tool does
The pruner wraps fflate; these are the script-side equivalents a developer might use instead. There is no JAD API today, so automation means scripting one of these.
| Goal | JAD tool | Node/CLI equivalent |
|---|---|---|
| Remove empty dir records (keep files' parents) | Empty Folder Pruner | fflate unzipSync + filter dirs by referenced set + zipSync |
| Diff two ZIPs' entries | Archive Diff | diff <(unzip -l a.zip) <(unzip -l b.zip) |
| Fix timestamps for reproducibility | Timestamp Normaliser | strip-nondeterminism / SOURCE_DATE_EPOCH |
| Produce a SHA-256 manifest | Checksum Generator | sha256sum * / Web Crypto subtle.digest |
| Rebuild a ZIP from a folder | Folder to ZIP | fflate zipSync / zip -r |
Tier limits that apply to the Empty Folder Pruner
Limits are enforced per input file before pruning begins (lib/tier-limits.ts, archive family). The pruner reads ONE ZIP at a time on every tier — it is not a batch tool, so the batch-files column only matters for sibling tools that accept folders. entryLimit counts every entry in the ZIP central directory, including the empty directory records you are trying to remove.
| Tier | Max ZIP size | Max entries per ZIP | Files per run |
|---|---|---|---|
| Free | 50 MB | 500 entries | 1 |
| Pro | 500 MB | 50,000 entries | 20 (other tools) |
| Pro + Media | 2 GB | 500,000 entries | 100 (other tools) |
| Developer | 2 GB | 500,000 entries | unlimited (other tools) |
| Enterprise | unlimited | unlimited | unlimited |
Cookbook
Developer recipes for the moments empty directory records actually bite — code review, reproducible builds, and verifying a clean change. Listings are illustrative.
Review a PR artifact without checking out
A teammate's PR ships build.zip with empty coverage/ and .nyc_output/ records left by the test step. Prune it in a tab and paste the count into the review.
Before (PR build.zip): dist/index.js dist/index.js.map coverage/ .nyc_output/ After (build-pruned.zip): dist/index.js dist/index.js.map dist/ Review comment: 'Packaging leaves 2 empty dirs (coverage/, .nyc_output/) - Empty Folder Pruner removes them. Suggest adding to the package step.'
Prove only empty dirs changed
Before merging a 'clean the artifact' change, diff original vs pruned so reviewers see the change is surgical: directory entries removed, files untouched.
Archive Diff (release.zip vs release-pruned.zip): - removed dir : tmp/ - removed dir : logs/ ~ changed : (timestamps reset on all entries, expected) files added/removed/modified: 0 Conclusion: only empty directories were pruned.
Match the logic in a Node script
If you want this in CI, replicate the tool with fflate: keep all files, keep only directories that are parents of a file.
import { unzipSync, zipSync } from 'fflate';
const z = unzipSync(buf);
const ref = new Set();
for (const n of Object.keys(z)) if (!n.endsWith('/')) {
const p = n.split('/'); let c = '';
for (let i=0;i<p.length-1;i++){ c+=p[i]+'/'; ref.add(c); }
}
const kept = {};
for (const [n,d] of Object.entries(z)) if (!n.endsWith('/') || ref.has(n)) kept[n]=d;
const out = zipSync(kept, { level: 6 });Reproducible artifact in three steps
Chain the browser tools to get a byte-identical ZIP with a committed manifest — equivalent to a deterministic CI packaging step, done by hand.
1. Empty Folder Pruner release.zip -> release-pruned.zip 2. Timestamp Normaliser fix to 1980-01-01 (ZIP minimum date) 3. Checksum Generator -> release.sha256 Commit release.sha256; CI re-runs the chain and asserts the hash matches.
Spot a packaging bug from the count
If the 'Empty dirs removed' count keeps growing build over build, your packaging step is recording more phantom folders over time — a signal to fix the script, not just the artifact.
Build 41: Empty dirs removed = 2 Build 58: Empty dirs removed = 7 Build 73: Empty dirs removed = 11 Trend = packaging records more empty dirs each release. Fix: clean temp dirs BEFORE the archive step.
Edge cases and what actually happens
No API for CI automation
Not availableThe tool is a browser page only (browser-only by design, no API). For an automated step, script the fflate snippet in the cookbook or zip -d — the tool is for in-flow, manual cleanup and review.
Artifact isn't a ZIP
Rejectedfflate parses ZIP only; a tar.gz or 7z artifact is rejected with 'Not a valid ZIP archive.' Convert with Archive Format Converter or use a CLI.
Diff shows timestamp changes too
ExpectedThe rebuild resets entry dates, so Archive Diff will flag timestamp changes alongside the removed directories. Normalise timestamps if you need a clean diff.
Over the entry limit on a monorepo artifact
Tier limit exceededFree caps at 500 entries, Pro at 50,000. Large monorepo build ZIPs can exceed this; empty directory records count too. Use a higher tier or script fflate locally.
Reproducible build wants stable dates
Action neededPruning alone won't give you a stable checksum because dates reset. Add Timestamp Normaliser before checksumming.
You needed to keep a placeholder dir
By designAn empty .gitkeep-style folder with no file inside will be removed (the .gitkeep FILE would keep it, but a bare directory record won't). Add a real file if the folder must persist.
Encrypted CI artifact
Not supportedfflate can't read encrypted entries, so password-protected artifacts fail. Prune the plaintext build before encryption.
Want batch pruning of many PR artifacts
Not supportedThe pruner handles one ZIP per run. For many at once, loop the fflate snippet in Node; this tool isn't a batch processor.
Output compression differs from input
ExpectedThe rebuild uses fflate level 6 regardless of the input's original compression, so byte sizes can shift slightly even when no files change. The file contents are identical.
Frequently asked questions
Is there a CLI or API equivalent?
No JAD API yet — the tool runs in the browser only. The exact local equivalent is fflate in Node (the cookbook has a drop-in snippet) or zip -d for a blunter delete.
How do I make the pruned output reproducible?
Pruning resets timestamps, so follow it with Timestamp Normaliser at a fixed date, then Checksum Generator for a manifest CI can verify.
Can I script bulk pruning?
Not through this tool — it's one ZIP per run. Loop the fflate snippet from the cookbook in Node, or use a shell loop with zip -d, for bulk work.
Will the output still work with my consumers?
Yes. It's a standard ZIP (fflate level 6) with no JAD wrapper — drop it into any extractor or CI step that reads ZIP.
How do I prove the change was surgical?
Run Archive Diff on original vs pruned. Expect only removed directory entries plus the (intended) timestamp reset; zero file changes.
Why does the diff show every file as changed?
Because rebuilding the ZIP rewrites entry timestamps. Run Timestamp Normaliser on both files first if you want a clean, content-only diff.
Does it match the logic I'd write myself?
Yes — it keeps every file and keeps only directories that are a parent of some file. The cookbook's fflate snippet is the literal equivalent.
Can I clean a PR artifact without checking out the branch?
That's the point. Download the artifact, prune it in a tab, and comment with the 'Empty dirs removed' count — no checkout, no rebuild.
What if the count keeps growing across builds?
That's a packaging smell: your build records more empty dirs over time. Fix the build to clean temp folders before archiving rather than pruning after.
Does it remove .gitkeep or placeholder files?
No — those are files, and a folder containing a .gitkeep is therefore referenced and kept. The pruner only removes bare directory records with no file under them.
What are the size and entry limits?
Free 50 MB / 500 entries, Pro 500 MB / 50,000, Pro+Media and Developer 2 GB / 500,000. Empty directory records count toward the entry limit.
Does it touch my files' contents?
Never. File entries are preserved byte-for-byte; only directory records are removed and the container is rebuilt (which is why timestamps reset).
Privacy first
Every JAD Archive tool runs entirely in your browser using fflate, @zip.js/zip.js, and the libarchive WASM bridge. Your archives never leave your device — verified by zero outbound network requests during processing.