謝辞
2022-10-30 追記: 自分がいつも読んでいる TechRacho さんの週刊Railsウォッチで取り上げていただきました!
(記事が公開された 2022-04-25 の時点で把握していたのですが、ここにリンクを貼るのを今まで忘れていました。。。)
はじめに
きっかけ
Rails アプリで使用していた Webpacker の開発が終了し、importmap-rails がデフォルトになったため。
Rails7 のリリース後、今後 importmap-rails がデフォルトになることが発表され、その後、Webpacker の開発終了が発表されました。また、プリコンパイルが必要な環境向けの選択肢として jsbundling-rails などが公開されました。
Rails チームは Webpacker の開発を終了しましたが、後継のプロジェクトとして Shakapackerが公開されており、開発中だった Webpacker v6 の機能を引き継いでいます。
自分の個人的なプロジェクトでは Webpacker v6 への移行準備をしていて、後は正式リリースを待つだけ、という状態だったので、開発終了はちょっと残念でした。。。
Shakapacker へ移行する道もあるのですが、なるべくデフォルトのツールを使っていきたいのと、フロントエンドで TypeScript などビルドが必要なライブラリを使用していないので、 importmap-rails に移行することにしました。
移行はそこそこ大変だったので、自分の備忘録も兼ねて移行手順をまとめました。
最終的には Webpacker -> importmap-rails + Propshaft + cssbundling-rails という構成になりました。
Webpack(er) からの脱却
もともと、自分のプロジェクトでは Asset Pipeline を使用せず、Webpacker で CSS (SCSS)と画像などのアセットも処理していました。
importmap-rails では CSS と画像の処理をしてくれないので、 Asset Pipeline を復活させることにしました。以前は Sprockets がその役目を担っていましたが、新しく Propshaft が登場したため、こちらを使います。
また、 SCSS を使用しているので、コンパイルのために cssbundling-rails を導入します。
移行手順
前提
この移行手順では WSL2 を使用しています。
また Rails アプリは、ざっくり以下のような構成です。
- Ruby 3.1.0
- Rails 7.0.1
- Webpacker 5.4.3
- Sprockets は未使用
- JavaScript は少なめ (Vanilla JS)
- SCSS を使用している
- SCSS, 画像(favicon 含む)も Webpacker で処理している
ライブラリの導入
Gemfile から webpacker を削除し、 importmap-rails, cssbundling-rails, propshaft を追加します。
- gem 'webpacker', '~> 5.4'
+ gem 'cssbundling-rails'
+ gem 'importmap-rails'
+ gem 'propshaft'
bundle install
を実行した後、 bin/rails importmap:install
を実行します。
するといくつかのファイルが追加/変更されます。
$ bin/rails importmap:install
Add Importmap include tags in application layout
insert app/views/layouts/application.html.erb
Create application.js module as entrypoint
create app/javascript/application.js
Use vendor/javascript for downloaded pins
create vendor/javascript
create vendor/javascript/.keep
Configure importmap paths in config/importmap.rb
create config/importmap.rb
Copying binstub
create bin/importmap
また、 bin/rails css:install:sass
も実行します。
こちらでもいくつかのファイルが追加/変更されます。
$ bin/rails css:install:sass
Build into app/assets/builds
create app/assets/builds
create app/assets/builds/.keep
append .gitignore
File unchanged! The supplied flag value not found! .gitignore
Remove app/assets/stylesheets/application.css so build output can take over
remove app/assets/stylesheets/application.css
Add stylesheet link tag in application layout
insert app/views/layouts/application.html.erb
Add default Procfile.dev
create Procfile.dev
Ensure foreman is installed
run gem install foreman from "."
Successfully installed foreman-0.87.2
1 gem installed
Add bin/dev to start foreman
create bin/dev
Install Sass
create app/assets/stylesheets/application.sass.scss
run yarn add sass from "."
yarn add v1.22.17
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
warning " > webpack-dev-server@3.11.3" has unmet peer dependency "webpack@^4.0.0 || ^5.0.0".
warning "webpack-dev-server > webpack-dev-middleware@3.7.3" has unmet peer dependency "webpack@^4.0.0 || ^5.0.0".
[4/4] Building fresh packages...
success Saved lockfile.
success Saved 3 new dependencies.
info Direct dependencies
└─ sass@1.49.7
info All dependencies
├─ immutable@4.0.0
├─ sass@1.49.7
└─ source-map-js@1.0.2
Done in 4.95s.
Add build:css script
run npm set-script build:css "sass ./app/assets/stylesheets/application.sass.scss ./app/assets/builds/application.css --no-source-map --load-path=node_modules" from "."
run yarn build:css from "."
yarn run v1.22.17
$ sass ./app/assets/stylesheets/application.sass.scss ./app/assets/builds/application.css --no-source-map --load-path=node_modules
Done in 0.23s.
ちなみに、css のインストールコマンドは他にも用意されており、現時点で以下のコマンドが使用できます。
./bin/rails css:install:[tailwind|bootstrap|bulma|postcss|sass]
ファイルの変更
インストールコマンドで追加/変更されたファイルに手を入れていきます。
app/views/layouts/application.html.erb
コマンド実行後は以下のように2行追加されています。
<%= javascript_packs_with_chunks_tag 'application', 'data-turbo-track': 'reload' %>
<%= stylesheet_packs_with_chunks_tag 'application', media: 'all', 'data-turbo-track': 'reload' %>
<%= favicon_pack_tag 'favicon.ico' %>
+ <%= javascript_importmap_tags %>
+ <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
javascript_packs_with_chunks_tag
, stylesheet_packs_with_chunks_tag
はそれぞれ、追加された javascript_importmap_tags
, stylesheet_link_tag
に置き換えます。
また、 Webpacker が提供していた favicon_pack_tag
はもう使用できないため、favicon_link_tag
に置き換えます。
変更後:
<%= javascript_importmap_tags %>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbo-track': 'reload' %>
<%= favicon_link_tag 'favicon.ico' %>
アセットファイルの移動
Webpacker のときとファイルの置き場所が変わっているので移動しておきます。
- JavaScript
- from:
app/packs/src
- to:
app/javascript/controllers
controllers というディレクトリ名である必要は無いですが、 javascript ディレクトリの直下には置かないことをオススメします。
- from:
- CSS
- from:
app/packs/stylesheets
- to:
app/assets/stylesheets
- from:
- 画像
- from:
app/packs/images
- to:
app/assets/images
- from:
app/javascript/application.js
Webpacker では app/packs/entrypoints/application.js
でしたが、この内容を app/javascript/application.js
に移動します。
その際、以下の点に対応する必要があります。
SCSS と画像読み込みのコードを削除
これらは今後 Propshaft によって Asset Pipeline で処理されるので、下記のコードは削除します。
import './application.scss'
require.context('../images', true)
JavaScript のパス変更に対応
自分で書いた JavaScript のパスを変更しているので、それに合わせて import も変更します。
-
変更前:
import '../src/module1'
-
変更後:
import 'controllers/module1'
app/assets/stylesheets/applications.sass.scss
Webpacker では app/packs/entrypoints/application.scss
でしたが、この内容を app/assets/stylesheets/applications.sass.scss
に移動します。
その際、以下の点に対応する必要があります。
package.json で管理している外部ライブラリの CSS を使用している場合
Webpacker のときは @import '~bootstrap/scss/bootstrap';
のようにライブラリ名の前に ~
(チルダ) を付けることで node_modules
ディレクトリの下から必要なファイルをインポートしていました。
今後 sass
ライブラリがコンパイルを行えるようにするには、 ~
を削除すればOKです。
-
変更前:
@import '~bootstrap/scss/bootstrap';
-
変更後:
@import 'bootstrap/scss/bootstrap';
CSS ファイルのパス変更に対応
CSS ファイルのパスを変更しているので、それに合わせて import も変更します。
- 変更前:
@import '../stylesheets/style1';
- 変更後:
@import './style1';
SCSS の @import
の廃止について
今後、@import
は廃止されることが発表されています。
However, doing away with
@import
entirely is the ultimate goal for simplicity, performance, and CSS compatibility. As such, we plan to gradually turn down support for@import
on the following timeline:
将来的に @use
や @forward
に置き換わるのですが、単純に置き換えができないので、移行には時間がかかりそうです。この移行例でも @import
のままにしています。
2022-10-30 追記: 当初は 2022-10-01 までには @import
のサポートを終了する方針だったそうですが、延長されたようです。(ユーザーの80%が Dart Sass に移行するまでは待つ、と言っています)
July 2022: In light of the fact that LibSass was deprecated before ever adding support for the new module system, the timeline for deprecating and removing
@import
has been pushed back. We now intend to wait until 80% of users are using Dart Sass (measured by npm downloads) before deprecating@import
, and wait at least a year after that and likely more before removing it entirely.
いずれにしても、最終的にはサポートが終了するということは変わらないので、早めに移行しておいた方が良さそうです。
config/importmap.rb
# Pin npm packages by running ./bin/importmap
pin "application", preload: true
以前は JavaScript のライブラリは package.json で管理していましたが、今後はこのファイルで管理します。登録には bin/importmap pin
コマンドを使います。
例えば bootstrap を追加するとこのようになります。
$ bin/importmap pin bootstrap
Pinning "bootstrap" to https://ga.jspm.io/npm:bootstrap@5.1.3/dist/js/bootstrap.esm.js
Pinning "@popperjs/core" to https://ga.jspm.io/npm:@popperjs/core@2.11.2/lib/index.js
config/importmap.rb
には以下の2行が追加されます。
pin "bootstrap", to: "https://ga.jspm.io/npm:bootstrap@5.1.3/dist/js/bootstrap.esm.js"
pin "@popperjs/core", to: "https://ga.jspm.io/npm:@popperjs/core@2.11.2/lib/index.js"
bootstrap は @popperjs/core に依存していますが、importmap はこの依存関係も解決して必要なライブラリを登録してくれます。
同様の手順で、必要なライブラリを全て登録していきます。
ちなみに pin コマンドの引数には bin/importmap pin library1 library2
のように複数のライブラリを渡せます。
外部ライブラリ以外の、自分で書いた JavaScript の読み込みには pin_all_from
が使えます。
app/javascript/controllers
配下に配置した場合、以下の記述で読み込めます。
pin_all_from "app/javascript/controllers", under: "controllers"
注意
Ruby の仕様上、文字列を括る文字はシングルクォーテーションでもダブルクオーテーションでも良いのですが、 importmap-rails v1.0.2 時点では、importmap.rb
の中で文字列をシングルクオーテーションで括った場合には pin
コマンドと unpin
コマンドが正常に動作しません。(unpin
コマンドは importmap.rb
からライブラリを削除してくれます)
そのうち修正が入るかもしれませんが、それまでは importmap.rb
ではダブルクオーテーションを使用しましょう。
その他
-
画像を表示するために
image_pack_tag
メソッドを使っていた場合、image_tag
メソッドに置き換えます。 -
jQuery を使っている場合、
application.js
においてrequire
はimport
に、global
はwindow
に置き換えます。beforeglobal.$ = require('jquery')
afterimport $ from 'jquery' window.$ = $
-
i18n-js の移行は少し手順が必要だったので、別記事を書きました。 -> i18n-js を importmap-rails で使う
動作確認
以下のコマンドで動作確認します。
bin/dev
このコマンドによって、 Rails の起動と sass のビルドが行われます。
これは foreman というアプリが Procfile.dev
の記述を実行することで実現されています。
どんなコマンドが実行されるかは Profile.dev
を見るとわかります。
自分は Chrome の開発者ツールを見ながら画面操作をして、エラーが発生しないか確認しました。
一連の操作をして問題ないことを確認できたら、importmap-rails への移行がほぼ完了です。
あとは Webpacker の掃除をしましょう。
後片付け
Webpacker を削除
package.json から Webpacker を削除します。
- "@rails/webpacker": "5.4.3",
- "webpack-dev-server": "^3.11.3"
以下の関連ファイルも削除します。
bin/webpack
bin/webpack-dev-server
config/webpack/development.js
config/webpack/environment.js
config/webpack/production.js
config/webpack/test.js
config/webpacker.yml
.browserslistrc
babel.config.js
postcss.config.js
package.json の掃除
importmap.rb
に移動したライブラリを package.json
から削除します。
ただし、 bootstrap などのライブラリを使用している場合、JavaScript は CDN から取得しますが、 CSS などスタイルシートについては node_modules
から読み込む必要があるので、残しておく必要があります。
掃除終了後に、正常に動作するか確認しておきましょう。
ここまでで importmap-rails への移行は終了です! 🎉
おまけ
Dependabot が非対応
GitHub 上のプロジェクトでは、ライブラリの自動更新を行ってくれる Dependabot を簡単に利用できます。
今回、JavaScript ライブラリの依存関係の記述を package.json から importmap.rb に移動しましたが、 2022年2月6日現在、 Dependabot は importmap.rb に対応していません。
これについては、以下の importmap-rails の issue でも言及されています。
セキュリティのためにも、なんとかして Dependabot の自動更新を使いたいので、次のような方法で対応しました。
-
Dependabot の自動更新を受ける専用のブランチを用意する。(ここでは yarn-dependabot ブランチにした)
-
importmap.rb で管理しているものと同じライブラリを package.json でも管理する。
-
初期状態では Dependabot はデフォルトのブランチにのみ PR を出すので、設定ファイルに追記して、専用ブランチにPRを出してもらうように変更する。
.github/dependabot.yml- package-ecosystem: npm directory: "/" schedule: interval: daily time: "20:00" target-branch: yarn-dependabot
(target-branch
オプションの詳細については以下の ドキュメント を参照してください。)
2022年5月24日追記: audit
と outdated
コマンドが v1.1.0 で追加されました。(実際の対応はこのPR↓)
./bin/importmap outdated
を実行することで、ライブラリの最新版があるか確認できます。
以下は出力例(上記PRより引用)
+-----------------+---------------+---------------+
| Package | Current | Latest |
+-----------------+---------------+---------------+
| @jspm/core | 2.0.0-beta.18 | 2.0.0-beta.19 |
| @jspm/core | 2.0.0-beta.2 | 2.0.0-beta.19 |
| aaaasssstimulus | 2.0.0 | Not found |
| glob-parent | 3.1.0 | 6.0.2 |
| is-glob | 3.1.0 | 4.0.3 |
| is-svg | 3.0.0 | 4.3.2 |
| lodash | 4.17.1 | 4.17.21 |
| nth-check | 1.0.0 | 2.0.1 |
| react | 16.0.0 | 17.0.2 |
| stimulus | 2.0.0 | 3.0.1 |
+-----------------+---------------+---------------+
10 outdated packages found
ただし、まだ Dependabot の方では未対応なので、手動で importmap outdated
を実行して確認する、ということができるようになったという段階です。
importmap outdated
を定期実行 -> 出力をパースして結果を通知するという動きを自動化することができれば楽になりますね。(Dependabot が対応してくれれば一番ですが)
終わりに
Webpacker が無くなったことで、Hot Module Replacement は使えなくなりましたが、
その代わりに以下の恩恵を受けられました。
- JavaScript のビルドが無いので速い
- node_modules ディレクトリが肥大化しなくなった
- Webpacker によって出ていた Dependabot alerts が消えた
- セキュリティ上の問題があるバージョンのライブラリを使用していても、 Webpacker が要求しているバージョンが古いために、バージョンアップができずに、脆弱性が残り続けていた。
移行にはそれなりに手間がかかりますが、最新の技術に取り残されないためにも、ぜひ移行しましょう!