`bin/webpack`を読んだ
webpackerを理解するため、rails g webpacker:installで追加されるbin/webpackや設定の中身を読むことにした。
bin/webpack
newenv = { "NODE_PATH" => NODE_MODULES_PATH.shellescape }
cmdline = ["yarn", "run", "webpack", "--", "--config", WEBPACK_CONFIG] + ARGV
Dir.chdir(APP_PATH) do
exec newenv, *cmdline
end
bin/webpackでは実際にはyarn run webpack -- --config WEBPACK_CONFIGを実行している。WEBPACK_CONFIGはconfig/webpack/#{NODE_ENV}.jsとなっているため、config/webpack/development.jsなどとなる。
config/webpack/development.js
const sharedConfig = require('./shared.js') module.exports = merge(sharedConfig, { // ... })
config/webpack/shared.jsというファイルが環境ごとの設定ファイルでmergeされているようだ。
config/webpack/shared.js
const { env, settings, output, loaderDir } = require('./configuration.js')
settingsはconfig/webpacker.ymlをロードしたオブジェクトを参照している。settings.extensions:[.coffee, .erb, .js, .jsx, .ts, .vue, ...]settings.source_path:app/javascriptsettings.source_entry_path:packs
outputはpathとpublicPathというプロパティをもったオブジェクトを参照している。path:public/packspublicPath: ‘/packs’ASSET_HOSTという環境変数を指定することでホストを変更できそう。
loadersDirはconfig/webpack/loaders/を参照している。
const extensionGlob = `**/*{${settings.extensions.join(',')}}*` const entryPath = join(settings.source_path, settings.source_entry_path) const packPaths = sync(join(entryPath, extensionGlob)) module.exports = { entry: packPaths.reduce( // ... ) }
entryはwebpackによってbundleされる対象のファイルを設定する。syncはhttps://github.com/isaacs/node-globからexportされている。同期的にglobサーチをしている。- ここでは、
app/javascript/packs/**/*{.coffee,.erb,.js,.jsx}*のようなglobでファイルを検索し、マッチしたファイルのリストがpackPathsに代入されている。 - つまり、
app/javascript/packs/以下のsettings.extensionsで指定された拡張子をもつファイルがwebpackによってbundleされるということになる。
module.exports = { entry: packPaths.reduce( (map, entry) => { const localMap = map const namespace = relative(join(entryPath), dirname(entry)) localMap[join(namespace, basename(entry, extname(entry)))] = resolve(entry) return localMap }, {} ) }
entryにオブジェクトが指定された場合、プロパティごとにbundleされるファイルが分割される。output.filenameで[name]と指定された箇所にプロパティ名が入る。
const { env, settings, output, loaderDir } = require('./configuration.js') module.exports = { output: { filename: '[name].js', path: output.path, publicPath: output.publicPath } }
outputはbundleされたファイルの出力先を設定する。output.filenameでbundleされたファイル名を設定する。entryがオブジェクトで指定されているため、[name]にはオブジェクトの各プロパティ名が代入される。output.pathは出力先のパスを設定する。上記の通りpublic/packsが設定されている。output.publicPathは本番ビルド時のCSSやHTML内のURLを設定する。これは本番のみCDNを使う場合に便利。上述の通りこれは/packsが設定されているが、ASSET_HOSTという環境変数でこれを変更することができるようになっている。
module.exports = { module: { rules: sync(join(loadersDir, '*.js')).map(loader => require(loader)) } }
rulesはwebpackのモジュールを設定する。config/webpack/loaders/*.jsにマッチするファイルを検索している。- マッチしたファイルを
requireしている。各ファイルは以下のようになっている。これによって、config/webpack/loaders/*.js内の設定を展開している。
module.exports = { test: /\.(jpg|jpeg|png|gif|svg|eot|ttf|woff|woff2)$/i, use: [{ loader: 'file-loader', options: { publicPath, name: env.NODE_ENV === 'production' ? '[name]-[hash].[ext]' : '[name].[ext]' } }] }
const webpack = require('webpack') const ExtractTextPlugin = require('extract-text-webpack-plugin') const ManifestPlugin = require('webpack-manifest-plugin') module.exports = { plugins: [ new webpack.EnvironmentPlugin(JSON.parse(JSON.stringify(env))), new ExtractTextPlugin(env.NODE_ENV === 'production' ? '[name]-[hash].css' : '[name].css'), new ManifestPlugin({ publicPath: output.publicPath, writeToFileEmit: true }) ] }
pluginsはwebpackのプラグインを設定する。webpack.EnvironmentPluginはprocess.envから環境変数にアクセスできるようにするプラグイン。ExtractTextPluginはコンパイルされたテキストを別ファイルに出力するプラグイン。コンパイルしたCSSをJavaScriptでロードする他にLinkタグからロードしたい場合、コンパイルしたCSSをCSSファイルとして出力するためにこのプラグインを使う。[name]-[hash].cssの[hash]はビルド毎のユニークなハッシュ値を表す。
ManifestPluginはマニフェストファイルを生成するプラグイン。マニフェストファイルには、ファイル名と対応するコンパイル後のファイル名が載っている。マニフェストファイルによって、コンパイル前のファイル名からコンパイル後のファイル名に名前解決し、ヘルパーからアクセスできる。
= stylesheet_pack_tag "application" # load /packs/application-xxxxxxxx.css
{ "application.css": "/packs/application-xxxxxxxx.css" }
module.exports = { resolve: { extensions: settings.extensions, modules: [ resolve(settings.source_path), 'node_modules' ] } }
resolveはモジュール解決方法を設定する。webpackはデフォルトではいい感じに設定されている。resolve.extensionsはファイル名からモジュールを解決する際に自動的に付与する拡張子を設定する。resolve.modulesはモジュールを解決する際に検索されるディレクトリを設定する。
github.com/rails/webpacker/lib/webpacker/helper.rb
#stylesheet_pack_tagがマニフェストファイルからどのようにアセットを参照するかを確認する。
def stylesheet_pack_tag(*names, **options) unless Webpacker.dev_server.running? && Webpacker.dev_server.hot_module_replacing? stylesheet_link_tag(*sources_from_pack_manifest(names, type: :stylesheet), **options) end end def sources_from_pack_manifest(names, type:) names.map { |name| Webpacker.manifest.lookup(pack_name_with_extension(name, type: type)) } end def pack_name_with_extension(name, type:) "#{name}#{compute_asset_extname(name, type: type)}" end
#sources_from_pack_manifestでマニフェストからアセットのファイル名を解決しているようだ。ActionView::Helpers::AssetUrlHelper#compute_asset_extnameはファイル名とtypeから適切な拡張子を返す。Webpacker.manifestはWebpacker::Manifestインスタンスを返す。
github.com/rails/webpacker/lib/webpacker/manifest.rb
def lookup(name) compile if compiling? find name end def find(name) data[name.to_s] || handle_missing_entry(name) end def data if env.development? refresh else @data ||= load end end def refresh @data = load end def load if config.public_manifest_path.exist? JSON.parse config.public_manifest_path.read else {} end end