インストールタスクにパッケージマネージャー自動検出を追加
javascript:install:* タスクでYarnコマンドがハードコードされていた問題を解消し、ロックファイルから使用中のパッケージマネージャーを自動検出する Jsbundling::PackageManager モジュールを導入しました。これにより、npm・pnpm・bunを使うプロジェクトでも正しいコマンドでセットアップが行われるようになります。
背景
インストールタスクと実際のプロジェクト環境の間にあったパッケージマネージャーの不一致が、この変更の起点です。build.rake では Jsbundling::Tasks を通じてロックファイルによるパッケージマネージャー検出が行われていましたが、javascript:install:[esbuild|rollup|webpack] を実行するインストールスクリプト群(install.rb)では yarn add や yarn build がハードコードされたままでした。(#217)
rails/rails 側では既に rails/rails#56636 で Rails::Generators::JsPackageManager が導入され、ジェネレーターでのパッケージマネージャー検出が統一されていました。本PRはその設計方針をjsbundling-railsにも適用し、インストールタスクとビルドタスクで検出ロジックを共通化します。
技術的な変更
新規モジュール Jsbundling::PackageManager が lib/jsbundling/package_manager.rb に追加され、ビルドタスクで使われていた Jsbundling::Tasks のインライン実装を置き換えます。
PackageManager はまずロックファイルの存在によってパッケージマネージャーを特定し、ロックファイルが見つからない場合は実行可能コマンドを探します。
def self.detect(root)
if root.join("bun.lock").exist? || root.join("bun.lockb").exist? || root.join("bun.config.js").exist?
:bun
elsif root.join("pnpm-lock.yaml").exist?
:pnpm
elsif root.join("package-lock.json").exist?
:npm
else
detect_by_executable || :yarn
end
end
def detect_by_executable
%i[ bun yarn pnpm npm ].each do |exe|
return exe if system "command -v #{exe} > /dev/null"
end
end
各パッケージマネージャーのコマンドは MANAGERS 定数にHashとして集約されており、add・add_dev・install・build の4種類が定義されています。
MANAGERS = {
bun: { add: "bun add %s", add_dev: "bun add %s --dev", install: "bun install", build: "bun run build" },
pnpm: { add: "pnpm add %s", add_dev: "pnpm add -D %s", install: "pnpm install", build: "pnpm run build" },
npm: { add: "npm install %s", add_dev: "npm install -D %s", install: "npm ci", build: "npm run build" },
yarn: { add: "yarn add %s", add_dev: "yarn add --dev %s", install: "yarn install", build: "yarn build" }
}.freeze
インストールスクリプト側では、ハードコードされていた yarn add と yarn build がそれぞれ Jsbundling::PackageManager.add_command と Jsbundling::PackageManager.build_command の呼び出しに置き換わります。
変更前:
run "yarn add --dev esbuild"
# ...
run %(yarn build)
変更後:
run Jsbundling::PackageManager.add_command("esbuild", dev: true)
# ...
run Jsbundling::PackageManager.build_command
同様の変更が rollup/install.rb・webpack/install.rb にも適用されています。Procfile.dev のウォッチコマンドも install_procfile.rb 経由で動的に生成されるようになり、たとえばnpmを検出した場合は js: npm run build -- --watch が書き込まれます。
build.rake では Jsbundling::Tasks のインライン実装(約50行)が削除され、新しい PackageManager モジュールへの委譲に統一されました。これにより、ビルドタスクとインストールタスクの両方で同一の検出ロジックが使われます。
設計判断
Jsbundling::Tasks を廃止し Jsbundling::PackageManager に一本化する アプローチが採られました。build.rake に埋め込まれていた検出ロジックを独立したモジュールに切り出すことで、インストールスクリプトからも再利用できる形になっています。これはrails/railsが Rails::Generators::JsPackageManager として同様の分離を行った設計と一致しており、両プロジェクト間での挙動の一貫性が意識されています。
ロックファイルが存在しない場合のデフォルトはYarnです。これはリリース済みのRails v8.1.2の挙動に合わせた判断であり、デフォルトをnpmに変えるべきという提案(#217)に対して、既存リリースとの一貫性を優先しています。ロックファイルが見つからない場合は detect_by_executable で実行可能コマンドを探すため、yarnがインストールされていない環境でも代替マネージャーが選択されます。
npmのインストールコマンドには npm install ではなく npm ci が使用されています。これはCIや再現性の高いビルド環境を想定した選択であり、package-lock.json の厳密な依存解決を優先する設計意図が読み取れます。
まとめ
Jsbundling::PackageManager の導入により、インストールタスクとビルドタスクの両方でパッケージマネージャーの検出ロジックが統一されました。ロックファイルという既存の環境情報を起点とした検出戦略は、rails/railsのジェネレーター実装とも整合しており、プロジェクトが使うパッケージマネージャーを明示的に指定しなくても正しく動作する設計が実現されています。