ビルドとアセットパイプラインの動作についてまとめてみた

# はじめに
<!-- 概要を記載 -->
プログラミング初学者の者です。
Rails7でミニアプリを作成していましたがビルドやアセットパイプラインの働きについて理解ができていなかったためまとめました。
間違いなども多いとは思うのですがご指摘いただけると嬉しいです。

# 対象者
<!-- 対象者. Rails学習中の方 -->
- Rails学習中の方

# 目次
| 章 |    タイトル    |    備考    　|
|---:|-------------|------------|
| 1  |[ミニアプリの動作環境](#chapter1)|           
| 2  |[ビルドとはなんなのか](#chapter1)|          |
| 3  |[ビルドツールが動作するために必要なもの](#chapter1)|            |
| 4 |[アセットパイプラインとは](#chapter1)|            |
| 5 |[アセットパイプラインの各コマンド](#参考文献)　|            |
| 5 |[私が勘違いしていたこと](#参考文献)　|             |
| 5 |[開発環境と本番環境でのビルドとプリコンパイル](#参考文献)　|            |
| 1  |[Rails7で標準となったImport mapsとはなんなのか](#chapter1)|         |


<!-- 各チャプター -->
## ミニアプリの動作環境
バージョンは以下の通りです。
Rails 7.0.4
Dockerを使って開発しました。
また、CSSにはTailwindCSSを採用しています。
TailwindはRails7から採用されたImport maps経由ではなく、ESbuild経由でインストールしました。
以下はそのためのGemです。
gem 'cssbundling-rails'
gem 'jsbundling-rails'

## ビルドとはなんなのか
ESbuildやWebpackやBabelなどのビルドツールで行われる圧縮などの動作ことを指します。
主に以下の３つを担当します。
1. 複数ファイルのソースを1ファイルにまとめる-bundle(バンドル)
2. 空白などを取り除き圧縮する-minify
3. 新しい規格で書かれたコードを古い規格に変換する

https://blog.tyspine.com/why-build-javascript/#:~:text=%E7%8F%BE%E5%9C%A8%E3%81%AE%E3%83%A1%E3%82%B8%E3%83%A3%E3%83%BC%E3%81%AA%E3%83%95%E3%83%AD%E3%83%B3%E3%83%88,%E3%81%A8%E5%91%BC%E3%82%93%E3%81%A7%E3%81%84%E3%81%BE%E3%81%99%E3%80%82

ここで先ほど挙げた複数ファイルのソースを1ファイルにまとめるバンドルと後で説明するアセットパイプラインのプリコンパイルによるファイルの結合の違いを説明します。
バンドルは、モジュールや外部のライブラリなどのリンク処理を行って動的ファイルから静的ファイルにします。その一方でアセットパイプラインで行われるファイルの結合は異なったローカルファイルのJavaScriptファイル同士を結合して一つのファイルにするという違いがあります。

ビルドのイメージの参考に以下の記事がわかりやすかったです。

https://qiita.com/hedrall/items/2548718cfdf7bef3efc0

ESbuildをビルドツールとして指定した場合は元のファイルの置き場所（バンドル前のエントリーポイントとなるファイル）は、app/assets/javascriptsになります。
そしてビルド後のファイルの置き場所はapp/assets/buildsディレクトリになります。
Webpackを使う場合はapp/javascript/packsとなるようです。

自分のプロジェクトのファイルを確認したところ今回CSSのビルドツールとしてESbuildではなくTailwindが使われているようでした。app/assets/buildsディレクトリに空白などが取り除かれたファイルのapplication.cssがあることが確認できました。
package.jsonに以下の記述があったためだと考えられます。
app/assets/stylesheets/application.tailwind.cssという元のCSSのファイルをminifyしたものがapp/assets/builds/application.cssに配置されるように指定されています。
```
package.json
"scripts": {
    "build:css": "tailwindcss -i ./app/assets/stylesheets/application.tailwind.css -o ./app/assets/builds/application.css --minify",
    "build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=/assets"
  },
```

app/assets/builds/application.css
```
/*! tailwindcss v3.3.3 |solid #e5e7eb}:after,:before{--tw-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto S
```


## ビルドツールが動作するために必要なもの
ESbuildなどのビルドツールについて説明しましたが、これらのツールが動くために必要なものを２つ説明します。
1つ目はNode.jsで２つ目はjs-bundlingとcss-bundlingです。

### 1. Node.jsについて
まずNode.jsについて説明します。
Node.jsの仕組みは主に大きく2つに分けられます。

1. JavaScriptをブラウザ以外で実行するためのランタイム環境としての役割
ランタイム環境とはコンピュータプログラムが実行されるための実行環境のことを指します。
ESbuildはJavaScriptファイルの圧縮などを行ってくれるGo言語で記述されたツールです。
そのESbuildがビルドする対象となるJavaScriptはブラウザ環境上で動作するのですが、ESbuildなどのビルドツールがサーバー上でJavaScriptファイルを処理するためにNode.jsが必要となります。
ESbuild自体はGo言語で書かれているツールですが、その入力と出力はJavaScriptで行われるためJavaSciptの動作環境としてNode.jsが必要となります。


2. npmを利用してJavaScriptライブラリを簡単にプロジェクトに追加する役割
パッケージの依存管理ツールとしての役割です。JavaScriptのライブラリをプロジェクトに追加したり管理したりするのに使います。node_moduleディレクトリ上でパッケージが管理されます。

https://zenn.dev/mk_coding/articles/f0b1fb4423f56e



### 2. js-bundlingとcss-bundlingについて
これらはESbuildなどが動作するために必要なGemになります。
Railsアプリケーション内でESbuildを使う際に、設定やビルドの手順を簡単にするために必要になるらしいです。

https://qiita.com/kazutosato/items/1ae1cf0ec380a75d4dc4

## アセットパイプラインとは

ここまでESbuildなどのビルドについて説明してきましたがここからはアセットパイプラインについて説明していきたいと思います。

アセットパイプラインとはリクエストがあった際にいちいちブラウザが３つのファイルを要求していたとすると３回サーバーにリクエストを送らなけらばならなくなって面倒なためファイルをビルドしてそれらのローカルのファイルを結合することです。その役割を担うのがSprocketsになります。

https://qiita.com/junya__ya/items/6d768e9873eab9015e7d

https://qiita.com/ttaka66/items/991a52081a92cb6c2738

https://autovice.jp/articles/143


ファイルがSprocketsによって結合されるには、どう結合するかという設定が必要になります。
この設定は通常はapp/assets/config/manifest.jsファイルに書かれています。このファイルのことをマニュフェストファイルと呼びます。
このファイルは、アプリケーションで使用するアセットファイルをSprocketsがどのようにコンパイルするかを指定するためのものです。
以下で例のファイルを示します。
```
//= link_tree ../images
//= link_tree ../builds
```

これは```//= link_tree ../builds```なら
ビルドされた後のapp/assets/builds ディレクトリ内のすべてのファイルをアセットとして結合してブラウザへ送ることを意味します。

このマニュフェストファイルに書いた設定を参考にしてターミナルで```rake assets:precompile```とコマンドを打つことでプリコンパイルを行うことができます。
```
rake assets:precompile
```
結果としてpublic/assetsディレクトリに以下のように結合されたファイルが配置されます。

```
$ rake assets:precompile

🌼 daisyUI 3.5.1 https://daisyui.com
╰╮
 ╰─ 
.
.

Done in 325ms.
Done in 1.22s.

/sample-app/public/assets/manifest-xxxxxxxxxxxxxxxxxxxxxx.js
/sample-app/public/assets/manifest-oooooooooooooooooooooo.js.gz
.
.
```
実際アセットパイプラインによってプリコンパイルされたファイルの確認をしてみると以下のようになっていました。
```
Rails.application.assets_manifest.assets
=> 
{"manifest.js"=>"manifest-xxxxxxxxxxxxxxxxxxxxxxxx.js",
 "application.css"=>"application-oooooooooooooooooo.css",
 "turbo.js"=>"turbo-1111111111111111111111111.js",
 "turbo.min.js"=>"turbo.min-000000000000000000000000000.js"}
.
.

```


またなぜconfig/manifest.jsの```//= link_tree ../builds```でapp/assets/buildsディレクトリを読み込むのかということですが、読み込むファイルはconfig/initializers/assets.rb設定ファイルで設定することができます。
node_module以下のディレクトリをアセットパイプラインを利用して結合するようにしたい場合は以下のように設定ファイルに追記すると読み込んでくれます。

```
# config/initializers/assets.rb
Rails.application.config.assets.paths << Rails.root.join('node_modules')
```

## アセットパイプラインの各コマンド

以下でrails consoleからアセットパイプラインの設定を確認できるコマンドを紹介します。

### 1つ目
プリコンパイル対象として登録されているパスを確認できます。
```
Rails.application.config.assets.paths
```
```
pry(main)> Rails.application.config.assets.paths
=> ["sample_app/app/assets/builds",
 "/sample_app/app/assets/config",
 "/sample_app/app/assets/images",
 "/sample_app/app/assets/javascripts",
 "/sample_app/app/assets/stylesheets",
 "/usr/local/bundle/gems/cocoon-1.2.15/app/assets/javascripts",
 "/usr/local/bundle/gems/jquery-rails-4.5.0/vendor/assets/javascripts",
 "/usr/local/bundle/gems/font-awesome-rails-4.7.0.8/app/assets/fonts",
 "/usr/local/bundle/gems/font-awesome-rails-4.7.0.8/app/assets/stylesheets",
 "/usr/local/bundle/gems/actiontext-7.0.4/app/assets/javascripts",
 "/usr/local/bundle/gems/actiontext-7.0.4/app/assets/stylesheets",
 "/usr/local/bundle/gems/actioncable-7.0.4/app/assets/javascripts",
 "/usr/local/bundle/gems/activestorage-7.0.4/app/assets/javascripts",
 "/usr/local/bundle/gems/actionview-7.0.4/lib/assets/compiled",
 #<Pathname:ルートディレクトリ/node_modules>,# 設定でパスを追加することも可能です
 "/usr/local/bundle/gems/bootstrap-sass-3.4.1/assets/stylesheets",
 "/usr/local/bundle/gems/bootstrap-sass-3.4.1/assets/javascripts",
 "/usr/local/bundle/gems/bootstrap-sass-3.4.1/assets/fonts",
 "/usr/local/bundle/gems/bootstrap-sass-3.4.1/assets/images"]

```

### ２つ目
以下はプリコンパイルの対象となっているファイルです。

```
Rails.application.config.assets.precompile
```
```
Rails.application.config.assets.precompile
=> 
["manifest.js",
 "turbo.js",
 "turbo.min.js",
 "turbo.min.js.map",
 "actiontext.js",
 "trix.js",
 "trix.css",
 "es-module-shims.js",
 "es-module-shims.min.js",
 "es-module-shims.js.map",
 "stimulus.js",
 "stimulus.min.js",
 "stimulus.min.js.map",
 "activestorage",
 "activestorage.esm",
 "actioncable.js",
 "actioncable.esm.js"]

```
### 3つ目
以下はプリコンパイルされてどのファイルができたかということを示すコマンドです。
このファイルはブラウザのソースから確認できます。
```
Rails.application.assets_manifest.assets
```

```
Rails.application.assets_manifest.assets
=> 
{"manifest.js"=>"manifest-xxxxxxxxxxxxxxxxxxxxxxxx.js",
 "application.css"=>"application-oooooooooooooooooo.css",
 "turbo.js"=>"turbo-1111111111111111111111111.js",
 "turbo.min.js"=>"turbo.min-000000000000000000000000000.js"}
.
.
```


## 私が勘違いしていたこと
1. アセットパイプラインによるプリコンパイルではローカルのファイルの結合しか行っていないと思っていました。今回```rake assets:precompile```コマンドを実行したときにpackage.jsonにyarn buildが設定されていないことでエラーが起きたのでビルドもプリコンパイル時におそらく行っているのだと思います。しかしプリコンパイル時のビルドはESbuildで行われるのではなくSprocketsで行われるのではないかと思っていたので疑問に思っています。
package.jsonにyarn buildのスクリプトを追記して再度コマンドを実行したところエラーが解消されました。

2. 開発環境でもプリコンパイルされたファイルを参照しているのかと思っていましたがrake assets:precompileのコマンドを実行するか設定を変更しない限りはbin/devした後のビルドされた後のファイルをブラウザが参照していました。


## 開発環境と本番環境でのビルドとプリコンパイル


ビルドやアセットファイルの結合を促すコマンドがどこに設定されているか気になったので調べてみました。
開発環境と本番環境について私が作ったプロジェクトでどのようにビルドやアセットパイプラインが設定されているかを確認していきたいと思います。

### 1. 本番環境

まずデプロイ後に実行されるファイルであるentorypoint.shファイルにプリコンパイルのコマンドを設定しました。
```
entorypoint.sh
if [ "$RAILS_ENV" = "production" ]; then
bundle exec rails assets:clobber
bundle exec rails assets:precompile

```
```bundle exec rails assets:precompile```はconfig/manifest.rbに従ってアセットを結合するコマンドです。

プリコンパイルのコマンドは他にも以下の方法で実行することができます。
1. ターミナル上で手動でrake assets:precompileを実行する
2. config/environments/production.rbでリクエスト時にコンパイルされるようにtrueに設定する。
```
# config/environments/production.rb
config.assets.compile = true
```
https://qiita.com/masarumosu/items/45c869a10cbf89d18075


また本番環境でプリコンパイルされたファイルはホスト名/assets以下のパスを入力するとWebで検索した際にプリコンパイルされたファイルが表示されます。そのときpublic 配下のファイルを公開する設定をしていないとエラーになります。

https://blog.aiandrox.com/posts/tech/2020/03/11/

### 開発環境

私は開発環境ではファイルに変更があるたびにbin/devしてビルドを行いそれをブラウザに反映させていました。
bin/devコマンドの元を辿っていきたいと思います。

```
#bin/dev
.
.
exec foreman start -f Procfile.dev "$@"
```
```
# Procfile.dev
bin/rails server
css: yarn build:css --watch
```
```
# package.json
"scripts": {
    "build:css": "tailwindcss -i ./app/assets/stylesheets/application.tailwind.css -o ./app/assets/builds/application.css --minify",
    "build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=/assets"
.
```
ビルドはbin/devで行われ、アセットパイプラインによるプリコンパイルはbin/devでは行われないみたいです。開発環境では、おそらくapp/assets/buildsディレクトリのビルド後のファイルが反映されているのだと思います。
application.html.erbでは以下のようになっており、app/assets/builds/application.cssを読み込んでいることがわかります。
```
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
```

ちなみに本番環境で```rake assets:precompile```コマンドを実行しアセットパイプラインでプリコンパイルした後はローカルのファイルの変更があっても、public/assetsディレクトリの変更を優先するのでローカル環境でbin/devしてできたビルド後のファイルを反映させたい時は以下コマンドを実行したら反映されるようになります。

```
rails assets:clobber
```
https://qiita.com/GandT/items/4b3701a6ca51ff84370c


# Rails7で標準となったImport mapsとはなんなのか
Rails7では、標準ではWebpackといったビルドツールを使わず、代わりにImport mapsを使って処理を行っています。
importmap-railsを使うことで、Node.jsを使ったJavaScriptファイルのビルドが不要となり、Node.jsのインストールもデフォルトでは行わないようになっているらしいです。

今後勉強していきたいと思います。

[Rails 7 : rails newのフロントエンド関連オプションの組み合わせを調べてみた](https://techracho.bpsinc.jp/hachi8833/2023_04_18/114730)
[Rails 7.0 で標準になった importmap-rails とは何なのか？](https://zenn.dev/takeyuwebinc/articles/996adfac0d58fb)
[Rails7のnewコマンドのオプションが多すぎて分からなくなった時のために（フロントエンド多め）](https://qiita.com/kyntk/items/0936598a903ac74e607d)
[Rails 7: importmap-rails + tailwindcss-railsでnode.jsが不要な理由
](https://techracho.bpsinc.jp/hachi8833/2022_02_17/115435)[Rails7 のフロントエンド関連の Gem を分類](https://qiita.com/kwtuku/items/3639e8f268f6f9a442b1)