Skip to content

fix(vite-plugin-ruby): reuse an already-emitted asset instead of fingerprinting it twice#612

Open
vitalyliber wants to merge 2 commits into
ElMassimo:mainfrom
vitalyliber:fix/dedupe-asset-fingerprinting
Open

fix(vite-plugin-ruby): reuse an already-emitted asset instead of fingerprinting it twice#612
vitalyliber wants to merge 2 commits into
ElMassimo:mainfrom
vitalyliber:fix/dedupe-asset-fingerprinting

Conversation

@vitalyliber

@vitalyliber vitalyliber commented Jul 7, 2026

Copy link
Copy Markdown

Summary

Closes #611.

fingerprintRemainingAssets unconditionally re-reads and re-emitFiles every asset matched by additionalEntrypoints (the default ~/{assets,fonts,icons,images}/**/* glob), even when the exact same file was already emitted once by Vite's own asset pipeline — e.g. because it's also imported from JS/CSS, or referenced from an HTML entrypoint.

Two independent emitFile calls for byte-identical content aren't guaranteed to resolve to the same content hash. In a production app that upgraded from Vite 7 to Vite 8 (which defaults to Rolldown instead of Rollup), this manifested as:

$ grep -o 'bluethumb-nav-logo[^"]*' public/vite/.vite/manifest.json | sort -u
bluethumb-nav-logo-hxTv2K1u.svg

$ grep -o 'bluethumb-nav-logo[^"]*' public/vite/.vite/manifest-assets.json | sort -u
bluethumb-nav-logo-hxTv2K1u2.svg

$ find public/vite -iname '*nav-logo*'
public/vite/assets/bluethumb-nav-logo-hxTv2K1u.svg

Same source file, two different hashes across manifest.json and manifest-assets.json, and only one of the two ever gets written to disk. Since ViteRuby::Manifest#load_manifest merges manifest files with Hash#merge and manifest-assets.json is loaded last, it wins — so vite_asset_path resolves to a file that doesn't exist, and Rails 404s. Full repro and versions are in #611.

Fix

Before fingerprinting a remainingAsset, scan the bundle for an OutputAsset whose originalFileNames (or the deprecated singular originalFileName) already contains the source path, and reuse its fileName instead of emitting again. This sidesteps the hash-mismatch entirely, regardless of which bundler is in use, and also avoids writing the same file to the output directory twice when it's already covered by Vite's own pipeline.

Second commit — found while verifying against the real app: the first version of this fix compared originalFileNames directly against the absolute source path, per Rollup's documented (and typed) contract that originalFileNames are absolute. I pointed the reporting app's package.json at this branch to confirm the fix before writing this up, and the mismatch still reproduced — the lookup never matched. Turned out Rolldown doesn't always honor that contract: for the specific asset from the report (matched by additionalEntrypoints and imported from a Vue SFC), it reported originalFileNames: ["assets/bluethumb-nav-logo.svg"] — relative to config.root, not absolute. The second commit resolves non-absolute entries against config.root before comparing, with a regression test for that exact shape.

Test plan

  • Added vite-plugin-ruby/tests/manifest.spec.ts, unit-testing assetsManifestPlugin directly against a fake bundle/PluginContext — including a case with originalFileNames relative to config.root. Confirmed each case is red without its corresponding piece of the fix, green with it.
  • pnpm -C vite-plugin-ruby build && pnpm -C vite-plugin-ruby test --run — 17/17 passing.
  • pnpm -C vite-plugin-ruby lint on the touched files — clean.
  • pnpm -r build && pnpm -C vite-plugin-rails test --run against examples/rails (Vite 8) — images/logo.png (matched by both additionalEntrypoints and the index.html <link> tag) resolves to the same hash in both manifests before and after the fix in this fixture, so no regression there. I wasn't able to get this specific example to reproduce the hash mismatch itself, which is why the unit tests above exercise the dedup logic directly.
  • Verified end-to-end against the real reporting app (the one from Vite 8: manifest.json and manifest-assets.json disagree on content hash for assets referenced from both JS and Ruby #611): pointed its package.json at this branch, ran RAILS_ENV=production bin/vite build from a clean public/vite twice in a row — manifest.json and manifest-assets.json now agree on a single hash for the previously-mismatched asset, the file exists on disk, and it serves HTTP 200 from a running Rails server.
  • tests/build.spec.ts's snapshot test failed for me on both main and this branch with an unrelated chunk-hash diff for vue.js (vue-CoJ_KGkH.js vs a freshly-built hash) — pre-existing flakiness in my local environment, not caused by this change.

…erprinting it twice

fingerprintRemainingAssets re-reads and re-emits any asset matched by
additionalEntrypoints (e.g. app/frontend/{assets,images,...}/**/*),
even if the same file was already emitted by Vite's own asset pipeline
because it's also imported from JS/CSS or referenced from an HTML
entrypoint.

Two independent emitFile calls for byte-identical content aren't
guaranteed to resolve to the same content hash — this can differ
between Rollup and Rolldown (Vite v8's default bundler). When they
diverge, manifest-assets.json (which wins when vite_ruby merges
manifest files, since it's loaded last) ends up pointing at a hashed
filename that vite-plugin-ruby computed but never actually wrote to
disk, while a different file with the correct content sits under the
hash from manifest.json. Rails then 404s on any vite_asset_path call
for that entry.

Scan the bundle for an OutputAsset whose originalFileNames (or the
deprecated singular originalFileName) already contains the absolute
path before emitting again, and reuse its fileName. This removes the
double-emit for any asset that's both entrypoint-globbed and
otherwise referenced, regardless of which bundler is in use, and also
avoids writing the same file to the output directory twice.

See ElMassimo#611 for the
originating report and a full production repro.
Verified the previous commit's fix against the real production app from
issue ElMassimo#611 (Vite 8 + a large Vue2/Rails app) by pointing its
package.json at this branch. The mismatch still reproduced — the
dedup lookup never matched.

Traced it to a difference in what Rolldown reports for
OutputAsset#originalFileNames: Rollup's types document it as absolute
paths, and that holds for most assets in this build, but for the one
matched by additionalEntrypoints AND imported from a Vue SFC
(bluethumb-nav-logo.svg), Rolldown reported it as a single path
relative to config.root ("assets/bluethumb-nav-logo.svg") instead of
absolute. findExistingAsset compared that directly against the
absolute filename from vite-plugin-ruby's own glob and never matched,
so it fell through to emitFile and re-triggered the original bug.

Resolve non-absolute originalFileNames against config.root before
comparing. Added a regression test for this exact shape, and
confirmed against the real app: manifest.json and manifest-assets.json
now agree on a single hash across repeated clean builds, and the
asset resolves (HTTP 200) when served.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Vite 8: manifest.json and manifest-assets.json disagree on content hash for assets referenced from both JS and Ruby

1 participant