[Rails]あなたのwebpacker、minify効いてますか?(webpackerでTypeScriptを使った時にハマった話)

こんにちは、システムエンジニアの kouki です

WESEEK では Rails と webpacker を使ったプロジェクトを多数担当しています。今回はその中で起こったトラブルと対処法についてご紹介します

最近では

と assets 関連が盛り上がっていますが、まだ webpacker が現役なため、今回は webpacker にフォーカスを当てています

(esbuild のビルドが速いと聞いて興味は沸いているので機会があればまた別の記事にて)

おさらい: webpacker で minify を効かせる方法

Rails + webpacker を利用しているアプリにて production で assets (JS, CSS etc) を提供する際には下記のようなコマンドを実行します

$ RAILS_ENV=production bin/rails assets:precompile

これは webpacker が assets:precompile rake タスクに特定のタスクを挟み込むことで production build を実現しています

また、RAILS_ENV=production を指定するだけで、よしなに minify を働かせてくれるのは webpacker が下記のようなコードを提供してくれているからです

利用しているプロジェクトの環境 (ハマった環境)

では、今回トラブルに遭遇した環境を紹介します

  • Ruby on Rails
    • Rails '~> 6.0.3', '>= 6.0.3.2'
    • webpacker ~5.4
  • webpacker (もとい JavaScript 側で利用しているライブラリ)
    • React
    • TypeScript
      • (ポイント) devDependenciestypescript@types/* を追加している

production build をしてみたが ... エラー発生

ここから遭遇したエラーを紹介します

「さて、assets:precompile を production でビルドするぞ」と意気込んで、下記コマンドを実行しました (実際は Dockerfile 内に記載しています)

$ RAILS_ENV=production NODE_ENV=production bundle exec rake assets:precompile

そうすると下記のようなエラーになってしまいました

TS2580: Cannot find name 'require'. Do you need to install type definitions for node? Try `npm i @types/node`.
(略)
TS7016: Could not find a declaration file for module ...

@typesdevDependencies に追加済みのはずなのにおかしいですね

調査フェーズ

上記エラーに対処するために色々と調査をしたところ、下記のような状況になっていることが分かりました

  1. assets:precompile 実行後、node_modules 配下を見ると @types npm パッケージが追加されていない
  2. それ以外にも devDependencies に記載しているパッケージがインストールされていない
  3. どうやら NODE_ENV=production の指定が入っていることにより、dependencies のみ npm パッケージが install されている

次にそれぞれ (rails, webpacker) のコードを見て挙動を確認していきました

それ以外にも GitHub の issue で下記のようなコメントを見つけました

https://github.com/rails/webpacker/issues/117#issuecomment-282798638

Yes let's fix the root issue and move away from errant use of devDependencies.

dhh (28 Feb 2017)

2017 時点でのコメントなので現在では意見が異なると思いますが、「devDependencies の誤った利用 (errant use of devDependencies.)」 と言われてしまうと困ってしまいますね

解決案候補 & 採用した解決方法

ある程度情報が揃ったので解決案の候補を出していきます

ここでのやりたいことは 「yarn install 時には devDependencies のパッケージもインストールして、ビルド時にはNODE_ENV=production でビルドできること」 ということにしています

ボツ案も含め、一旦はザッと思い浮かんだものをリストアップし、最終的に 5. の案にすることにしました

それぞれの案の却下理由は下記の通りです

  1. devDependencies に記載しているパッケージを dependencies に移す
    • 先述した GitHub issue にて提示されている方法
    • フロントエンド側からすると直感的ではない
      • 各 npm パッケージのサイトでも devDependencies に追加する例示もあって、プロジェクトメンバーにそれを意識させたくない
  2. NODE_ENV=developmentassets:precompile を実行
    • minify が効かない
    • minify を効かせることもできるが手間がかかるし、トリッキー
  3. assets:precompile タスクを実行せず、rake タスクを分割して実行
    • こちらもトリッキーすぎる
    • 「このプロジェクトでは assets:precompile は実行しないようにしてください」という注意書きにしたくない
  4. webpacker をアップデートしてみて、assets:precompile を実行してみる
    • integrity チェックが無くなった、という話もあるのでこれは有効な案
    • 調査に時間がかかりそうだったため、今回は見送り
  5. [採用] yarn:install task を上書きする

最終的に案5を採用することにしました

自然な形で導入できる、かつ上書きをするタスクが局所的である、という点が採用ポイントです

下記のような rake タスクをプロジェクトの lib/tasks/yarn.rake というファイルパスで追加しました

# assets:precompile 時に yarn install を NODE_ENV=development で実行させるモンキーパッチ
# 
# refs: https://github.com/rails/webpacker/issues/405#issuecomment-750384173
if ENV['FORCE_YARN_INSTALL_IN_DEVELOPMENT']
  Rails.application.config.after_initialize do
    Rake::Task['yarn:install'].clear

    # rails/rails v6.0.4.1 から引用
    # https://github.com/rails/rails/blob/v6.0.4.1/railties/lib/rails/tasks/yarn.rake
    namespace :yarn do
      desc "Install all JavaScript dependencies as specified via Yarn"
      task :install do
        # 強制的に NODE_ENV=development で yarn install を実行させる
        node_env = 'development'
        system({ "NODE_ENV" => node_env }, "#{Rails.root}/bin/yarn install --no-progress --frozen-lockfile")
      end
    end

    # Run Yarn prior to Sprockets assets precompilation, so dependencies are available for use.
    if Rake::Task.task_defined?("assets:precompile")
      Rake::Task["assets:precompile"].enhance [ "yarn:install" ]
    end
  end
end

そして実行するコマンドは下記のようになりました

$ FORCE_YARN_INSTALL_IN_DEVELOPMENT=true NODE_ENV=production bundle exec rake assets:precompile

これであれば他の人が見たときに「FORCE_YARN_INSTALL_IN_DEVELOPMENTって何?」ってなった時にコードを検索することで辿ることができます

オプションを指定しない場合は先述したとおりエラーになるのですが、それは標準的な動きからあまり挙動を変えたくなかったためです

まとめ

今回は「webpacker で TypeScript を使った時にハマった話」からの各種調査についてご紹介させていただきました

「あなたの webpacker、minify 効いてますか?」という文言はただ単に「皆さん、deploy されている環境で assets が minify されているか確認してますよね?」というお話でした

こういった記事でも参考になる方がいらっしゃれば幸いです