Migration Guide: Moving to Opal-Vite
This guide helps you migrate from traditional Opal setups (Sprockets, plain Opal, etc.) to Opal-Vite.
Table of Contents
- Why Migrate to Opal-Vite?
- Migration Paths
- Step-by-Step Migration
- Common Migration Scenarios
- Troubleshooting
- Migration Checklist
Why Migrate to Opal-Vite?
Benefits
- Faster Development: Hot Module Replacement (HMR) for instant feedback
- Modern Build Tool: Leverages Vite's optimized build pipeline
- Better DX: Improved error messages, source maps, and debugging
- Smaller Bundles: Tree-shaking and modern JavaScript output
- ES Modules: Native ESM support for better performance
- Stimulus Integration: Built-in support for Stimulus controllers
- TypeScript Support: Optional TypeScript for type safety
When to Migrate
✅ Good time to migrate:
- Starting a new project
- Major refactoring planned
- Performance issues with current setup
- Want to use modern JavaScript features
⚠️ Consider carefully:
- Large existing codebase (migrate incrementally)
- Tight deadlines (test thoroughly first)
- Heavy customizations to build process
Migration Paths
Path 1: From Sprockets (Rails Asset Pipeline)
Rails + Sprockets + Opal
↓
Rails + Opal-VitePath 2: From Plain Opal
Plain Opal + Custom Build
↓
Opal-VitePath 3: From Other Ruby-to-JS Solutions
Opal + Webpack/esbuild
↓
Opal-ViteStep-by-Step Migration
Prerequisites
- Node.js 18 or higher
- pnpm, npm, or yarn
- Ruby 3.0 or higher (if using gems)
Step 1: Project Setup
1.1 Initialize Node.js Project
# Initialize package.json
pnpm init
# Or if using existing project
pnpm install1.2 Install Dependencies
# Install Vite and Opal plugin
pnpm add -D vite vite-plugin-opal
# Install Stimulus (optional but recommended)
pnpm add @hotwired/stimulus1.3 Create Vite Config
Create vite.config.ts:
import { defineConfig } from 'vite';
import opal from 'vite-plugin-opal';
export default defineConfig({
plugins: [
opal({
/* options */
})
],
server: {
port: 3000
}
});Step 2: Project Structure Migration
Old Structure (Sprockets)
app/
├── assets/
│ └── javascripts/
│ ├── application.js
│ └── components/
│ └── my_component.rbNew Structure (Opal-Vite)
app/
├── javascript/
│ └── application.js # JavaScript entry
├── opal/
│ ├── application.rb # Ruby/Opal entry
│ └── controllers/
│ └── my_controller.rb # Stimulus controllers
└── styles/
└── application.css # StylesStep 3: Update Entry Points
3.1 JavaScript Entry (app/javascript/application.js)
Before (Sprockets):
//= require opal
//= require opal_ujs
//= require_tree .After (Opal-Vite):
// Import Stimulus
import { Application } from "@hotwired/stimulus"
// Expose Stimulus
window.Stimulus = Application.start()
window.application = window.Stimulus
// Import Opal code
import('../opal/application.rb')3.2 Opal Entry (app/opal/application.rb)
Before:
require 'opal'
require 'opal-jquery'
require_tree './components'After:
# backtick_javascript: true
require 'native'
require 'opal_stimulus/stimulus_controller'
# Load controllers
require 'controllers/my_controller'
# Register all controllers
StimulusController.register_all!Step 4: Migrate Components to Controllers
Before (Plain Opal Component)
# app/assets/javascripts/components/counter.rb
class Counter
def initialize
@count = 0
setup_listeners
end
def setup_listeners
`
document.querySelector('.increment').addEventListener('click', function() {
#{increment}
});
`
end
def increment
@count += 1
update_display
end
def update_display
`document.querySelector('.count').textContent = #{@count}`
end
end
Counter.newAfter (Stimulus Controller)
# app/opal/controllers/counter_controller.rb
# backtick_javascript: true
class CounterController < StimulusController
self.targets = ["count"]
self.values = { count: :number }
def connect
puts "Counter connected!"
end
def increment
`this.countValue = this.countValue + 1`
end
def count_value_changed
`this.countTarget.textContent = this.countValue`
end
endHTML Update:
<!-- Before -->
<div>
<span class="count">0</span>
<button class="increment">+</button>
</div>
<!-- After -->
<div data-controller="counter" data-counter-count-value="0">
<span data-counter-target="count">0</span>
<button data-action="click->counter#increment">+</button>
</div>Step 5: Update HTML
Before (Rails with Sprockets)
<!-- app/views/layouts/application.html.erb -->
<!DOCTYPE html>
<html>
<head>
<%= javascript_include_tag 'application' %>
</head>
<body>
<%= yield %>
</body>
</html>After (Rails with Vite)
Using vite_rails:
<!DOCTYPE html>
<html>
<head>
<%= vite_javascript_tag 'application' %>
</head>
<body>
<%= yield %>
</body>
</html>Or standalone HTML:
<!DOCTYPE html>
<html>
<head>
<title>My App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/app/javascript/application.js"></script>
</body>
</html>Step 6: Update Package Scripts
Add to package.json:
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
}
}Step 7: Environment Configuration
Create .env files:
# .env.development
VITE_API_URL=http://localhost:3000/api
# .env.production
VITE_API_URL=https://api.example.comAccess in code:
# Opal code
api_url = `import.meta.env.VITE_API_URL`Common Migration Scenarios
Scenario 1: Migrating jQuery Dependencies
Before:
require 'opal-jquery'
Element.find('#myButton').on(:click) do
alert 'Clicked!'
endAfter:
# backtick_javascript: true
class MyController < StimulusController
def connect
puts "Controller connected"
end
def handle_click
`alert('Clicked!')`
end
end<button data-controller="my" data-action="click->my#handle_click">
Click me
</button>Scenario 2: Migrating Global State
Before:
$app_state = {
user: { name: 'John' },
theme: 'light'
}After:
Use Stimulus Values:
class AppController < StimulusController
self.values = {
user: :object,
theme: :string
}
def connect
`
this.userValue = { name: 'John' };
this.themeValue = 'light';
`
end
def user_value_changed
`console.log('User changed:', this.userValue)`
end
endOr use LocalStorage:
def save_user(user)
`localStorage.setItem('user', JSON.stringify(#{user.to_n}))`
end
def load_user
`JSON.parse(localStorage.getItem('user') || '{}')`
endScenario 3: Migrating AJAX Requests
Before (opal-jquery):
HTTP.get('/api/users') do |response|
if response.ok?
puts response.json
end
endAfter (Fetch API):
def fetch_users
`
fetch('/api/users')
.then(response => response.json())
.then(users => {
console.log('Users:', users);
// Handle users
})
.catch(error => console.error('Error:', error));
`
endScenario 4: Migrating require_tree
Before:
# application.rb
require_tree './components'After:
# application.rb
require 'components/counter'
require 'components/modal'
require 'components/form'
# ... or individually require each fileBetter approach: Use controllers instead:
require 'controllers/counter_controller'
require 'controllers/modal_controller'
require 'controllers/form_controller'
StimulusController.register_all!Scenario 5: Migrating Gems with opal/ Directory
Some Opal gems (like inesita) use both lib/ and opal/ directories:
No changes needed! The vite-plugin-opal automatically detects and adds both directories to the load path, prioritizing opal/ over lib/ for browser compatibility.
Example with Inesita:
# Gemfile
gem 'inesita'
gem 'inesita-router'# app/opal/application.rb
require 'inesita'
require 'inesita-router'
# Works automatically - loads from opal/ directoryTroubleshooting
Issue: "Cannot find module"
Problem: Opal files not found during compilation.
Solution:
- Check file paths are correct
- Ensure
requirestatements match file structure - Add explicit paths to
vite.config.ts:
opal({
additionalLoadPaths: ['./app/lib', './vendor/opal']
})Issue: "backtick_javascript is required"
Problem: Inline JavaScript not working.
Solution: Add to top of Opal file:
# backtick_javascript: trueIssue: Controller not registering
Problem: Stimulus controller not found.
Solution:
- Ensure controller inherits from
StimulusController - Call
StimulusController.register_all! - Check controller naming (use kebab-case in HTML)
# MyThingController → my-thing
<div data-controller="my-thing">Issue: Opal gem dependencies
Problem: Server-side dependencies causing compilation errors.
Solution: Use Gemfile's :opal group or check gem for opal/ directory:
# Gemfile
group :opal do
gem 'inesita'
endThe plugin automatically prioritizes opal/ directories over lib/ to avoid server-side dependencies.
Issue: Build errors in production
Problem: Build works in dev but fails in production.
Solution:
- Check environment variables
- Ensure all dependencies installed
- Verify paths are correct
- Use absolute paths where possible
Migration Checklist
Pre-Migration
- [ ] Audit current dependencies
- [ ] Document current features
- [ ] Set up version control
- [ ] Create backup branch
- [ ] Test suite exists (if not, create basic tests)
During Migration
- [ ] Install Node.js and pnpm
- [ ] Create
vite.config.ts - [ ] Set up new directory structure
- [ ] Migrate entry points
- [ ] Convert components to controllers
- [ ] Update HTML with Stimulus attributes
- [ ] Migrate styles (if needed)
- [ ] Update package.json scripts
- [ ] Configure environment variables
Testing
- [ ] Test all features in development
- [ ] Test production build
- [ ] Verify HMR works
- [ ] Check source maps
- [ ] Test in multiple browsers
- [ ] Performance testing (bundle size, load time)
- [ ] Accessibility testing
Post-Migration
- [ ] Update documentation
- [ ] Train team on new setup
- [ ] Monitor for issues
- [ ] Optimize bundle size
- [ ] Set up CI/CD for new build process
Incremental Migration Strategy
For large projects, consider migrating incrementally:
Phase 1: Setup (Week 1)
- Install Vite and dependencies
- Set up basic configuration
- Create new directory structure (parallel to old)
Phase 2: Core Features (Week 2-3)
- Migrate most-used components
- Convert to Stimulus controllers
- Test thoroughly
Phase 3: Secondary Features (Week 4-5)
- Migrate remaining components
- Update all HTML
- Remove old asset pipeline
Phase 4: Cleanup (Week 6)
- Remove old code
- Optimize bundle
- Document changes
- Deploy to production
Resources
- Opal Documentation
- Vite Documentation
- Stimulus Handbook
- vite_rails - for Rails integration
Getting Help
- GitHub Issues: opal-vite/issues
- Opal Community: Gitter
- Stack Overflow: Tag with
opalandvite
Next Steps
After migration:
- Review TESTING.md for testing strategies
- Check TROUBLESHOOTING.md for common issues
- Explore examples/ for advanced patterns
- Set up CI/CD (see
.github/workflows/examples)
Happy migrating! 🚀
