食べログ 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 rubyENV["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)doWebpacker::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"moduleWebpackerclassWebpackRunner<Webpacker::Runnerdefrunenv=Webpacker::Compiler.envcmd=ifnode_modules_bin_exist?["#{@node_modules_bin_path}/webpack"]else["yarn","webpack"]endifARGV.include?("--debug")cmd=["node","--inspect-brk"]+cmdARGV.delete("--debug")endcmd+=["--config",@webpack_config]+@argvDir.chdir(@app_path)doKernel.execenv,*cmdendendprivatedefnode_modules_bin_exist?File.exist?("#{@node_modules_bin_path}/webpack")endendend
パッと見で、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 rubyENV["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)doWebpacker::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"moduleWebpackerclassDevServerRunner<Webpacker::Runnerdefrunload_configdetect_port!execute_cmdendprivatedefload_configapp_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?rescueErrno::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!enddefdetect_port!server=TCPServer.new(@hostname,@port)server.closerescueErrno::EADDRINUSE$stdout.puts"Another program is running on port #{@port}. Set a new port in #{@config.config_path} for dev_server"exit!enddefexecute_cmdenv=Webpacker::Compiler.envcmd=ifnode_modules_bin_exist?["#{@node_modules_bin_path}/webpack-dev-server"]else["yarn","webpack-dev-server"]endifARGV.include?("--debug")cmd=["node","--inspect-brk"]+cmdARGV.delete("--debug")endcmd+=["--config",@webpack_config]cmd+=["--progress","--color"]if@prettyDir.chdir(@app_path)doKernel.execenv,*cmdendenddefnode_modules_bin_exist?File.exist?("#{@node_modules_bin_path}/webpack-dev-server")endendend
ちょっとコードが長いですが、ざっくり処理を眺めると以下の流れが見えます。
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=truedefyarn_install_available?rails_major=Rails::VERSION::MAJORrails_minor=Rails::VERSION::MINORrails_major>5||(rails_major==5&&rails_minor>=1)enddefenhance_assets_precompile# yarn:install was added in Rails 5.1deps=yarn_install_available??[]:["webpacker:yarn_install"]Rake::Task["assets:precompile"].enhance(deps)doRake::Task["webpacker:compile"].invokeendendnamespace:webpackerdodesc"Compile JavaScript packs using webpack for production with digests"taskcompile: ["webpacker:verify_install",:environment]doWebpacker.with_node_env(ENV.fetch("NODE_ENV","production"))doWebpacker.ensure_log_goes_to_stdoutdoifWebpacker.compile# Successful compilation!else# Failed compilationexit!endendendendend# Compile packs after we've compiled all other assets during precompilationskip_webpacker_precompile=%w(no false n f).include?(ENV["WEBPACKER_PRECOMPILE"])unlessskip_webpacker_precompileifRake::Task.task_defined?("assets:precompile")enhance_assets_precompileelseRake::Task.define_task("assets:precompile"=>["webpacker:yarn_install","webpacker:compile"])endend
以下の処理がwebpack関連の実行処理ですね。
Webpacker.with_node_env(ENV.fetch("NODE_ENV","production"))doWebpacker.ensure_log_goes_to_stdoutdoifWebpacker.compile# Successful compilation!else# Failed compilationexit!endendend
Webpacker.with_node_env(ENV.fetch("NODE_ENV", "production"))
という処理が出てきます。
処理はこちらに記載されていますが、引数で受け取った文字列をENV["NODE_ENV"]
に突っ込むという処理を行っていますね。
次にensure_log_goes_to_stdout
というメソッドを実行した後にWebpacker.compile
を実行しています。
では次にWebpacker.compile
の処理内容を見に行きましょう。
処理はこちらに記載されています。
defcompilecompiler.compile.tapdo|success|manifest.refreshifsuccessendend
今度はcompiler.compile
というメソッドを実行しているので、その処理を見に行きます。
こちらにに記載されています。
defcompileifstale?run_webpack.tapdo|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/2113record_compilation_digestendelselogger.info"Everything's up-to-date. Nothing to do"trueendend
また色々やっていますが、run_webpack
辺りが臭いですね。
なので、こちらに記載されているrun_webpack
の中身を見に行きます。
defrun_webpacklogger.info"Compiling..."stdout,stderr,status=Open3.capture3(webpack_env,"#{RbConfig.ruby} ./bin/webpack",chdir: File.expand_path(config.root_path))ifstatus.success?logger.info"Compiled all packs in #{config.public_output_path}"logger.error"#{stderr}"unlessstderr.empty?ifconfig.webpack_compile_output?logger.infostdoutendelsenon_empty_streams=[stdout,stderr].delete_if(&:empty?)logger.error"Compilation failed:\n#{non_empty_streams.join("\n\n")}"endstatus.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ヶ月で部の雰囲気がめちゃくちゃ良くなった話」です。
いよいよ最後ですね!
明日もよろしくおねがいします!