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 to build/ (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 is nav.36da3320, hash is 36da3320, new name: nav.36da3320.36da3320.js
nav.js — stem is nav, hash is 36da3320, 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 logic
This 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
Bug:
fingerprint_assets()stacks hashes on incremental buildsFile
nitro/core/bundler.py, methodfingerprint_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 hashesOn a clean first build, this works correctly:
On the second build (incremental), step 1 copies fresh
nav.jsintobuild/, but the previously fingerprintednav.36da3320.jsis still there from the last build. Thenfingerprint_assets()runsself.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.jsResult: both
nav.36da3320.jsandnav.36da3320.36da3320.jsexist. Each subsequent build adds another hash layer. After 6 builds you get filenames like: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
.cssand.jsfiles in the build directory, including previously fingerprinted ones:Line 253-255 — the new filename is built by appending the hash to
asset_path.stem, which for an already-fingerprinted file likenav.36da3320.jsproducesnav.36da3320.{hash}.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:
This skips
nav.36da3320.js(stemnav.36da3320matches\.[0-9a-f]{8}$) but processesnav.js(stemnavdoesn'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:
Both approaches solve it. The skip approach is simpler; the delete approach keeps the build directory cleaner.
Reproduction