Skip to content

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

Description

@vitalyliber

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
  • I have tried upgrading by running bundle update vite_ruby (already on latest 3.10.2).
  • I have read the troubleshooting section before opening an issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions