Rails 7.1アップデート後にautoloadのエラーで起動しなくなった

Rails 7からautoloadの仕組みがzeitwerkに完全移行したそうだ。

zeitwerkはファイル名から定数名を推測するという仕組みになっているらしい。例えば、自動ロードパスに置かれているhoge_fuga/piyo.rb というファイルからはHogeFuga::Piyo という名前の定数(モジュール・クラス)を読み込もうとする。もしHogeFuga::Piyo が存在しないと以下のようなエラーになる。

$ bundle exec rails zeitwerk:check
Hold on, I am eager loading the application.
bin/rails aborted!
NameError: uninitialized constant HogeFuga::Piyo

ここで注意すべきは HogeFuga::PIYO という定数があってもダメということだ。ActiveSupportのcamelizeでファイル名を定数名に変換しているため、全部大文字の定数を受け付けない。この場合、autoloaderの設定に単語を登録すると動作するようになる。詳細はRailsドキュメントを参照していただきたい

なんで7.0で問題なかったの?

ここで私が疑問に思ったのは、このエラーがRails 7.0では発生しなかったのに7.1アップデートで発生したこと。7.0の時点でzeitwerkが有効になっているにも関わらず、である

これは以下のような理由だった

  • エラーが発生するファイルは lib/hoge_fuga/piyo.rb
  • Rails 7.0まではlibディレクトリは自動ロードパスではない
    • このため自動ロードではなくrequireで普通にロードしていた
  • Rails 7.1でlibが自動ロードパスに含まれるようになった
    • lib/hoge_fuga/piyo.rb が自動ロードの対象となった
    • HogeFuga::Piyoが定義されていないためエラーが発生した

Rails 7.1でconfig/application.rbに config.autoload_lib(ignore: %w(assets tasks))というコードが追加されており、これがlibを自動ロードパスに追加する設定である。rails app:updateでコンフリクトしたときにこの設定を外してマージしなければならなかったのだ

というわけで config.autoload_lib(ignore: %w(assets tasks))コメントアウトするのが1番簡単な解決法なのだが、せっかくなので全体をzeitwerkの流儀に合わせることにした

まず、config/initializers/zeitwerk.rbを作成し、以下のコードを追加

Rails.autoloaders.each do |autoloader|
  autoloader.inflector.inflect(
    'fuga' => 'FUGA'
  )
end

requireしているコード(今回はlib/initializers/hoge.rb)を直す

- require 'hoge_fuga/piyo'
- HogeFuga::PIYO.init(config: Rails.root.join('piyo_secret.json'))
+ Rails.configuration.after_initialize do
+   HogeFuga::PIYO.init(config: Rails.root.join('piyo_secret.json'))
+ end

zeitwerkの流儀に従うためrequireを取り除く。ただ、initializerが実行されている時点では自動ロードが完了していないようなのでafter_initializeの中で定数にアクセスしなければならない