JavaScript BundlingとCSS Bundlingのしくみ

Rails 5-6時代に使われた[Webpacker](https://github.com/rails/webpacker)は、公式に「has been retired」とされ、Rails 7では[importmap](https://github.com/rails/importmap-rails/)が標準となりました。

Rails7はimportmapとは別に、JavaScriptのバンドラやCSSのフレームワークを導入するしくみを用意しています。2つのgem、 [JavaScript Bundling（jsbundling-rails）](https://github.com/rails/jsbundling-rails) と [CSS Bundling（cssbundling-rails）](https://github.com/rails/cssbundling-rails) です。この2つは、Rails 6に入れることもできます。

## 概要

JavaScript Bundling は、JavaScriptのバンドラ（esbuildやwebpack）を使い、JSファイルを app/assets の下に出力するものです。CSS Bundling は、CSSのフレームワーク（Bootstrapやbulma）をsassコマンドで app/assets の下に出力します。バンドラやCSSフレームワークはyarnコマンド でnode_modules 下にインストールされます。

各コマンドは、package.json の中で指定されます。開発環境では、foreman（bin/dev）を使ってRailsサーバー、JSの変更監視、CSSの変更監視、の3つを動かします。

なお、この記事では CSS Bundling が扱えるCSSのうち、tailwindとpostcssについては試していません。この2つではsassコマンドは使われません。


## 背景

[DHHはRails 7のJavaScriptについていろいろ語っていますが](https://world.hey.com/dhh/rails-7-will-have-three-great-answers-to-javascript-in-2021-8d68191b)、私の考えでは、**もはやIEを配慮する必要はない** という環境の変化が大きいと思います。つまり、JSファイルにあれこれ変換をかけたりせずに、Web標準の機能をそののまま使えばいい、バンドラが必要なら好きなものを使ってちょ、というのが今後のRailsのノリなのではないでしょうか。

Webpackerを使うとデフォルトでBabelやcore-jsが入ってしまいます。IE対応なしにすれば、そのぶんアプリケーションをすっきりさせて軽くできます。

## インストール

Rails 7では、アプリケーション作成時に `-j`オプションで使用するバンドラ（importmap/webpack/esbuild/rollup）を指定できます。importmap以外を選ぶと、JavaScript Bundling が入ります。`--css`オプションでCSSフレームワーク（bootstrap/bulma）を指定すると、CSS Bundling が入ります（`--css sass`とすると、フレームワークなしでsassコマンドが使えます）。

```
% rails new app -j esbuild --css bootstrap
```

Rails 6、7にあとからインストールするときは、Gemfile で指定して`bundle install`します。

```ruby:Gemfile
gem "jsbundling-rails"
gem "cssbundling-rails"
```

あとからインストールしたときは、Rakeコマンド `javascript:install:何々` や `css:install:何々` でインストールと初期設定ができます。

app/assets/stylesheets/application.css が上書きされてしまうので、既存のアプリケーションに導入する場合はバックアップしておいてください。

```
% bin/rails javascript:install:esbuild
% bin/rails css:install:bootstrap
```

## package.json中のコマンド

package.json には、2つのコマンドが加わります。`yarn build`とするとバンドラ（以下の例はesbuild）がJSファイルを変換します。`yarn build:css`ではsassコマンドがscssファイルを変換します。

なお、production環境にデプロイするときは、assets:precompile の実行時にこの2つのコマンドが実行されます。

```json:package.json
  "scripts": {
    "build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds",
    "build:css": "sass ./app/assets/stylesheets/application.bootstrap.scss ./app/assets/builds/application.css --no-source-map --load-path=node_modules"
  }
```

package.json 中のesbuildやsassのオプションは、好きに変えられます。入力/出力ファイルの場所や名前を変えたければ、ここで指定します。

webpackの場合は webpack.config.js で、rollupでは rollup.config.js で入力/出力ファイルを指定できます。

## JSとCSSの置き場所

JavaScript Bundling でバンドル前のエントリーポイントとなるファイルは、app/javascript の直下の application.js となります。Webpackerの場合（app/javascript/packs）とは場所が変わるのでご注意ください。

CSS Bundling では、app/assets/stylesheets の下に application.bootstrap.scss（Bootstrapの場合）のような名前のファイルができます。

## JSとCSSの出力先

JS、CSSともに、出力されるファイルは app/assets/builds の下に入ります。app/assets/config/manifest.js には、アセットパイプラインが app/assets/builds の下のファイルを使うように指定されています。

`//= link_directory ../stylesheets .css` の行がある場合は、削除されることにご注意ください。

```app/assets/config/manifest.js
//= link_tree ../images
//= link_tree ../builds

// //= link_directory ../stylesheets .css は削除される
```

buildsの下に出力されたファイルは、昔ながらの stylesheet_link_tag や javascript_include_tag でアセットパイプライン経由でリンクできます。つまり、バンドラやsassで出力されたファイルは、さらにアセットパイプラインで処理されることになるわけです。

```erb:app/views/layouts/application.html.erb
    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
    <%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %>
```

## Procfile.dev

アプリケーションのルートにできる Procfile.dev では、一度に3つのプログラム（rails、JSバンドラ、sass）が常駐するように設定してあります。--watch オプションによってファイルを監視し、変更があれば自動的に変換します。

```Procfile.dev
web: bin/rails server -p 3000
js: yarn build --watch
css: yarn build:css --watch
```

binディレクトリの下のdevコマンドでは、[foreman](https://github.com/ddollar/foreman)を使って Procfile.dev のコマンドを動かしています。foremanがないときは自動的にインストールします。

```bash:bin/dev
#!/usr/bin/env bash

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

foreman start -f Procfile.dev "$@"
```

開発環境でRailsを動かすときは、`rails s` の代わりに `bin/dev` を使います。

```
% bin/dev
```

以上が、JavaScript Bundling と CSS Bundling のしくみです。


## 既存のJavaScript/CSSと同居させる

既存のRailsアプリケーションのJavaScriptやCSSと同居させつつ移行したいときは、コマンドのオプションや設定ファイルでファイル名を変更すればよいでしょう。

まずCSSですが、app/assets/config/manifest.js で`//= link_directory ../stylesheets .css`の行を復活させます。

```app/assets/config/manifest.js
//= link_tree ../images
//= link_tree ../builds
//= link_directory ../stylesheets .css
```

app/assets の下に application2 というディレクトリを作り、app/assets/stylesheets/application.bootstrap.scss （bootstrapの場合）を移動して application2.scss にリネームします。

package.json でsassコマンドの入力/出力ファイルの名前を変更します。


```json:package.json
  "scripts": {
    "build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds",
    "build:css": "sass ./app/assets/stylesheets2/application2.scss ./app/assets/builds/application2.css --no-source-map --load-path=node_modules"
  }
```

JavaScriptはそのままでもいいですが、試しに app/javascript/builds というディレクトリを作って app/javascript/application.js を移動し、application2.js にリネームします。

package.json でesbuildコマンドに渡すディレクトリ名を変更します。

```json:package.json
  "scripts": {
    "build": "esbuild app/javascript/builds/*.* --bundle --sourcemap --outdir=app/assets/builds",
    "build:css": "sass ./app/assets/stylesheets2/application2.scss ./app/assets/builds/application2.css --no-source-map --load-path=node_modules"
  }
```

webpackの場合は、設定ファイル webpack.config.js で入力/出力先を変更できます。

```webpack.config.js
  entry: {
    application2: "./app/javascript/builds/application2.js"
  },
```

既存のアプリケーションの中で、一部のページに JavaScript Bundling と CSS Bundling を使った新しいレイアウトを導入することにします。新しいレイアウトテンプレートを作り、JavaScriptとCSSを指定します。

```erb:app/views/layouts/application2.html.erb
<%= stylesheet_link_tag "application2", media: "all", "data-turbo-track": "reload" %>
<%= javascript_include_tag "application2", "data-turbo-track": "reload", defer: true %>
```

なお、Rails 7ではgemの sassc-rails がデフォルトで入らないので、既存のassetsで.scssファイルを使い続けたいときは、Gemfileで指定しておく必要があります。

```ruby:Gemfile
gem "sassc-rails"
```