webpackerからshakapackerへの移行方法

# 背景
webpackerの開発が終了しており、後継であるshakapackerへの移行をする必要があります。

しかしながら、webpackerからshakapackerへの移行方法については、ネット記事が多数存在しますが、個別具体性が高く効率的に移行を進めることは困難でした。

そこで、本記事ではwebpackerからshakapackerへの移行に関する私の記録を残します。

本記事も個別具体性が高いかもしれませんが、これからwebpackerからshakapackerへの移行を試みる方々の参考になれば嬉しいです。

# バージョン
Ruby:3.2.4
Rails:6.1.7.7
bundler:2.5.3
node:22.11.0
npm:10.9.0
yarn:4.0.0
webpack:4.46.0

※shakapackerは、yarn3系もしくは4系と互換性があります。shakapackerのインストール前に、yarnのバージョンを更新しておきましょう。

※webpacker利用者の中にはnode18系を利用している方も多いかと思います。shakapackerへの移行を機に、node22系に更新しましょう。

# 開発環境
macOS: Sonoma14.7.1
エディター: Cursor@VSCode

# webpackerからshakapackerへの移行手順
## 1. shakapackerのインストール
公式ドキュメントを参考にして、shakapackerをインストールします。

~~~
bundle add shakapacker --strict
./bin/bundle install
./bin/rails shakapacker:install
~~~

https://github.com/shakacode/shakapacker?tab=readme-ov-file#installation

## 2. webpackerの削除
以下のコードにより、webpackerを削除します。

~~~
bundle remove webpacker
yarn remove @rails/webpacker
~~~

必要なBabelをインストールし、webpack-dev-serverを削除します。

~~~
yarn add @babel/core @babel/plugin-transform-runtime @babel/preset-env @babel/runtime babel-loader compression-webpack-plugin terser-webpack-plugin webpack webpack-assets-manifest webpack-cli webpack-merge
yarn add -D webpack-dev-server
~~~

## 3. webpack.config.jsの修正
webpack.config.jsは2種類存在します。

①プロジェクトファイル直下のwebpack.config.js
②app>config>webpack直下のwebpack.config.js
※webpack.config.jsが存在しない場合は、新規でファイルを作成してください。

プロジェクト直下のwebpack.config.js（①）は通常、shakapackerとは無関係で、独立したwebpackプロジェクト用に使用されることが多いです。

shakapackerは、webpackの設定ファイルとしてconfig/webpack/webpack.config.js（②）を使用します。

参考までに、プロジェクト直下のwebpack.config.js（①）とconfig/webpack/webpack.config.js（②）のコードは以下のとおりです。

~~~
# プロジェクト直下のwebpack.config.js（①）

const { env, webpackConfig, merge } = require('shakapacker') // shakapackerからmergeをインポート
const { existsSync } = require('fs')
const { resolve } = require('path')
const webpack = require('webpack') // Webpackをインポート

// CSS関連の設定
const cssConfig = {
  resolve: {
    extensions: ['.css', '.scss', '.sass'], // CSSやSASSファイルの拡張子を解決
  },
}

// 環境ごとの設定を読み込む関数
const envSpecificConfig = () => {
  const path = resolve(__dirname, `${env.nodeEnv}.js`)
  if (existsSync(path)) {
    console.log(`Loading ENV specific webpack configuration file ${path}`)
    return require(path)
  } else {
    console.log(`WARNING: Using default webpack configuration. Did not find a Env specific file at path ${path}`)
    return {}
  }
}

// 共通設定（以前のenvironment.jsから移行）
const commonConfig = {
  plugins: [
    new webpack.ProvidePlugin({
      $: 'jquery/src/jquery',
      jQuery: 'jquery/src/jquery',
    }),
  ],
}

// 環境ごとの設定、共通設定、CSS設定をマージ
module.exports = merge(webpackConfig, commonConfig, cssConfig, envSpecificConfig())
~~~
~~~
# app>config>webpack直下のwebpack.config.js（②）

// See the shakacode/shakapacker README and docs directory for advice on customizing your webpackConfig.
const { generateWebpackConfig } = require('shakapacker');
const { merge } = require('webpack-merge'); // webpack設定をマージするためのライブラリ
const webpack = require('webpack');
const path = require('path');

// 基本的なWebpack設定を生成
const webpackConfig = generateWebpackConfig();

// jQueryをグローバルスコープに提供するための設定
const customConfig = {
  plugins: [
    new webpack.ProvidePlugin({
      $: 'jquery', // `$`としてjQueryを使用可能に
      jQuery: 'jquery', // `jQuery`として使用可能に
      jquery: 'jquery', // `jquery`としても使用可能に
      Popper: ['popper.js', 'default'], // 必要に応じてPopper.jsも追加
    }),
  ],
};

// エントリポイントを追加（既存コード）
webpackConfig.entry = {
  application: path.resolve(__dirname, '../../app/javascript/application.js'),
  maps_autocomplete: path.resolve(__dirname, '../../app/javascript/maps_autocomplete.js'),
};

// Shakapackerの基本設定とカスタム設定をマージしてエクスポート
module.exports = merge(webpackConfig, customConfig);
~~~
### 補足① プロジェクト直下のwebpack.config.js（①）について
以下のコードが、environment.jsの役割を担っています。したがって、environment.jsファイルを削除します。

~~~
// 環境ごとの設定を読み込む関数
const envSpecificConfig = () => {
  const path = resolve(__dirname, `${env.nodeEnv}.js`)
  if (existsSync(path)) {
    console.log(`Loading ENV specific webpack configuration file ${path}`)
    return require(path)
  } else {
    console.log(`WARNING: Using default webpack configuration. Did not find a Env specific file at path ${path}`)
    return {}
  }
}
~~~

### 補足② app>config>webpack直下のwebpack.config.js（②）について
アプリのjsファイルではjQueryを使用しています。そのため、以下のコードのように「jQueryをグローバルスコープに提供するための設定」をコーディングしています。

~~~
const customConfig = {
  plugins: [
    new webpack.ProvidePlugin({
      $: 'jquery', // `$`としてjQueryを使用可能に
      jQuery: 'jquery', // `jQuery`として使用可能に
      jquery: 'jquery', // `jquery`としても使用可能に
      Popper: ['popper.js', 'default'], // 必要に応じてPopper.jsも追加
    }),
  ],
};
~~~
jsファイルは「app>javascript>application.js」の他にも、「app>javascript>maps_autocomplete.js」が存在します。maps_autocomplete.jsは個人的にカスタマイズしているファイルです。maps_autocomplete.jsをapplication.jsに統合するとシンプルになりますが、maps_autocomplete.js内のコード（主にGoogleMap表示関連のコード）は別管理したいので、以下のコードのようにエントリポイントを設定しています。

~~~
// エントリポイントを追加（既存コード）
webpackConfig.entry = {
  application: path.resolve(__dirname, '../../app/javascript/application.js'),
  maps_autocomplete: path.resolve(__dirname, '../../app/javascript/maps_autocomplete.js'),
};
~~~
## 4. development.js, test.js, production.jsの修正
development.js, test.js, production.jsのコードは、以下のとおりです。必要に応じてカスタマイズしてください。
~~~
const { webpackConfig } = require('shakapacker')

// 必要に応じてカスタマイズを追加
// 例: webpackConfig.devtool = 'cheap-module-source-map'

module.exports = webpackConfig
~~~
## 5. webpacker.ymlからshakapacker.ymlを作成する。
config>webpacker.ymlをもとに、
config>shakapacker.ymlを作成します。

私の場合はPerplexity（chatGPTでもたぶん大丈夫です）に『現在の「webpacker.yml」のコードは以下のとおりです。このコードをshakapacker.ymlのコードに修正してください。』と依頼、shakapacker.ymlのコードを生成しました。
~~~
# config>shakapacker.yml

default: &default
  source_path: app/javascript
  source_entry_path: /
  public_root_path: public
  public_output_path: packs
  cache_path: tmp/cache/shakapacker
  webpack_compile_output: true
  nested_entries: false

  # Additional paths webpack should lookup modules
  # ['app/assets', 'engine/foo/app/assets']
  additional_paths: []

  # Reload manifest.json on all requests so we reload latest compiled packs
  cache_manifest: false

  # Extract and emit a css file
  extract_css: false

  static_assets_extensions:
    - .jpg
    - .jpeg
    - .png
    - .gif
    - .tiff
    - .ico
    - .svg
    - .eot
    - .otf
    - .ttf
    - .woff
    - .woff2

  extensions:
    - .jsx
    - .mjs
    - .js
    - .sass
    - .scss
    - .css
    - .module.sass
    - .module.scss
    - .module.css
    - .png
    - .svg
    - .gif
    - .jpeg
    - .jpg

development:
  <<: *default
  compile: true

  # Reference: https://webpack.js.org/configuration/dev-server/
  dev_server:
    https: false
    host: localhost
    port: 3035
    public: localhost:3035
    hmr: false
    # Inline should be set to true if using HMR
    inline: true
    overlay: true
    compress: true
    disable_host_check: true
    use_local_ip: false
    quiet: false
    pretty: false
    headers:
      'Access-Control-Allow-Origin': '*'
    watch_options:
      ignored: '**/node_modules/**'

test:
  <<: *default
  compile: true

  # Compile test packs to a separate directory for testing purposes.
  public_output_path: packs-test

production:
  <<: *default

  # Production depends on precompilation of packs prior to booting for performance.
  compile: false

  # Extract and emit a CSS file for production builds.
  extract_css: true

  # Cache the manifest.json for performance in production.
  cache_manifest: true

~~~
なお「nested_entries: false」は必ず設定してください。app>javascript直下のファイルのみを対象とするためです。「nested_entries: false」を設定しないと、app>javascript配下のサブディレクトリも全て対象となってしまいます。

shakapacker.ymlでは「source_entry_path: /」を設定します。このタイミングで、app>javascript>packs配下のファイルを、app>javascript配下に移動します。
## 6. package.jsonの修正
package.jsonに以下のコードを書いてください。
~~~
"browserslist": [
  "defaults"
]
~~~
~~~
"babel": {
  "presets": [
    "./node_modules/shakapacker/package/babel/preset.js"
  ]
}
~~~
「"./node_modules/shakapacker/package/babel/preset.js"」が重複で書かれている場合がありますが、重複は避けましょう。

次いで、プロジェクト直下の「.browserslist」を削除しておきます。

## 7. cssおよびsassの設定
以下のコードを実行します。
~~~
yarn add css-loader style-loader mini-css-extract-plugin css-minimizer-webpack-plugin sass sass-loader
~~~

## 8. コンパイルの実行
以下のコードを実行します。
~~~
bin/rails assets:clobber assets:precompile
~~~

## 9. 古いbinstubの削除
以下のコードで、古いbintubをgitの対象外とします。
~~~
git rm bin/webpack bin/webpack-dev-server
~~~

## 10. webpackerのヘルパーの修正
「javascript_pack_tag」と「stylesheet_pack_tag」は、各ファイルに一度しか使えません。基本形は、公式ドキュメントのとおりです。

~~~
<%= javascript_pack_tag 'application' %>
<%= stylesheet_pack_tag 'application' %>
~~~

https://github.com/shakacode/shakapacker#view-helpers

私の場合は、「application.html.erb」で「application.js」と「maps_autocomplete.js」の2つのファイルを読み取る必要がありました。

コーディングは、以下のとおりです。

~~~
<%= javascript_pack_tag 'application', 'maps_autocomplete', type: 'module', 'data-turbolinks-track': 'reload' %>
~~~

「'data-turbolinks-track': 'reload'」が必要かは不明ですが、念のため、webpacker使用時のコードを残しています。

※画像パスも「asset_pack_path('media/images/hoge')」から「asset_pack_path('static/hoge')」に修正する必要があるかもしれませんので、ご注意ください。

## 11. サーバーの起動
最後に「bin/rails s」で、サーバーを立ち上げてみてください。

# まとめ
webpackerからshakapackerへの移行方法を紹介させていただきました。

コンパイル時にエラーが発生したりして、やってみると結構大変です。

エラーに関しては、Perplexityを多用すると解消できます。しかしながら、細かいことまで自分で理解していかないと解消しきれないエラーも出ますので、ゆっくりでもいいので確実に移行を進めてみましょう！