---
title: "Howto migrate from Webpacker to jsbundling-rails in Ruby on Rails"
description: "Migrating an existing Ruby on Rails Application using webpacker to jsbundling-rails. Sunset webpacker now."
url: "https://qameta.com/posts/howto-migrate-from-webpacker-to-jsbundling-rails-in-ruby-on-rails/"
image: "https://qameta.com/images/posts/webpacker-to-js-bundling-in-ruby-on-rails.png"
---

# Howto migrate from Webpacker to jsbundling-rails in Ruby on Rails

![Using jsbundling-rails for JavaScript pipeline](https://qameta.com/images/posts/webpacker-to-js-bundling-in-ruby-on-rails.png)

A lot of [Ruby on Rails](https://rubyonrails.org) applications created with Ruby
on Rails version 5.x or 6.x or even upgraded from the previous versions are
using [webpacker](https://github.com/rails/webpacker) as their primary asset
pipeline. But now as webpacker has retired, it is time to move on.
[jsbundling-rails](https://github.com/rails/jsbundling-rails) is or will be
the most obvious step to go. Don't try to move to
[importmap-rails](https://github.com/rails/importmap-rails) directly, as you may
encounter more problems you have to solve.

If you are using webpack(er) for [CSS](https://en.wikipedia.org/wiki/CSS) too
as I did, I recommend moving your CSS pipeline to an alternative before moving
your [JavaScript](https://en.wikipedia.org/wiki/JavaScript) setup to an
alternative like [jsbundling-rails](https://github.com/rails/jsbundling-rails)
or [importmap-rails](https://github.com/rails/importmap-rails). Check this:
[Howto migrate from Webpacker to cssbundling-rails in Ruby on Rails for
CSS](https://qameta.com/posts/howto-migrate-from-webpacker-to-cssbundling-rails-in-ruby-on-rails/).

## Why move at all?

As previously said, [webpacker](https://github.com/rails/webpacker) was sunset
and development already stopped in 2021. Also, webpacker has a lot of old
dependencies, especially [webpack](https://github.com/webpack/webpack) version 3
which can't be upgraded.

Webpacker suffers a lot of problems that were never solved and now never would
be solved at all. It was pretty hard to set up, but delivered quite a lot of
useful and needed features back in the days when only
[sprockets-rails](https://github.com/rails/sprockets-rails) were the only
option for handling assets.

## Prepare your project

First of all, you have to upgrade your project to at least rails 6.1.x (better
rails 7.0.x) as it has everything prepared to get your project on
jsbundling-rails. According to Gemfile from
[jsbundling-rails](https://github.com/rails/jsbundling-rails/blob/main/Gemfile)
Ruby on Rails 6.1.0 is the minimum version.

I've tried to get all of my projects to the up-to-date Ruby on Rails (current is
7.0) before I tackle such issues. But this is your decision. Ruby on Rails 6.1.X
is enough for this step.

## Moving images delivered through webpack(er) first

If you have loaded images through webpacker, you should move them before
installing jsbundling-rails. As soon as you remove webpacker from your build
system, all *_pack_* links will be broken.

You can search for them using `grep` or `ag`:

```sh
# Using grep
grep -R "_pack_" app/
# Using ag
ag "_pack_" app/
```

**HINT:** Search in *app/* instead of *app/views* as your project might have
helpers or other things, which may use paths from pack.

## Install jsbundling-rails gem

Installing [jsbundling-rails](https://github.com/rails/jsbundling-rails) is
pretty straightforward:

```sh
./bin/bundle add jsbundling-rails
```

You can also add it through your favorite editor to the Gemfile and run `bundle
install` to install it.

## Select and run the setup for your project

[jsbundling-rails](https://github.com/rails/jsbundling-rails) has three
different install options you can use:

* [esbuild](https://esbuild.github.io)
* [rollup](https://rollupjs.org)
* [webpack](https://webpack.js.org)

I prefer using webpack as the first step, as most of the things will stay the
same and you don't need to tackle differences to esbuild or rollup. In my
experience, most projects will be able to migrate to esbuild directly.

Check which one you prefer and run:

```sh
./bin/rails javascript:install:[esbuild|rollup|webpack]
```

I'll explain how to use esbuild here. But most things are the same for all three
of them.

## Load application.js built by jsbundling-rails

If you are not using `application.html.erb` as your primary layout file or you
have different layout files, align the changes to your needs:

```erb
-    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload'
+      'data-turbolinks-track': 'reload' %>
+    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload',
+      defer: true %>
```

As not all applications are running
[hotwired/turbo](https://www.npmjs.com/package/@hotwired/turbo) before migrating
to jsbundling-rails, this part must be aligned to your needs:

* `data-turbo-track` for hotwired/turbo
* `data-turbolinks-track` for turbolinks

Update Procfile.dev to use es yarn for JavaScript:

```diff
diff --git a/Procfile.dev b/Procfile.dev
index f418999..f3abf32 100644
--- a/Procfile.dev
+++ b/Procfile.dev
@@ -1,3 +1,3 @@
 web: bin/rails server -p $PORT
-webpack: bin/webpack-dev-server
 css: yarn build:css --watch
+js: yarn build --watch
```

Your package.json using esbuild should look similar to this:

```diff
diff --git a/package.json b/package.json
index ba79da2..4275619 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,7 @@
     "@rails/webpacker": "^5.2.1",
     "bootstrap": "^4.6.0",
     "bootstrap-icons": "^1.10.3",
+    "esbuild": "^0.17.8",
     "jquery": "^3.6.0",
     "popper.js": "^1.16.1",
     "sass": "^1.58.0",
@@ -21,6 +22,7 @@
     "fsevents": "^2.3.2"
   },
   "scripts": {
-    "build:css": "sass ./app/assets/stylesheets/application.scss:./app/assets/builds/application.css --no-source-map --load-path=node_modules"
+    "build:css": "sass ./app/assets/stylesheets/application.scss:./app/assets/builds/application.css --no-source-map --load-path=node_modules",
+    "build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=assets"
   }
```

## Installing stimulus through gem

Before stimulus was installed using webpack(er) and `package.json` file.
Everything was separated from Ruby on Rails. To make installation easier
[stimulus-rails](https://github.com/hotwired/stimulus-rails) was built, which
makes installing stimulus a part of rails command line tooling.

First of all, add `stimulus-rails` to your Gemfile by running this command or
manually:

```sh
./bin/bundle add stimulus-rails
```

As stimulus-rails know whether you have
[importmap-rails](https://github.com/rails/importmap-rails) or
[jsbundling-rails](https://github.com/rails/jsbundling-rails) installed, it does
the basic setup for you through this command:

```sh
./bin/rails stimulus:install
```

The next step is to load all your existing controllers by adding them to
`app/javascript/controllers/application.js` through this command:

```sh
./bin/rails stimulus:manifest:update
```

I expect you to use [git](https://git-scm.com) or any other  version control for
your project. Just let the stimulus installation overwrite all the existing
files and have a look at them. This one makes it simpler to get them running
again.

If you want to load all your controllers, have a look at: [JavaScript
Bundling](https://github.com/hotwired/stimulus-rails#with-javascript-bundler)
how to do this.

### Importing third party stimulus controllers

If you are using some kind of third party stimulus controllers like from
[stimulus components](https://www.stimulus-components.com/) or
[stimulus-use](https://stimulus-use.github.io/stimulus-use/) they will be
removed by manifest update:

```diff
-//Load controller from npm
-import StimulusTomSelect from "stimulus-tom-select";
-application.register("tom-select", StimulusTomSelect);
```

You need to add them manually after every manifest update, as they get removed.

## New JavaScript Entry point

After you have run `./bin/rails javascript:install:[esbuild|rollup|webpack]`
there is a new entry point defined in your setup, which is located under
`app/javascript/application.js`.

To make the migration simpler, just move `app/javascript/packs/application.js`
to `app/javascript/application.js` and update to something like:

```diff
diff --git app/javascript/packs/application.js app/javascript/application.js

+// Entry point for the build script in your package.json

-// console.log('Hello World webpacker')
+// console.log('Hello World jsbundling-rails')


import Turbolinks from "turbolinks"
Turbolinks.start()

// Load and start ujs
import Rails from "@rails/ujs"
Rails.start()

// Channels
-// import "channels"
+// import "./channels"

// Import controllers/index.js
-import "controllers"
+import "./controllers"

// Bootstrap and jQuery
-import "src/bootstrap_custom"
+import "./src/bootstrap_custom"
```

Before webpack did the lookup by trying the local path first and then lookup in
`node_modules`. Now you need to define, that an import is located in the current
path by defining it for every import:

```diff
-import "controllers"
+import "./controllers"
```

You can define a single file like we did with:

```diff
-import "src/bootstrap_custom"
+import "./src/bootstrap_custom"
```

Or you can import a directory by importing the `index.js` file in a directory
like this:

```diff
// Import controllers/index.js
-import "controllers"
+import "./controllers"
```

### Bootstrap 5 Import

Now you need to import [Bootstrap 5](https://getbootstrap.com/docs/5.2/) related
JavaScript code and setup tooltips and popover to work.

If you are on Bootstrap 5 and
[turbolinks](https://github.com/turbolinks/turbolinks) it will look something
like this:

```js
import * as bootstrap from 'bootstrap'

document.addEventListener("turbolinks:load", function() {
  var tooltipTriggerList = [].slice.call(
  document.querySelectorAll('[data-bs-toggle="tooltip"]'))

  var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
      return new bootstrap.Tooltip(tooltipTriggerEl)
  })

  var popoverTriggerList = [].slice.call(
  document.querySelectorAll('[data-bs-toggle="popover"]'))

  var popoverList = popoverTriggerList.map(function (popoverTriggerEl) {
    return new bootstrap.Popover(popoverTriggerEl)
  })
})
```

If you are on Bootstrap 5 with
[hotwired/turbo](https://www.npmjs.com/package/@hotwired/turbo) it is:

```js
import * as bootstrap from 'bootstrap'

document.addEventListener("turbo:load", function() {
  var tooltipTriggerList = [].slice.call(
  document.querySelectorAll('[data-bs-toggle="tooltip"]'))

  var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
      return new bootstrap.Tooltip(tooltipTriggerEl)
  })

  var popoverTriggerList = [].slice.call(
  document.querySelectorAll('[data-bs-toggle="popover"]'))

  var popoverList = popoverTriggerList.map(function (popoverTriggerEl) {
    return new bootstrap.Popover(popoverTriggerEl)
  })
})
```
### Bootstrap 4 Import

Now you need to import [Bootstrap 4](https://getbootstrap.com/docs/4.6/) related
JavaScript code and setup tooltips and popover to work. Bootstrap 4 still needs
[jQuery](https://jquery.com) to work, that's why it must be imported first.

If you are using Bootstrap 4 with
[turbolinks](https://github.com/turbolinks/turbolinks):

```diff
+// jQuery as global
+export { $ } from 'jquery'

// Import everything
import * as bootstrap from 'bootstrap'

document.addEventListener("turbolinks:load", function() {
  var tooltipTriggerList = [].slice.call(
  document.querySelectorAll('[data-bs-toggle="tooltip"]'))

  var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
      return new bootstrap.Tooltip(tooltipTriggerEl)
  })

  var popoverTriggerList = [].slice.call(
  document.querySelectorAll('[data-bs-toggle="popover"]'))

  var popoverList = popoverTriggerList.map(function (popoverTriggerEl) {
    return new bootstrap.Popover(popoverTriggerEl)
  })
})
```

If you are using Bootstrap 4 with
[hotwired/turbo](https://www.npmjs.com/package/@hotwired/turbo):

```diff
+// jQuery as global
+export { $ } from 'jquery'

// Import everything
import * as bootstrap from 'bootstrap'

document.addEventListener("turbo:load", function() {
  var tooltipTriggerList = [].slice.call(
  document.querySelectorAll('[data-bs-toggle="tooltip"]'))

  var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
      return new bootstrap.Tooltip(tooltipTriggerEl)
  })

  var popoverTriggerList = [].slice.call(
  document.querySelectorAll('[data-bs-toggle="popover"]'))

  var popoverList = popoverTriggerList.map(function (popoverTriggerEl) {
    return new bootstrap.Popover(popoverTriggerEl)
  })
})
```

## Remove webpack(er) from your project

First of all remove everything webpacker related from your Gemfile:

```diff
-# Install webpacker
-gem 'webpacker', '~> 5.2'

@@ -113,7 +111,6 @@ group :development do
   gem 'capistrano-asdf', require: false
   gem 'capistrano-passenger', require: false
   gem 'capistrano-rails', '~> 1.6', require: false
-  gem 'capistrano-webpacker-precompile', require: false
 end
```

After removing run `bundle install`.

### Remove JS dependencies

As webpacker has also a lot of JavaScript dependencies, you can now get rid of
them too in your *package.json*:

```diff
git diff package.json
diff --git a/package.json b/package.json
index 7340977..3b488f7 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,6 @@
   "dependencies": {
     "@hotwired/stimulus": "^3.2.1",
     "@rails/ujs": "^7.0.0",
-    "@rails/webpacker": "^5.2.1",
     "bootstrap": "^4.6.0",
     "bootstrap-icons": "^1.10.3",
     "esbuild": "^0.17.8",
@@ -17,7 +16,6 @@
     "turbolinks": "^5.2.0"
   },
   "devDependencies": {
-    "webpack-dev-server": "^3.11.0"
   },
```

Now run `yarn install` and see your `yarn.lock` file shrink by at least 2900
lines.

This is the great moment where you can run `yarn audit` again:

```sh
% yarn audit
yarn audit v1.22.19
0 vulnerabilities found - Packages audited: 55
✨  Done in 0.33s.
```

As next step you have to get rid of all webpack(er) related configurations by
removing these files:

* config/webpack/  <- just remove the whole directory
* config/webpacker.yml
* app/javascript/packs/ <- just remove the directory
* bin/webpack-dev-server
* bin/webpack

You may have more files to check. Just `grep` or `ag` for files:

```sh
# use grep
grep -R "webpack" .
# use ag
ag "webpack" .
```

There may be some dependencies defined in your yarn.lock like this one for
[@hotwired/stimulus](https://www.npmjs.com/package/@hotwired/stimulus):

```sh
% ag "webpack" .
yarn.lock
115:"@hotwired/stimulus-webpack-helpers@^1.0.0":
117:  resolved "https://registry.yarnpkg.com/@hotwired/stimulus-webpack-helpers/-/stimulus-webpack-helpers-1.0.1.tgz#4cd74487adeca576c9865ac2b9fe5cb20cef16dd"
313:    "@hotwired/stimulus-webpack-helpers" "^1.0.0"
```

Now you can run assets compilation and check, whether it is working without
having any webpacker dependencies before deploying your project to staging or
even production:

```sh
# compile your assets
./bin/rails assets:precompile
# cleanup them afterwards
./bin/rails assets:clobber
```

If everything is working you should be done with removing webpacker from your
project.

## Extra: hot reloading in development

Before I used webpack for hot reloading. As webpack is gone, I need an other
solution.

There are several solutions you can use to:

* [hotwire-livereload](https://rubygems.org/gems/hotwire-livereload)
* [rails_live_reload](https://rubygems.org/gems/rails_live_reload)
* and many more

In current setup I use
[rails_live_reload](https://rubygems.org/gems/rails_live_reload). It is easy to
setup and do what it was expected to do. Have a look at the
[README on GitHub](https://github.com/railsjazz/rails_live_reload).

## Summary

By removing webpacker from your project, you can improve the build time for your
assets, fix a lot of JS related security issues by removing the lock on old webpack
version. You project will leave the deprecated webpacker setup.
