アップグレードガイド Rails 7.1.3 (jsbundling-rails with Webpack)

## はじめに
projectでRails6系からRails7.1.3へのアップグレードを対応することになり、ついでにwebpackerを剥がして、jsbundling-rails with Webpackに切り替えた際の備忘録を残します。

### 想定読者
- rails6系からrails7系にアップグレードする際、脱webpackerしたいけどwebpacker利用時の恩恵は残したいなと調べている人

## Rails6系からRails7系へのアップグレード
### コードに修正が必要になった変更点（rails-6.1.6 switch to rails-7.1.3）
#### enumの新構文が強制
- コード例
    ```ruby
    # before
    enum hoge: {1: fuga}, _prefix: "aaa"
    
    # after
    enum :hoge, {1: fuga}, prefix: "aaa"
    ```

#### テーブルにカラムの存在しないenum名の定義にはattribuitesの定義が必須
- コード例
    ```ruby
    # before
    enum non_column_backed_example: {1: fuga}, _prefix: "aaa"
    
    # after
    attribute :non_column_backed_example, :integer
    enum :non_column_backed_example, {1: fuga}, prefix: "aaa"
    ```
- 問題になるパターン
    migrationファイルでModelを呼び出すコードがあり、そのモデルに事後的にenumが追加されていた場合、migrationの途中で最終的にはテーブルにカラムが存在するenumの定義がnon_column_backed_exampleとして扱われる事象が発生する。

    **migration1**. companyテーブルの作成
    **migration2**. Company.update_column(status: true)
        => この時点ではCompanyクラスには```enum :publish_status```が定義されているがcompanyテーブルにはpublish_statusカラムがまだ存在しないのでエラーが発生する
    **migration3**. companyテーブルにpublish_status(enum)カラムを追加
- 対応
    - 一次対応
        enumメソッドをオーバーライドしてis_non_column_backedの際にattributeを呼び出す
        ```ruby
        def enum(name, values, **args)
          is_non_column_backed = attributes_to_define_after_schema_loads[name.to_s].nil?
          if is_non_column_backed
            data_type = values.values.first.is_a?(Integer) ? :integer : :string
            attribute(name, data_type)
          end
    
          super(name, values, **args)
        end
        ```
    - 恒久対応
        **migrationではmodelを呼び出さない**
        > モデルクラスを使わない
        migration内でデータの検査や変更をする場合など、ActiveRecordのモデルを使いたくなりますが、これは危険です。
        migrationは基本的にずっと残るので、仮に将来そのモデルファイルが削除された場合、rake db:migrate:resetが通らなくなってしまいます。
        SQLを直接使用するか、以下のように一時的にActiveRecord::Baseを継承したクラスを作るなどの対策が必要です。
        ```Object.const_set "User", Class.new(ActiveRecord::Base)```
        
        引用: https://techracho.bpsinc.jp/baba/2012_05_15/5462
        
#### 2. BigDecimal(to_d)のround()の桁数が増えたため計算精度が上がった。
- 実際はrubyを3系に上げた時点で計算結果は変わっていたが、Rails6系ではBigDecimalをオーバーライドして以前のround()の桁数で実行されるようになっていたのでRails7系に上げたタイミングでBigDecimalの精度が上がった。
- コード例
    ```ruby
    # before
    1.to_d/3.to_d
    =>0.3333333333333333333e0
    
    # after
    1.to_d/3.to_d
    => 0.333333333333333333333333333333333333e0
    ```
    
#### Psych::DisallowedClass error
- ActiveRecord::Coders::YAMLColumnを使用したシリアライズ/デシリアライズ    
    - safe_loadしたいclassはホワイトリスト(config.active_record.yaml_column_permitted_classes)に追加する
        
        ```ruby
        # serializeメソッドを使用した例
        serialize :hoge, type: MyClass, coder: YAML
        
        # config/application.rb
        config.active_record.yaml_column_permitted_classes = [Hash, Symbol, MyClass] # << MyClassを追加する
        ```
        
- GemでYaml.loadを使用している場合
        
    最新versionでも対応されていない場合は、モンキーパッチが必要。
    
#### Sessionのオブジェクトに変更があり、RspecでのHashによるモックができない。
- create_session_mockヘルパーメソッドを作成して対応
    ```ruby
    module SessionHelper
      def create_session_mock(data)
        # カスタムセッションクラスの定義
        custom_hash_class = Class.new(Hash) do
          # ActionDispatch::Request::Sessionに似たメソッドを追加する
          def enabled?
            true
          end
    
          def loaded?
            true
          end
        end
    
        # 新しいカスタムセッションハッシュの作成
        session = custom_hash_class.new
    
        # dataの内容をsessionに追加
        data.each do |key, value|
          session[key] = value
        end
    
        session
      end
    end
    ```
    呼び出しはこんな感じ
    ```ruby
    let(:session) { create_session_mock({}) }
    allow_any_instance_of(ActionDispatch::Request).to receive(:session).and_return(session)
    ```
    
#### Rails 7.1でのActive Recordインスタンスの`Marshal.dump`のシリアライズ方法に大きな変更が加えられました。

- インスタンス変数がシリアライズに含まれなくなった
    - changed?
    
    ```ruby
    # sessionを通すデータの受け渡しのエミュレート
    dump_instance = Marshal.dump(instance)
    load_instance = Marshal.load(dump_instance)
    
    # before
    load_instance.changed?
    => true
    
    # after
    load_instance.changed?
    => false
    ```
    
    - attr_accessor
    
    ```ruby
    # sessionを通すデータの受け渡しのエミュレート
    instance.attr_accessor_A = "データがあります"
    dump_instance = Marshal.dump(instance)
    load_instance = Marshal.load(dump_instance)
    
    # before
    load_instance.attr_accessor_A
    => "データがあります"
    
    # after
    load_instance.attr_accessor_A
    => nil
    ```
    
    - *他にもインスタンス変数に関わるものはあるかも*

### ガイド
https://railsguides.jp/upgrading_ruby_on_rails.html

基本的にはガイドに従って、適宜修正していく。

## 脱webpacker
### jsbundling-railsの概要(webpacker目線)
- scriptタグ
    ```javascript_pack_tag```はwebpackerで定義されたヘルパーメソッドなので、脱webpackerした際に使えなくなる。Railsのひとまずの対応としてはsprockets-railsの```javascript_include_tag```を使用すること。Webpack & Sprockets 構成が続くのがRailsの方針っぽい。
- assetsの公開
    - Webpackの出力をapp/assets/config/manifest.jsに読み込むことで、sprocketsに公開させる。
    - 流れ: Webpackでバンドル → Sprocketsが(実はもう一度にバンドルしてる&)publicに出力して```localhost:3000/assets/application.js```などのpathでリクエスト可能にしている。
    
    - 下記の場合はapp/assets/buildsに、Webpackの出力を書き込んでいる。
        ```
        //= link_tree ../images
        //= link_directory ../stylesheets .css
        //= link manifest.json
        //= link_tree ../builds
        ```
- source-map
    sproketsを通して公開することのデメリットとして２重でバンドルされることが挙げられる。
    source-mapのurlがsproketsによって生成され最終行に付与される。だが実際はwebpackがsource-mapを生成しているが、sproketsのsourceMappingURLによって上書きされてしまう。
    ```javascript
    // こっちはwebpackによって生成されたsourceMappingURL
    //# sourceMappingURL=http://localhost:3000/assets/packs/application-react.js-68840d1623831308455b5ffee7b7f258bbe9af8b6fd2979a13c32c2b459fcbb6.map
    ;
    
    // こっちはsprocketsによって生成されたsourceMappingURL->不要
    //# sourceMappingURL=application-react.js-34bc9e7e31e414474d36f3bc9ebe5954be87a260253cbaa4cd4c6e7058b755c8.map
    ```
    - 対応
        ```config.assets.debug = true```の設定を削除することでsprocketsでsource-mapを生成することを防げる。
    - issue: https://github.com/rails/jsbundling-rails/issues/73
- webpack-dev-server
    - jsbundling-rails
        webpack-dev-serverのサポートはwebpackerが行なっていたので、jsbundling-railsからは標準ではwebpack-dev-serverはサポートされない。
    - もしjsbundling-railsでもwebpack-dev-serverを使いたかったら下記の対応が必要
        - ```javascript_include_tag```をオーバーライド
            - webpack-dev-server起動時はsprocketsの管理から検索しないようにする
                ```ruby
                def javascript_include_tag(*source)
                    if dev_server_runnning?
                        super '', src: webpack_assets(*source)
                    else
                        super(super)
                    end
                end

                def dev_server_runnning?
                    # dev_serverにTCPリクエストしてアクセスが成功したらtrueを返す
                end
                
                def webpack_assets(*source)
                    # dev_serverのmanifest.jsonを取得して、ファイル名からpathを返す
                    # "application" => "/assets/application.js"
                end

                ```
        - Webpackのバンドル時にmanifest.jsonを出力する
            ```webpack.config.js
              plugins: [
                new WebpackManifestPlugin({
                  fileName: 'manifest.json',
                }),
              ]
            ```
        - Webpack管理のファイルにアクセスがあった際に```middlware```を通して、dev-serverのホストにリクエストを転送する
            - [webpackerのコード](https://github.com/rails/webpacker/blob/a715e055d937748c7cfd34b0450295348ca13ee6/lib/webpacker/railtie.rb#L12)が参考になります。
                イメージは、assets/webpack(webpack用のpath)にアクセスがあったら、webpack-dev-serverにリクエストを転送するmiddlewareを設定する。
    - 参考: https://tech.stmn.co.jp/entry/2021/01/15/161056

### ガイド
https://github.com/rails/jsbundling-rails/blob/main/docs/switch_from_webpacker.md

基本的には上記のガイドで進められるがwebpack-dev-serverはサポートしていないのとmapについての言及がないので混乱する。

## 最後に
蛇足だったので書きませんでしたが、このprojectではreact_on_railsを使用しており、同時に脱react_on_railsしたので、その部分も上記の脱webpackerする前に行いました。要望があればもしかしたら追記するかもです。
かなり省略して書いたので、時間がある時に追記していきます。