36
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Webpacker から importmap-rails に移行した

Last updated at Posted at 2022-02-06

謝辞

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 を追加します。

Gemfile
- 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 ディレクトリの直下には置かないことをオススメします。
  • CSS
    • from: app/packs/stylesheets
    • to: app/assets/stylesheets
  • 画像
    • from: app/packs/images
    • to: app/assets/images

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

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行が追加されます。

config/importmap.rb
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 配下に配置した場合、以下の記述で読み込めます。

config/importmap.rb
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 において requireimport に、 globalwindow に置き換えます。

    before
    global.$ = require('jquery')
    
    after
    import $ 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 を削除します。

package.json
-  "@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日追記: auditoutdated コマンドが 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 が要求しているバージョンが古いために、バージョンアップができずに、脆弱性が残り続けていた。

移行にはそれなりに手間がかかりますが、最新の技術に取り残されないためにも、ぜひ移行しましょう!

36
21
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
36
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?