【Ruby on Rails, Sprockets】app/assets/とmanifest.jsを複数配置してアセットパイプラインを走らせたい!🏃‍♂️💨

こんにちは、akitoshigaです。

先日、アセットファイルを`app/assets/**`以外の場所に配置した上で`manifest.js`を利用して複数の場所からアセットパイプラインを走らせる必要が生じたのですが、類似の事例が見つからず自身が実践した方法を共有します。

## 1. 前提：アセットパイプラインとは
アセットパイプラインとは、アセットファイル（画像・CSS・JavaScriptなど）を効率的に配信するためのフレームワークでありRuby on Railsに組み込まれています。

複数のCSSファイルを圧縮して一つにまとめたり、フィンガープリントを利用したキャッシングの管理をしてくれます。

アセットパイプラインは内部で`sprockets`や`importmap-rails`というgemを利用しています。

詳しい説明はRailsガイドをご覧ください。

https://railsguides.jp/asset_pipeline.html

## 2. やりたいこと
下記のように、Ruby on Rails指定のアセットファイルの置き場である`app/assets/`配下に加えて`packs/new_module/app/assets/`配下にアセットファイルを配置します。
その上で`packs/new_module/app/assets/config/manifest.js`を読み込んでビューのレイアウトからアセットファイルの読み込みを行おうとしていました。

※ 以降一部のディレクトリ名やファイル名は実際のものから変更しています。

```
project_root
└── app Rails指定
│   └── assets
│       ├── config
│       │   └── manifest.js
│       ├── images
│       ├── javascripts
│       └── stylesheets
└── packs ⭐追加
    └── new_module
        └── app
            └──assets
               ├── config
               │   └── manifest.js
               ├── images
               ├── javascripts
               └── stylesheets
```

## 3. 何故やりたいのか
モジュラーモノリスなRailsアプリケーションを構築するために、`packs-rails`を用いてビューファイルやそこで使うアセットファイルを一部別のディレクトリに切り出したかったためです。

`packs-rails`を簡単に説明すると、ドキュメントルートの`app/`と同じ階層に`packs/`というディレクトリを作り、その配下で複数のパッケージ構成を可能にするgemです。

https://github.com/rubyatscale/packs-rails

`packs-rails`指定のディレクトリ構成に従う代わりに面倒なオートロードの設定を省略してくれます。
しかし、利用する過程で`assets`は対象外だったことが発覚したため今回自力での対応に至りました。

## 4. 最初に行ったこと
### 4.1. アセットファイルの追加
1. 上記のディレクトリにアセットファイルを追加します。
2. 追加したアセットファイルを読み込むための`manifest.js`を`packs/new_module/assets/config/`配下に追加します。
``` packs/new_module/apps/assets/config/manifest.js
//= link_tree ../images
//= link_tree ../stylesheets .css
//= link_tree ../javascripts .js
```
### 4.2. 追加したファイルをアセットパイプラインに追加
追加したファイルをアセットファイルパイプラインの対象とするために`assets.rb`に定義を追加します。
今回のケースでは`packs-rails`を利用しているため`packs/new_module/config/initializers/assets.rb`を追加してそこに`new_module`で使用するアセットファイルの定義を追加しました。
``` packs/new_module/config/initializers/assets.rb
Rails.application.config.assets.paths << Rails.root.join('packs','new_module','app','assets','config')
Rails.application.config.assets.paths << Rails.root.join('packs','new_module','app','assets','stylesheets')
Rails.application.config.assets.paths << Rails.root.join('packs','new_module','app','assets','javascripts')
Rails.application.config.assets.paths << Rails.root.join('packs','new_module','app','assets','images')

```

## 5. しかし、反映されない
Railsを再起動して、アセットファイルを使用しているビューにアクセスしてみると`Sprockets::Rails::Helper::AssetNotPrecompiledError`というエラーが発生します。
![スクリーンショット 2024-03-07 15.22.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546472/b92599e9-845a-e559-a16e-ef1454d29ac6.png)
新しく配置したアセットファイルがプリコンパイルされず読み込むことができないためにエラーが送出されているようです。

## 6. 検証してみる　

### 6.1 パスの確認
`assets.rb`で指定したパスが正しく読み込まれているか確認します。
Railsコンソールを開いて以下の値を確認します。
```rb
Rails.application.config.assets.paths
```
そうすると以下のような形で読み込み対象のパスの一覧が出力されるようになります。

```rb
=> ["/path/to/document/app/assets/config",
 "/path/to/document/app/assets/images",
 "/path/to/document/app/assets/javascripts",
 "/path/to/document/app/assets/others",
 "/path/to/document/app/assets/stylesheets",
 "/path/to/document/vendor/assets/javascripts",
 "/path/to/document/vendor/assets/stylesheets",
 #<Pathname:/path/to/project_root/packs/new_module/app/assets/config>,
 #<Pathname:/path/to/project_root/packs/new_module/app/assets/stylesheets>,
 #<Pathname:/path/to/project_root/packs/new_module/app/assets/javascripts>,
 #<Pathname:/path/to/project_root/packs/new_module/app/assets/images>]
```

出力されたパスを環境の`ls`コマンドの引数にわたすと意図したファイルが出力されるのでパスはあっていそうです。

### 6.2 プリコンパイルしてみる
rakeタスク`assets:precompile`を実行してアセットファイルを出力してみます。
```shell
$ bundle exec rails assets:precompile
```
このタスクを実行すると開発環境でも`public/assets/`にアセットファイルがプリコンパイルされてひとまとめに出力されます。
その中を確認するとやはり新しく配置した静的アセットファイルが入っていませんでした。

上記のことからパスは合っている、呼び出し先が間違っているのではなくプリコンパイルがされていないことが確認できました。

## 7. 解決方法
試行錯誤の結果、以下の解決方法にたどりつきました。

### 7.1. manifest.jsのファイル名を変更
新しく追加した方の`manifest.js`のファイル名を変更します。
自分の場合は下記のようにモジュール名をプレフィックスにしました。
`new_module_manifest.js`

### 7.2 プリコンパイルの対象に追加する
`new_module_manifest.js`がプリコンパイルの対象となるように新しい方の`assets.rb`に定義を追加します。

``` packs/new_module/config/initializers/assets.rb
Rails.application.config.assets.paths << Rails.root.join('packs','new_module','app','assets','config')
Rails.application.config.assets.paths << Rails.root.join('packs','new_module','app','assets','stylesheets')
Rails.application.config.assets.paths << Rails.root.join('packs','new_module','app','assets','javascripts')
Rails.application.config.assets.paths << Rails.root.join('packs','new_module','app','assets','images')

Rails.application.config.assets.precompile += %w[new_module_manifest.js] # ⭐追加
```
これらを実施することで無事アセットファイルを読み込めるようになりました!

sproketsは`app/assets/config/`配下の`manifest.js`以外で、トップレベルターゲットを定義するファイルを用意する場合は、都度対象となるファイルを定義しなければいけないようです。（ドキュメント等確認しましたが明確に記載されておらず、、、どなたか情報提供お待ちしています。）

## 8. まとめ
おわって見れば単純な問題でしたが、解決に結構かかりました。

現在Ruby on Railsアプリケーションのモジュラーモノリス化は注目されていると思うので、同じような試みをされている方の一助になれば幸いです🙏