食べログ Advent Calendar 2019 24日目の記事です。
はじめまして。
好きな筋トレはバーベルシュラッグ。
好きな小説家は宮内悠介。
食べログのフロントエンドチームに所属している@sn_____です。
クリスマスイヴなのでWebpackerの話をします。
皆さんWebpacker使ってます?
個人的にはWebpackerは好みではありません。
Webpackerは面倒なwebpack回りの設定をやってくれるので、Railsアプリケーション開発では重宝されるケースも多いと思います。
しかし、提供されるコマンドの内部処理はブラックボックス化されており、詳細を把握していない人も多いのではないでしょうか。
フロントエンドエンジニア的にはそこらへんも抑えておきたいので、Webpackerが提供しているコマンドの内部処理を調査してみました。
調査対象
- webpacker@4.2.2(2019/12/24時点での最新版)
調査対象コマンド
./bin/webpack
./bin/webpack-dev-server
bundle exec rails webpacker:compile
./bin/webpack
github上にはここにコードがあります。
#!/usr/bin/env ruby
ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development"
ENV["NODE_ENV"] ||= "development"
require "pathname"
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
Pathname.new(__FILE__).realpath)
require "bundler/setup"
require "webpacker"
require "webpacker/webpack_runner"
APP_ROOT = File.expand_path("..", __dir__)
Dir.chdir(APP_ROOT) do
Webpacker::WebpackRunner.run(ARGV)
end
./bin/webpack
では環境変数のRAILS_ENV
やNODE_ENV
を規定し、Webpacker::WebpackRunner.run(ARGV)
を実行しています。
RAILS_ENV
やNODE_ENV
の中身は./bin/webpack
実行時どちらもdevelopment
です。
ではWebpacker::WebpackRunner.run(ARGV)
の処理を見に行きましょう。
アプリケーション上の/.bundle/ruby/x.x.x/gems/webpacker-x.x.x/lib/webpacker/webpack_runner.rb
が該当します。
github上にはここにコードがあります。
require "shellwords"
require "webpacker/runner"
module Webpacker
class WebpackRunner < Webpacker::Runner
def run
env = Webpacker::Compiler.env
cmd = if node_modules_bin_exist?
["#{@node_modules_bin_path}/webpack"]
else
["yarn", "webpack"]
end
if ARGV.include?("--debug")
cmd = [ "node", "--inspect-brk"] + cmd
ARGV.delete("--debug")
end
cmd += ["--config", @webpack_config] + @argv
Dir.chdir(@app_path) do
Kernel.exec env, *cmd
end
end
private
def node_modules_bin_exist?
File.exist?("#{@node_modules_bin_path}/webpack")
end
end
end
パッと見で、webpackのビルドコマンドを構築していることがわかります。
ですが、@app_path
、@node_modules_bin_path
、@webpack_config
といった不明なインスタンス変数が出てきましたね。
これらのインスタンス変数はこちらで宣言されています。
中身は以下です。
変数名 | 説明 | サンプル |
---|---|---|
@app_path |
アプリケーションの絶対パス | ****/app-root |
@node_modules_bin_path |
アプリケーション内に存在するnode_modules の絶対パス |
****/app-root/node_modules |
@webpack_config |
環境に応じたwebpack設定ファイルの絶対パス |
****/app-root/config/webpack/development.js ( NODE_ENV の中身がdevelopment だった場合) |
つまりKernel.exec env, *cmd
で実行している内容は、以下と同一です。
****/app-root/node_modules/.bin/webpack --config ****/app-root/config/webpack/development.js
Nodeコマンドで言い換えると
内部処理を追った結果./bin/webpack
のビルド処理は以下と同一でした。
./node_modules/.bin/webpack --config ./config/webpack/development.js
yarn
ならば以下のように置き換えられます。
yarn webpack --config ./config/webpack/development.js
./bin/webpack-dev-server
github上にはここにコードがあります。
#!/usr/bin/env ruby
ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development"
ENV["NODE_ENV"] ||= "development"
require "pathname"
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
Pathname.new(__FILE__).realpath)
require "bundler/setup"
require "webpacker"
require "webpacker/dev_server_runner"
APP_ROOT = File.expand_path("..", __dir__)
Dir.chdir(APP_ROOT) do
Webpacker::DevServerRunner.run(ARGV)
end
ほぼ、./bin/webpack
と同じですね。
こちらでは、最後にWebpacker::DevServerRunner.run(ARGV)
をしているので、その中身を見に行きます。
アプリケーション上の./.bundle/ruby/x.x.x/gems/webpacker-x.x.x/lib/webpacker/dev_server_runner.rb
が該当します。
github上にはここにコードがあります。
require "shellwords"
require "socket"
require "webpacker/configuration"
require "webpacker/dev_server"
require "webpacker/runner"
module Webpacker
class DevServerRunner < Webpacker::Runner
def run
load_config
detect_port!
execute_cmd
end
private
def load_config
app_root = Pathname.new(@app_path)
@config = Configuration.new(
root_path: app_root,
config_path: app_root.join("config/webpacker.yml"),
env: ENV["RAILS_ENV"]
)
dev_server = DevServer.new(@config)
@hostname = dev_server.host
@port = dev_server.port
@pretty = dev_server.pretty?
rescue Errno::ENOENT, NoMethodError
$stdout.puts "webpack dev_server configuration not found in #{@config.config_path}[#{ENV["RAILS_ENV"]}]."
$stdout.puts "Please run bundle exec rails webpacker:install to install Webpacker"
exit!
end
def detect_port!
server = TCPServer.new(@hostname, @port)
server.close
rescue Errno::EADDRINUSE
$stdout.puts "Another program is running on port #{@port}. Set a new port in #{@config.config_path} for dev_server"
exit!
end
def execute_cmd
env = Webpacker::Compiler.env
cmd = if node_modules_bin_exist?
["#{@node_modules_bin_path}/webpack-dev-server"]
else
["yarn", "webpack-dev-server"]
end
if ARGV.include?("--debug")
cmd = [ "node", "--inspect-brk"] + cmd
ARGV.delete("--debug")
end
cmd += ["--config", @webpack_config]
cmd += ["--progress", "--color"] if @pretty
Dir.chdir(@app_path) do
Kernel.exec env, *cmd
end
end
def node_modules_bin_exist?
File.exist?("#{@node_modules_bin_path}/webpack-dev-server")
end
end
end
ちょっとコードが長いですが、ざっくり処理を眺めると以下の流れが見えます。
-
load_config
でconfig/webpacker.yml
からhost,port
の設定を取得 -
detect_port
で同一hostname,portが使われていないか調査 -
execute_cmd
でwebpack-dev-server
関連のコマンドを実行
ではexecute_cmd
の処理は? と確認すると、webpacker/webpack_runner.rb
とかなり似ていますね。
つまりexecute_cmd
で実行している内容は、以下と同一です。
****/app-root/node_modules/.bin/webpack-dev-server --config ****/app-root/config/webpack/development.js
Nodeコマンドで言い換えると
内部処理を追った結果./bin/webpack-dev-server
の実行処理は以下と同一でした。
./node_modules/.bin/webpack-dev-server --config ./config/webpack/development.js --port 3035
(port番号はwebpacker.ymlの初期値)
yarn
ならば以下のように置き換えられます。
yarn webpack-dev-server --config ./config/webpack/development.js --port 3035
bundle exec rails webpacker:compile
アプリケーション上の./.bundle/ruby/x.x.x/gems/webpacker-x.x.x/lib/tasks/webpacker/compile.rake
がwebpacker:compile
と対応しています。
実行される処理のコードは以下です。
github上にはここにコードがあります。
$stdout.sync = true
def yarn_install_available?
rails_major = Rails::VERSION::MAJOR
rails_minor = Rails::VERSION::MINOR
rails_major > 5 || (rails_major == 5 && rails_minor >= 1)
end
def enhance_assets_precompile
# yarn:install was added in Rails 5.1
deps = yarn_install_available? ? [] : ["webpacker:yarn_install"]
Rake::Task["assets:precompile"].enhance(deps) do
Rake::Task["webpacker:compile"].invoke
end
end
namespace :webpacker do
desc "Compile JavaScript packs using webpack for production with digests"
task compile: ["webpacker:verify_install", :environment] do
Webpacker.with_node_env(ENV.fetch("NODE_ENV", "production")) do
Webpacker.ensure_log_goes_to_stdout do
if Webpacker.compile
# Successful compilation!
else
# Failed compilation
exit!
end
end
end
end
end
# Compile packs after we've compiled all other assets during precompilation
skip_webpacker_precompile = %w(no false n f).include?(ENV["WEBPACKER_PRECOMPILE"])
unless skip_webpacker_precompile
if Rake::Task.task_defined?("assets:precompile")
enhance_assets_precompile
else
Rake::Task.define_task("assets:precompile" => ["webpacker:yarn_install", "webpacker:compile"])
end
end
以下の処理がwebpack関連の実行処理ですね。
Webpacker.with_node_env(ENV.fetch("NODE_ENV", "production")) do
Webpacker.ensure_log_goes_to_stdout do
if Webpacker.compile
# Successful compilation!
else
# Failed compilation
exit!
end
end
end
Webpacker.with_node_env(ENV.fetch("NODE_ENV", "production"))
という処理が出てきます。
処理はこちらに記載されていますが、引数で受け取った文字列をENV["NODE_ENV"]
に突っ込むという処理を行っていますね。
次にensure_log_goes_to_stdout
というメソッドを実行した後にWebpacker.compile
を実行しています。
では次にWebpacker.compile
の処理内容を見に行きましょう。
処理はこちらに記載されています。
def compile
compiler.compile.tap do |success|
manifest.refresh if success
end
end
今度はcompiler.compile
というメソッドを実行しているので、その処理を見に行きます。
こちらにに記載されています。
def compile
if stale?
run_webpack.tap do |success|
# We used to only record the digest on success
# However, the output file is still written on error, (at least with ts-loader), meaning that the
# digest should still be updated. If it's not, you can end up in a situation where a recompile doesn't
# take place when it should.
# See https://github.com/rails/webpacker/issues/2113
record_compilation_digest
end
else
logger.info "Everything's up-to-date. Nothing to do"
true
end
end
また色々やっていますが、run_webpack
辺りが臭いですね。
なので、こちらに記載されているrun_webpack
の中身を見に行きます。
def run_webpack
logger.info "Compiling..."
stdout, stderr, status = Open3.capture3(
webpack_env,
"#{RbConfig.ruby} ./bin/webpack",
chdir: File.expand_path(config.root_path)
)
if status.success?
logger.info "Compiled all packs in #{config.public_output_path}"
logger.error "#{stderr}" unless stderr.empty?
if config.webpack_compile_output?
logger.info stdout
end
else
non_empty_streams = [stdout, stderr].delete_if(&:empty?)
logger.error "Compilation failed:\n#{non_empty_streams.join("\n\n")}"
end
status.success?
end
Open3.capture3(webpack_env, "#{RbConfig.ruby} ./bin/webpack")
がビルド実行箇所ですね。
変数webpack_env, RbConfig.ruby
の中身を確認してみたところ、以下の結果でした。
-
webpack_env
={"WEBPACKER_ASSET_HOST"=>nil, "WEBPACKER_RELATIVE_URL_ROOT"=>nil}
-
RbConfig.ruby
=/usr/local/ruby-x.x.x/bin/ruby
つまりbundle exec rails webpacker:compile
実行時のビルド処理はざっくり言うと。
-
NODE_ENV
をproduction
にし -
./bin/webpack
を実行
と同一であると言えます。
Nodeコマンドで言い換えると
内部処理を追った結果bundle exec rails webpacker:compile
のビルド処理は以下と同一でした。
NODE_ENV=production ./bin/webpack
前述のように./bin/webpackのコマンドは以下のように置き換えられます。
./node_modules/.bin/webpack --config ./config/webpack/production.js
更にyarn
ならば以下のように置き換えられます。
yarn webpack --config ./config/webpack/production.js
内部処理を追ってみての感想
どのコマンドも素直なyarn
コマンドに置き換えられるなーと感じました。
なので、単純にビルドを実行させたい時は、yarn
コマンドでそのまま実行してもよいですね。
最後に調査した各コマンドの対応表を貼っておきます。
Webpacker提供コマンド | yarnコマンド |
---|---|
./bin/webpack |
yarn webpack --config ./config/webpack/development.js |
./bin/webpack-dev-server |
yarn webpack-dev-server --config ./config/webpack/development.js --port 3035 |
bundle exec rails webpacker:compile |
yarn webpack --config ./config/webpack/production.js |
それでは皆さん良いwebpackライフを。
さてさて明日は、@tkyowaさんの「技術部門にOKRを導入したら3ヶ月で部の雰囲気がめちゃくちゃ良くなった話」です。
いよいよ最後ですね!
明日もよろしくおねがいします!