Repository navigation
Fingerprint stacking: incremental builds produce duplicate hashed filenames #11
Description
Bug: fingerprint_assets() stacks hashes on incremental builds
File
nitro/core/bundler.py, method fingerprint_assets() (line 236)
What happens
On each nitro build, the build sequence is:
generator.generate()copies fresh source files tobuild/(nav.js,matrix.js,main.css)bundler.fingerprint_assets()renames them with content hashes
On a clean first build, this works correctly:
nav.js → nav.36da3320.js (stem="nav", hash=36da3320)
matrix.js → matrix.3cc73026.js
On the second build (incremental), step 1 copies fresh nav.js into build/, but the previously fingerprinted nav.36da3320.js is still there from the last build. Then fingerprint_assets() runs self.build_dir.rglob("*.js") which finds both files:
nav.36da3320.js— stem isnav.36da3320, hash is36da3320, new name:nav.36da3320.36da3320.jsnav.js— stem isnav, hash is36da3320, new name:nav.36da3320.js
Result: both nav.36da3320.js and nav.36da3320.36da3320.js exist. Each subsequent build adds another hash layer. After 6 builds you get filenames like:
nav.613004d2.613004d2.613004d2.613004d2.613004d2.613004d2.36da3320.js
The HTML references stay correct (they point to the single-hash version), so the site works — but the build directory accumulates garbage files that grow with every build.
Root cause
Line 239-240 — the glob collects all .css and .js files in the build directory, including previously fingerprinted ones:
for pattern in ["*.css", "*.js"]:
asset_files.extend(self.build_dir.rglob(pattern))Line 253-255 — the new filename is built by appending the hash to asset_path.stem, which for an already-fingerprinted file like nav.36da3320.js produces nav.36da3320.{hash}.js:
stem = asset_path.stem # "nav.36da3320"
suffix = asset_path.suffix # ".js"
new_name = f"{stem}.{content_hash}{suffix}" # "nav.36da3320.36da3320.js"Suggested fix
Before fingerprinting a file, skip it if the stem already ends with a fingerprint hash. Add this check inside the loop at line 247, before processing:
import re
FINGERPRINT_RE = re.compile(r'\.[0-9a-f]{8}$')
for asset_path in asset_files:
# Skip files that were already fingerprinted in a previous build
if FINGERPRINT_RE.search(asset_path.stem):
continue
# ... rest of the fingerprinting logicThis skips nav.36da3320.js (stem nav.36da3320 matches \.[0-9a-f]{8}$) but processes nav.js (stem nav doesn't match). The previously fingerprinted file becomes an orphan, which gets overwritten by the fresh rename on line 262 since it produces the same filename.
Alternatively, you could delete stale fingerprinted files before the loop:
for asset_path in asset_files:
if FINGERPRINT_RE.search(asset_path.stem):
asset_path.unlink()
# Re-collect after cleanup
asset_files = []
for pattern in ["*.css", "*.js"]:
asset_files.extend(self.build_dir.rglob(pattern))Both approaches solve it. The skip approach is simpler; the delete approach keeps the build directory cleaner.
Reproduction
rm -rf build/ .nitro/
nitro build # clean build — correct output
nitro build # second build — stacked hashes appear
ls build/*.js # nav.36da3320.js AND nav.36da3320.36da3320.js
Activity
Fix fingerprint stacking on incremental builds (#11)
Merge pull request #12 from nitrosh/11-fingerprint-stacking-increment…