One of the notable features of the recent Rails 7 release is CSS/JS bundling, which replaces Rails 6’s Webpacker.

Rails now creates projects that will add two parameters.

1
2
-j, [--javascript=JAVASCRIPT]  # Choose JavaScript approach [options: importmap (default), webpack, esbuild, rollup]
-c, [--css=CSS]                # Choose CSS processor [options: tailwind, bootstrap, bulma, postcss, sass...]

For example, to use esbuild and sass you can use the following command.

1
$ rails new myapp -j esbuild -c sass

But most people are probably concerned about how to switch from an existing project to a new solution, as explained further below.

Script switching

Rails has added two gems cssbundling-rails and jsbundling-rails to support the bundling scheme. They depend on Rails >= 6, so you don’t need to upgrade to Rails 7 to use them. Add the following to the Gemfile.

1
2
gem 'cssbundling-rails'
gem 'jsbundling-rails'

After executing bundle install, install the required front-end tools with the following command.

1
2
$ bin/rails css:install:sass
$ bin/rails javascript:install:esbuild

You can see what has changed by using git diff, the things to note are.

  1. the CSS and JS build source files are now app/assets/stylesheets/application.sass.scss and app/javascript/application.js, which need to be migrated manually.
  2. app/assets/builds is the place to store the compiled static files.
  3. add a bin/dev script to start rails server and CSS/JS build process together with foreman, the configuration file is Procfile.dev.
  4. Check the tags in the layout and remove the webpacker reference.

If the project files are placed according to the default Rails 6 directory structure, you can see the CSS and JS build processes working if you start the development process with bin/dev.

Manual Switching

If your project is more complex and scripted installation is not sufficient, then it can be handled manually. In reality the contents of cssbundling-rails and jsbundling-rails are just some installation scripts and Rake tasks, there is no runtime code. Understanding how to manually switch to CSS/JS bundling can make the configuration more compatible with the project’s needs. Here’s how to do it.

Adding a builds directory

Assume that the project’s static files are stored in the following structure.

1
2
3
4
5
6
7
8
9
app/assets/
├── config
│   └── manifest.js
├── images
└── stylesheets
    └── application.css
app/javascript/
└── packs
    └── application.js

First create the builds directory to store the compiled files.

1
2
$ mkdir app/assets/builds
$ touch app/assets/builds/.keep

Add this directory to .gitignore to avoid checking in the files inside.

1
2
/app/assets/builds/*
!/app/assets/builds/.keep

Modify app/assets/config/manifest.js to remove the following configuration.

1
//= link_directory ../stylesheets .css

Add the following configuration.

1
//= link_tree ../builds

Now Assets pipeline knows to get the static files from the builds directory.

Configuring sass

Installing sass.

1
$ yarn add sass

Add the following to package.json.

1
2
3
  "scripts": {
    "build:css": "sass app/assets/stylesheets/application.scss app/assets/builds/application.css --no-source-map --load-path=node_modules"
  }

Create the file /app/assets/stylesheets/application.scss and migrate the contents of app/assets/stylesheets/application.css into it.

Note that the require_tree and require_self comments provided by Assets Pipeline no longer work and should be replaced with @import from sass.

If it works, use yarn build:css to see the CSS compiled successfully.

Configuring esbuild

Install esbuild.

1
$ yarn add esbuild

Add the following to package.json.

1
2
3
  "scripts": {
    "build:js": "esbuild app/javascript/application.js --bundle --outdir=app/assets/builds"
  }

Create the file app/javascript/application.js and migrate the contents of app/javascript/packs/application.js into it.

Note that if you used webpacker before, you may need to change the require syntax if you switch to esbuild, for example, import "channels" should be changed to import ". /channels". Also webpack dependent statements will need to be changed, for example Stimulus’ Webpack Helpers will not be available.

If it works, you can see that the JS compilation was successful using yarn build:js.

Adding the bin/dev script

Now you need to start three processes in your development environment, a rails server, a sass, and an esbuild. starting these three processes each time you develop can be tedious, so you can use some tools to manage them, here is foreman for example.

Add a bin/dev file with the following content

1
2
3
4
5
6
7
8
9
#!/usr/bin/env bash

if ! command -v foreman &> /dev/null
then
  echo "Installing foreman..."
  gem install foreman
fi

foreman start -f Procfile.dev

Add executable permissions to bin/dev.

1
$ chmod +x bin/dev

Add the file Procfile.dev with the following contents

1
2
3
web: bin/rails server -p 3000
css: yarn build:css --watch
js: yarn build:js --watch

This allows you to start three processes together with bin/dev while developing.

Adding the Rake task

The last thing is for Assets Pipeline to properly invoke external builds when compiling static files.

Add the file lib/tasks/build.rake with the following contents

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
namespace :build do
  desc "Run yarn install"
  task :install do
    system "yarn install"
  end

  desc "Build Javascript and CSS"
  task :all => [:javascript, :css]

  desc "Build JavaScript"
  task :javascript => :install do
    system "yarn build:js"
  end

  desc "Build CSS"
  task :css => :install do
    system "yarn build:css"
  end
end

Rake::Task["assets:precompile"].enhance(["build:all"])

If it works, build:css and build:js will now be executed automatically when you execute bin/rails assets:precompile.

This is the process of manually switching between CSS/JS bundling. Depending on the complexity of your project, you may need to make changes accordingly. After the switch is complete, you can delete the webpacker-related packages and configuration.

Summary

After reading how to switch bundling manually, you should see that the bundling is done before the Assets Pipeline. After compiling, you put the files into the builds directory and tell Assets Pipeline to read them from that directory.

This is a process that decouples the Rails main body from the front-end toolchain, so we can work with the front-end files in any way we like, as long as it’s finally incorporated into the Assets Pipeline management. This adds a great deal of flexibility and eliminates the need to fight the framework’s default configuration - it’s all a matter of choice.