RailsVite
Vite integration for Rails, inspired by Laravel's Vite plugin. No proxy, no config duplication, no magic.
Table of Contents
- How It Works
- Quick Start
- Usage
- Vite Config
- Adding Frameworks
- SSR
- Auto Build
- Testing the Build
- Custom Paths
- Rake Tasks
- jsbundling Mode
- Migrating from vite_rails
- Contributing
- License
How It Works
Development: The Vite plugin writes tmp/rails-vite.json with the dev server URL. The Rails helper reads it and emits <script> tags pointing directly at Vite. The browser talks to Vite — Puma never touches your assets.
Production: vite build outputs fingerprinted assets to public/vite/ with a standard Vite manifest. The Rails helper reads the manifest and emits the correct tags.
No Rack proxy. No config/vite.json. No extra binstubs.
Quick Start
Add to your Gemfile:
gem "rails_vite"Run the install generator:
bundle install
bin/rails generate rails_vite:installThis creates vite.config.ts, installs dependencies, and updates your layout.
Start development:
bin/devUsage
In your layout:
<%= vite_tags "application.js" %>Short names are automatically prefixed with sourceDir (default: app/javascript). Paths containing / are used as-is.
Development output (when tmp/rails-vite.json exists):
<script src="http://localhost:5173/@vite/client" type="module"></script>
<script src="http://localhost:5173/app/javascript/application.js" type="module"></script>Production output (reads manifest):
<link rel="modulepreload" href="/vite/assets/vendor-b3c4d5e6.js" />
<script src="/vite/assets/application-a1b2c3d4.js" type="module"></script>
<link rel="stylesheet" href="/vite/assets/application-x9y8z7w6.css" />Helpers
| Helper | Purpose |
|---|---|
vite_tags(*entries, **options) |
Emits script, stylesheet, and modulepreload tags |
vite_javascript_tag(*entries, **options) |
Same as vite_tags, appends .js to extensionless names |
vite_stylesheet_tag(*entries, **options) |
Same as vite_tags, appends .css to extensionless names |
vite_typescript_tag(*entries, **options) |
Same as vite_tags, appends .ts to extensionless names |
vite_asset_path(name) |
Returns the fingerprinted path from the manifest |
vite_image_tag(name, **options) |
Image tag with manifest-resolved src |
All tag helpers accept arbitrary HTML attributes:
<%= vite_tags "application.js", "application.css",
"data-turbo-track": "reload", nonce: content_security_policy_nonce %>CSS Entry Points
CSS files are detected by extension and emit <link rel="stylesheet">:
<%= vite_tags "application.css" %>CSP Nonces
<%= vite_tags "application.js", nonce: content_security_policy_nonce %>Subresource Integrity (SRI)
SRI lets browsers verify that fetched assets haven't been tampered with by checking cryptographic hashes. Install the vite-plugin-manifest-sri plugin:
npm install -D vite-plugin-manifest-sriimport { defineConfig } from 'vite';
import rails from 'rails-vite-plugin';
import manifestSRI from 'vite-plugin-manifest-sri';
export default defineConfig({
plugins: [
rails(),
manifestSRI(),
],
});That's it — integrity and crossorigin="anonymous" attributes are automatically added to all script, stylesheet, and modulepreload tags when the manifest includes integrity hashes.
Asset Discovery (Images, Fonts)
Use import.meta.glob in your entry point to include assets in the Vite manifest:
// app/javascript/application.js
import.meta.glob(['../assets/images/**'], { eager: true });Then reference them in views:
<%= vite_image_tag "app/assets/images/logo.png", alt: "Logo" %>Vite Config
The install generator creates a minimal vite.config.ts:
import { defineConfig } from 'vite';
import rails from 'rails-vite-plugin';
export default defineConfig({
plugins: [
rails(),
],
});Plugin Options
| Option | Default | Description |
|---|---|---|
input |
auto-detected | Entry point(s). If sourceDir/entrypoints/ exists, all files in it are used. Otherwise, detects application.{js,ts,jsx,tsx} in sourceDir
|
sourceDir |
'app/javascript' |
Source directory. Short names are prefixed with this. Also sets the @ import alias |
ssr |
— | SSR entry point |
ssrOutDir |
'ssr' |
SSR output directory |
devMetaFile |
'tmp/rails-vite.json' |
Dev metadata file path |
buildDir |
'vite' |
Build output subdirectory inside public/
|
publicDir |
'public' |
Public directory |
refresh |
true |
Paths to watch for full-page reload. true watches app/views/** and app/helpers/**
|
Multiple Entry Points
rails({
input: ['application.js', 'admin.js'],
})<!-- In application layout -->
<%= vite_tags "application.js" %>
<!-- In admin layout -->
<%= vite_tags "admin.js" %>Custom Source Directory
rails({
input: ['entrypoints/application.ts', 'entrypoints/admin.ts'],
sourceDir: 'app/frontend',
})<%= vite_tags "entrypoints/application.ts" %>Adding Frameworks
React
npm install -D @vitejs/plugin-reactimport { defineConfig } from 'vite';
import rails from 'rails-vite-plugin';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
react(),
rails(),
],
});The React Refresh preamble is injected automatically when @vitejs/plugin-react is detected — no manual setup needed.
Vue
npm install -D @vitejs/plugin-vueimport { defineConfig } from 'vite';
import rails from 'rails-vite-plugin';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [
vue(),
rails(),
],
});SSR
Set ssr to the entry point used for server-side rendering. When you run npx vite build --ssr, the plugin uses this as the input and outputs to the ssrOutDir (default: ssr/).
rails({
ssr: 'ssr.tsx',
})Build and run:
npx vite build && npx vite build --ssr
node ssr/ssr.jsAuto Build
When the Vite dev server is not running, rails_vite automatically rebuilds assets on the first request if sources have changed. This is useful for system tests and quick checks without running bin/dev.
Disable it:
# config/initializers/rails_vite.rb
Rails.application.config.rails_vite.auto_build = falseBy default, auto build is enabled in development and test (Rails.env.local?).
Note: for parallel test runners, disable auto build and use rake vite:build before the suite instead.
Testing the Build
To verify your production build works in development:
rake vite:build # build assets
bin/rails s # start Rails without Vite dev serverWithout the Vite dev server running (no tmp/rails-vite.json), Rails serves built assets from public/vite/. To switch back to dev mode, start Vite again — the dev metadata takes priority.
Clean up built assets with rake vite:clobber.
Custom Paths
If you override build.outDir in vite.config.ts, tell the gem where to find things:
# config/initializers/rails_vite.rb
Rails.application.config.rails_vite.manifest_path = Rails.root.join("public/custom/manifest.json")
Rails.application.config.rails_vite.asset_prefix = "/custom"Defaults match the plugin defaults — no config needed if you follow conventions.
Rake Tasks
| Task | Description |
|---|---|
rake vite:build |
Build assets for production |
rake vite:install |
Install JavaScript dependencies |
rake vite:clobber |
Remove public/vite/
|
vite:build hooks into assets:precompile and test:prepare automatically. Skip with SKIP_VITE_BUILD=1.
jsbundling Mode
If you're using jsbundling-rails with Propshaft and want Vite as your bundler, you don't need the rails_vite gem — just the npm package:
npm install -D rails-vite-plugin vite// vite.config.ts
import { defineConfig } from 'vite';
import jsbundling from 'rails-vite-plugin/jsbundling';
export default defineConfig({
plugins: [
jsbundling(),
],
});How it works: In production, Vite builds to public/assets/ and copies entry files to app/assets/builds/ so Propshaft can serve them via javascript_include_tag and stylesheet_link_tag. In development, the plugin writes stub files to app/assets/builds/ that redirect the browser to Vite's dev server for HMR.
jsbundling Options
| Option | Default | Description |
|---|---|---|
input |
auto-detected | Entry point(s). If sourceDir/entrypoints/ exists, all files in it are used. Otherwise, detects application.{js,ts,jsx,tsx} in sourceDir
|
sourceDir |
'app/javascript' |
Source directory. Short names are prefixed with this. Also sets the @ import alias |
assetPipelineDir |
'app/assets/builds' |
Directory where Propshaft/Sprockets picks up entry files |
outputDir |
'public/assets' |
Public directory for the full Vite build output |
ssr |
— | SSR entry point. String or { entry, outDir }
|
refresh |
— | Paths to watch for full-page reload. true watches app/views/** and app/helpers/**
|
devMetaFile |
'tmp/rails-vite.json' |
Dev metadata file path. Set to false to disable |
Replacing esbuild
In your Procfile.dev, replace the esbuild command:
web: bin/rails server -p 3000
-js: yarn build --watch
+vite: npx vite
CSS and JS entries in app/javascript/entrypoints/ are auto-discovered. Both javascript_include_tag and stylesheet_link_tag work unchanged — Propshaft resolves them from app/assets/builds/ as before.
Frameworks
React and Vue work the same as in the standard plugin — add the framework plugin before jsbundling():
import { defineConfig } from 'vite';
import jsbundling from 'rails-vite-plugin/jsbundling';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
react(),
jsbundling(),
],
});Upgrading to rails_vite
To switch from jsbundling mode to the full rails_vite gem:
- Add
gem "rails_vite"to your Gemfile andbundle install - Change the import in
vite.config.tsfromrails-vite-plugin/jsbundlingtorails-vite-plugin - Replace
javascript_include_tag/stylesheet_link_tagwithvite_tagsin your layouts - Remove
jsbundling-railsfrom your Gemfile
In development, jsbundling mode writes tmp/rails-vite.json — the same file the rails_vite gem reads. You can add the gem and verify vite_tags works in dev before deploying.
Migrating from vite_rails
1. Swap dependencies
# Gemfile
- gem "vite_rails"
+ gem "rails_vite"// package.json — replace vite-plugin-ruby with rails-vite-plugin
- "vite-plugin-ruby": "^5.1.1"
+ "rails-vite-plugin": "^0.2.0"2. Replace vite.config.ts
import { defineConfig } from 'vite';
import rails from 'rails-vite-plugin';
export default defineConfig({
plugins: [
rails({
sourceDir: 'app/frontend',
}),
],
});If you have an entrypoints/ directory inside sourceDir, all files in it are auto-discovered — no need to list them. Otherwise, set input explicitly.
3. Delete files
-
config/vite.json— settings now live invite.config.ts -
bin/vite— no longer needed,Procfile.devrunsnpx vitedirectly
4. Update layouts
Remove vite_client_tag and vite_react_refresh_tag — both are automatic now.
The vite_javascript_tag, vite_stylesheet_tag, and vite_typescript_tag helpers work as drop-in replacements:
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/skryukov/rails_vite.
License
The gem is available as open source under the terms of the MIT License.