Railsジェネレータがロックファイルから自動でパッケージマネージャを検出
Rails 8.1では、actiontext:installやrails generate channelなどのジェネレータが、プロジェクトのロックファイルを検出して適切なJavaScriptパッケージマネージャを自動選択するようになりました。
背景
これまでRailsのジェネレータはyarnをハードコードしており、npmやpnpm、bunを使用しているプロジェクトでは、ジェネレータ実行後に生成されたファイルを手動で修正する必要がありました。#47416で報告されていたこの問題に対応し、#56636でロックファイルベースの自動検出機能が実装されています。
技術的な変更
検出ロジックの実装
新しく追加されたRails::Generators::JsPackageManagerモジュールが、以下の優先順位でパッケージマネージャを検出します:
-
bun.lockbまたはbun.config.jsが存在 → bun -
pnpm-lock.yamlが存在 → pnpm -
package-lock.jsonが存在 → npm - 上記のいずれも存在しない → yarn(デフォルト)
module Rails
module Generators
module JsPackageManager
MANAGERS = {
bun: {
add: "bun add %s",
install: "bun install --frozen-lockfile",
lockfile: "bun.lockb",
audit: nil
},
pnpm: {
add: "pnpm add %s",
install: "pnpm install --frozen-lockfile",
lockfile: "pnpm-lock.yaml",
audit: "pnpm audit"
},
npm: {
add: "npm install %s",
install: "npm ci",
lockfile: "package-lock.json",
audit: "npm audit"
},
yarn: {
add: "yarn add %s",
install: "yarn install --immutable",
lockfile: "yarn.lock",
audit: "yarn audit"
}
}.freeze
def self.detect(root)
if 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
:yarn
end
end
def package_manager
@package_manager ||= JsPackageManager.detect(project_root)
end
def package_add_command(package)
config = MANAGERS[package_manager]
config[:add] % package
end
end
end
end
ジェネレータの変更
ActionTextとActionCableのジェネレータが、このモジュールをincludeして統一的にパッケージマネージャを扱うようになりました。
変更前(ActionTextインストーラ):
def install_editor
editor = options[:editor]
say "Installing #{editor} JavaScript dependency", :green
if using_bun?
run "bun add #{editor}"
elsif using_node?
run "yarn add #{editor}"
end
end
変更後:
include Rails::Generators::JsPackageManager
def install_editor
return unless using_js_runtime?
editor = options[:editor]
say "Installing #{editor} JavaScript dependency", :green
run package_add_command(editor)
end
各パッケージマネージャに応じて、以下のコマンドが実行されます:
-
bun:
bun add trix -
pnpm:
pnpm add trix -
npm:
npm install trix -
yarn:
yarn add trix
Rakeタスクの更新
yarn:installタスクも同様に更新され、javascript:installタスクとして汎用化されています。
namespace :javascript do
desc "Install all JavaScript dependencies"
task :install do
valid_node_envs = %w[test development production]
node_env = ENV.fetch("NODE_ENV") do
valid_node_envs.include?(Rails.env) ? Rails.env : "production"
end
manager = Rails::Generators::JsPackageManager.detect(Rails.root)
config = Rails::Generators::JsPackageManager::MANAGERS[manager]
system({ "NODE_ENV" => node_env }, config[:install], exception: true)
rescue Errno::ENOENT
$stderr.puts "#{manager} failed to execute."
$stderr.puts "Ensure #{manager} is installed and available in PATH."
exit 1
end
end
namespace :yarn do
task install: "javascript:install"
end
後方互換性のため、yarn:installはjavascript:installのエイリアスとして残されています。
設計判断
ロックファイルベースの検出
PR内では、環境変数や設定ファイルでパッケージマネージャを指定する方法も検討されましたが、ロックファイルの存在チェックが最もシンプルで確実な方法として採用されています。プロジェクトにpackage.jsonが存在しても、実際にどのマネージャを使っているかはロックファイルを見ることで判断できます。
bunの検出にbun.config.jsを含める理由
jsbundling-railsを使用しているプロジェクトでは、まだbun.lockbが生成されていない段階でbun.config.jsが存在するケースがあるため、両方をチェックしています。これは元のコードで実装されていた判定ロジックを引き継いだものです。
パッケージマネージャごとのコマンド差異
各パッケージマネージャで、CI環境での再現性を重視したコマンドが選択されています:
-
bun/pnpm:
--frozen-lockfileでロックファイルの更新を禁止 -
npm:
npm ciでpackage-lock.jsonを厳密に再現 -
yarn:
--immutableでロックファイルの変更を禁止(Yarn 2+)
これにより、ジェネレータ実行時に意図しないロックファイルの更新が発生しません。