Description 📖
After upgrading vite from 7.3.x to 8.1.3 (keeping vite-plugin-ruby@5.2.2), a static asset that is both imported from application JS/Vue code and referenced directly from Ruby via vite_asset_path ends up with two different content hashes across public/vite/.vite/manifest.json and public/vite/.vite/manifest-assets.json.
Since ViteRuby::Manifest#load_manifest merges all manifest files with .inject({}, &:merge) (later files win), the entry from manifest-assets.json overrides the one from manifest.json. Only the file referenced by manifest.json actually exists on disk (with the correct/deduped hash), so vite_asset_path resolves to a hashed filename that was never written to public/vite/assets, and the app 404s on that asset.
On Vite 7.3.x with the exact same source files and the exact same vite-plugin-ruby version, both manifests agree on the same hash and everything works.
Reproduction
assets/bluethumb-nav-logo.svg lives under app/javascript/assets/.
- It is referenced directly from Ruby:
vite_asset_path("assets/bluethumb-nav-logo.svg") (used inside a Rails helper/component to render <img>).
- It is also imported as a URL from a Vue SFC template:
<img src="@/assets/bluethumb-nav-logo.svg">.
Running a clean production build (RAILS_ENV=production bin/vite build after rake vite:clobber) with Vite 8.1.3:
$ grep -o 'bluethumb-nav-logo[^"]*' public/vite/.vite/manifest.json | sort -u
bluethumb-nav-logo-hxTv2K1u.svg
bluethumb-nav-logo.svg
$ grep -o 'bluethumb-nav-logo[^"]*' public/vite/.vite/manifest-assets.json | sort -u
bluethumb-nav-logo-hxTv2K1u2.svg
bluethumb-nav-logo.svg
$ find public/vite -iname '*nav-logo*'
public/vite/assets/bluethumb-nav-logo-hxTv2K1u.svg
Note the hash in manifest-assets.json (hxTv2K1u2) differs from the one in manifest.json (hxTv2K1u) by one extra character, for what should be byte-identical source content. Only the manifest.json hash's file is actually written to disk. Since manifest-assets.json is loaded after manifest.json and merged with Hash#merge (later wins), vite_asset_path("assets/bluethumb-nav-logo.svg") returns the path to the missing file, and the app serves a 404 for that asset in production.
This reproduces on every clean build (tried 3 times in a row, same result each time). Reverting vite to ^7.3.0 with no other changes makes both manifests agree on the same hash immediately, and the asset resolves correctly.
I believe this is caused by fingerprintRemainingAssets calling ctx.emitFile a second time (to build manifest-assets.json) for an asset that Rollup/Vite's own asset pipeline already fingerprinted once (because it's also referenced from JS/Vue). On Vite 7 both emitFile calls apparently produced the same content hash for the same buffer, so Rollup deduped them into one physical file referenced consistently by both manifests. Something in Vite 8's bundled Rollup appears to make these two emitFile calls for identical content produce different filenames, breaking the assumption that manifest-assets.json and manifest.json stay in sync for shared assets.
System info
vite: 8.1.3
vite-plugin-ruby: 5.2.2
vite_ruby: 3.10.2
vite_rails: 3.11.0
rails: 8.1.1
ruby: 3.4.5
node: 22.22.3
Description 📖
After upgrading
vitefrom7.3.xto8.1.3(keepingvite-plugin-ruby@5.2.2), a static asset that is both imported from application JS/Vue code and referenced directly from Ruby viavite_asset_pathends up with two different content hashes acrosspublic/vite/.vite/manifest.jsonandpublic/vite/.vite/manifest-assets.json.Since
ViteRuby::Manifest#load_manifestmerges all manifest files with.inject({}, &:merge)(later files win), the entry frommanifest-assets.jsonoverrides the one frommanifest.json. Only the file referenced bymanifest.jsonactually exists on disk (with the correct/deduped hash), sovite_asset_pathresolves to a hashed filename that was never written topublic/vite/assets, and the app 404s on that asset.On Vite
7.3.xwith the exact same source files and the exact samevite-plugin-rubyversion, both manifests agree on the same hash and everything works.Reproduction
assets/bluethumb-nav-logo.svglives underapp/javascript/assets/.vite_asset_path("assets/bluethumb-nav-logo.svg")(used inside a Rails helper/component to render<img>).<img src="@/assets/bluethumb-nav-logo.svg">.Running a clean production build (
RAILS_ENV=production bin/vite buildafterrake vite:clobber) with Vite 8.1.3:Note the hash in
manifest-assets.json(hxTv2K1u2) differs from the one inmanifest.json(hxTv2K1u) by one extra character, for what should be byte-identical source content. Only themanifest.jsonhash's file is actually written to disk. Sincemanifest-assets.jsonis loaded aftermanifest.jsonand merged withHash#merge(later wins),vite_asset_path("assets/bluethumb-nav-logo.svg")returns the path to the missing file, and the app serves a 404 for that asset in production.This reproduces on every clean build (tried 3 times in a row, same result each time). Reverting
viteto^7.3.0with no other changes makes both manifests agree on the same hash immediately, and the asset resolves correctly.I believe this is caused by
fingerprintRemainingAssetscallingctx.emitFilea second time (to buildmanifest-assets.json) for an asset that Rollup/Vite's own asset pipeline already fingerprinted once (because it's also referenced from JS/Vue). On Vite 7 bothemitFilecalls apparently produced the same content hash for the same buffer, so Rollup deduped them into one physical file referenced consistently by both manifests. Something in Vite 8's bundled Rollup appears to make these twoemitFilecalls for identical content produce different filenames, breaking the assumption thatmanifest-assets.jsonandmanifest.jsonstay in sync for shared assets.System info
bundle update vite_ruby(already on latest 3.10.2).