Webpack Encoreをwebpack4に書き直す

2018年12月18日 by unio PHP Symfony4

この記事はSymfony Advent Calendar 201818日目の記事です。

今年2018年の11月に Webpack Encore のwebpack4対応が 発表 され、新しくwebpack-encore-bundleが追加されました。 Webpack Encoreを使うことでフロントエンドの開発が楽になる一方で、利用にはいくつかのデメリットもあります。
メリット
  • webpackの設定を簡潔に書ける
  • twigのencore_entry_script_tags関数など便利な機能がある
  • 総じてSymfonyフレームワーク側との連携が強力なので、開発が簡単
デメリット
  • 情報が少ない
  • 複雑な設定になると結局webpackの知識が必要になる
  • 本家webpackへの追従にタイムラグがある
  • バックエンドがSymfonyにロックインされる可能性
今回は、すでにWebpack Encoreで作っちゃったけどやっぱりピュアなwebpack4に書き直したい!という方向けの記事です。 なおWebpack Encoreのバージョンは0.22.2で、webpackはwebpack4が対象です。

で、失敗したやつ

さっそくですが、一番最初に試して失敗した方法です。 Webpack Encoreはざっくり言うとwebpackコンフィグビルダーなので、 Encore.getWebpackConfig()の結果をwebpack.config.jsに書けばいけるはずです。
webpack.config.js
            
                var Encore = require('@symfony/webpack-encore');

                Encore
                    ...
                ;

                console.log(JSON.stringify(Encore.getWebpackConfig()));
            
        
Encoreで作成した設定のJSONをデシリアライズするだけでいけると思いましたが、 modulepluginsのオブジェクトがうまく展開されずに撃沈しました。

Webpack Encore vs. webpack4

仕方がないので、Webpack Encoreに用意されている関数を調べながら書き換えていきます。 下記はWebpack Encoreで書いたwebpack.config.jsのサンプルです。 たぶんよく使われるであろう設定をいくつか抜粋しました。
webpack.config.js
            
                var Encore = require('@symfony/webpack-encore');

                Encore
                    .setOutputPath('public/build/')
                    .setPublicPath('/build')
                    .cleanupOutputBeforeBuild()
                    .addEntry('hoge', './assets/hoge/index.js')
                    .enableSourceMaps(!Encore.isProduction())
                    .enableSingleRuntimeChunk()
                    .splitEntryChunks()
                    .autoProvidejQuery()
                    .enablePostCssLoader()
                    .enableSassLoader()
                    .enableVueLoader()
                ;

                module.exports = Encore.getWebpackConfig();
            
        

書き換え結果

以下はdevelopmentビルド時のwebpack設定です。 実際はNODE_ENVやSymfony4で定義した.envを使って、設定をうまく切り替える仕組みを導入する必要があります。
webpack.config.js
            
                const path = require('path');

                // 以下は@symfony/webpack-encoreをremoveした際に個別でaddが必要
                const webpack = require('webpack');
                const MiniCssExtractPlugin = require('mini-css-extract-plugin');
                const ManifestPlugin = require('webpack-manifest-plugin');
                const CleanWebpackPlugin = require('clean-webpack-plugin');
                const VueLoaderPlugin = require('vue-loader/lib/plugin');
                const AssetsWebpackPlugin = require('assets-webpack-plugin');

                module.exports = {
                    mode: 'development',
                    context: __dirname,
                    // Encore.addEntryの設定
                    entry: {
                        hoge: './assets/hoge/index.js'
                    },
                    // Encore.setOutputPathとEncore.setPublicPathの設定
                    output: {
                        path: path.resolve(__dirname, 'public/build'),
                        filename: '[name].js',
                        publicPath: '/build/',
                        pathinfo: true
                    },
                    module: {
                        rules: [
                            // Babel7適用ルール
                            // v0.22.2ではデフォルト適用される
                            {
                                test: /\.jsx?$/,
                                exclude: /(node_modules|bower_components)/,
                                use: {
                                    loader: 'babel-loader',
                                    options: {
                                        cacheDirectory: true,
                                        presets: ['@babel/preset-env'],
                                        plugins: ['@babel/plugin-syntax-dynamic-import']
                                    }
                                }
                            },
                            // CSS適用ルール
                            // Encore.enablePostCssLoaderの設定
                            {
                                test: /\.css$/,
                                use: [
                                    path.resolve(__dirname, 'node_modules/mini-css-extract-plugin/dist/loader.js'),
                                    {loader: 'css-loader', options: {minimize: false, sourceMap: true, importLoaders: 1}},
                                    {loader: 'postcss-loader', options: {sourceMap: true}}
                                ]
                            },
                            {
                                test: /\.(png|jpg|jpeg|gif|ico|svg|webp)$/,
                                loader: 'file-loader',
                                options: {name: 'images/[name].[hash:8].[ext]', publicPath: '/build/'}
                            },
                            {
                                test: /\.(woff|woff2|ttf|eot|otf)$/,
                                loader: 'file-loader',
                                options: {name: 'fonts/[name].[hash:8].[ext]', publicPath: '/build/'}
                            },
                            // Sass、Scss適用ルール
                            // Encore.enablePostCssLoaderとEncore.enableSassLoaderの設定
                            {
                                test: /\.s[ac]ss$/,
                                use: [
                                    path.resolve(__dirname, 'node_modules/mini-css-extract-plugin/dist/loader.js'),
                                    {loader: 'css-loader', options: {minimize: false, sourceMap: true, importLoaders: 1}},
                                    {loader: 'postcss-loader', options: {sourceMap: true}},
                                    {loader: 'resolve-url-loader', options: {sourceMap: true}},
                                    {loader: 'sass-loader', options: {sourceMap: true}}
                                ]
                            },
                            // Vue.js適用ルール
                            // Encore.enableVueLoaderの設定
                            {
                                test: /\.vue$/,
                                loader: 'vue-loader'
                            }
                        ]
                    },
                    plugins: [
                        new MiniCssExtractPlugin({filename: '[name].css', chunkFilename: '[name].css'}),
                        new ManifestPlugin({
                            publicPath: null,
                            basePath: 'build/',
                            fileName: 'manifest.json',
                            transformExtensions: /^(gz|map)$/i,
                            writeToFileEmit: true,
                            seed: {},
                            filter: (file) => {
                                return (!file.isChunk || !['_tmp_shared', '_tmp_copy'].includes(file.chunk.id));
                            },
                            map: null,
                            generate: null,
                            sort: null
                        }),
                        new webpack.LoaderOptionsPlugin({
                            debug: true,
                            options: {
                                context: __dirname,
                                output: {path: path.resolve(__dirname, 'public/build')}
                            }
                        }),
                        new webpack.ProvidePlugin({'$': 'jquery', jQuery: 'jquery', 'window.jQuery': 'jquery'}),
                        // Encore.cleanupOutputBeforeBuildの設定
                        new CleanWebpackPlugin(['**/*'], {
                            root: path.resolve(__dirname, 'public/build'),
                            verbose: false,
                            allowExternal: false,
                            dry: false
                        }),
                        new webpack.DefinePlugin({'process.env': {NODE_ENV: 'development'}}),
                        new VueLoaderPlugin(),
                        new AssetsWebpackPlugin({
                            path: path.resolve(__dirname, 'public/build'),
                            filename: 'entrypoints.json',
                            includeAllFileTypes: true,
                            entrypoints: true,
                            // Webpack Encoreで定義された関数をそのままコピペ
                            processOutput: (assets) => {
                                for (const entry of ['_tmp_shared', '_tmp_copy']) {
                                    delete assets[entry];
                                }
                                delete assets.entrypoints;
                                for (const asset in assets) {
                                    for (const fileType in assets[asset]) {
                                        if (!Array.isArray(assets[asset][fileType])) {
                                            assets[asset][fileType] = [assets[asset][fileType]];
                                        }
                                    }
                                }
                                return JSON.stringify({
                                    entrypoints: assets
                                }, null, 2);
                            }
                        }),
                    ],
                    // splitChunks
                    // Encore.enableSingleRuntimeChunkの設定
                    optimization: {
                        namedModules: true,
                        runtimeChunk: 'single',
                        splitChunks: {chunks: 'all'}
                    },
                    devtool: 'inline-source-map',
                    performance: {hints: false},
                    stats: {
                        hash: false,
                        version: false,
                        timings: false,
                        assets: false,
                        chunks: false,
                        maxModules: 0,
                        modules: false,
                        reasons: false,
                        children: false,
                        source: false,
                        errors: false,
                        errorDetails: false,
                        warnings: false,
                        publicPath: false,
                        builtAt: false
                    },
                    resolve: {
                        extensions: ['.wasm', '.mjs', '.js', '.json', '.jsx', '.vue', '.ts', '.tsx'],
                        alias: {'vue$': 'vue/dist/vue.esm.js'}
                    },
                    externals: {}
                };
            
        
サンプルのEncoreのコードに比べると、(当然ですが)記述量はかなり増えました。 実際は不要な設定もあるかと思いますが、一旦Webpack Encoreとほぼ同じ動作をする形に記述しています。 それぞれの設定の意味は、各パッケージサイトやwebpack本家サイトをご覧いただくとして、 いくつか注意点を抜粋して説明いたします。

@symfony/webpack-encore依存を切り離す

Encore.getWebpackConfig()の出力結果と比較して頂くと分かりますが、 Webpack Encoreが独自に提供しているプラグインを切り離す関係上いくつかの設定を削除しています。
クラス名 Encore独自機能 説明
DeleteUnusedEntriesJSPlugin YES addStyleEntryでcssを登録する際に、webpack本体で生成される同名のjsを削除するプラグイン
FriendlyErrorsWebpackPlugin NO Encore独自のフィルタを使っていたため、一旦除外
AssetOutputDisplayPlugin YES FriendlyErrorsWebpackPluginと連携して、asset情報を表示するプラグイン

@symfony/webpack-encoreを削除する際に

@symfony/webpack-encoreを削除した場合は、本家 package.json のdependenciesに記載されている各種パッケージを追加するのを忘れないでください。 loader名やpluginsの設定を参考に、必要なパッケージを個別にaddします。

まとめ

Webpack Encoreを使うと簡潔にwebpack.config.jsを書くことができます。 しかしバックエンドがSymfonyに限らない状況になると、途端にベンダーロックインの辛さが発生します。 (実際はバックエンドによらない設計思想らしいので、大きな影響はないのですが) また、細かい設定が必要になると結局webpackの知識も必要になり、Encoreも含めると余計な学習コストが発生します。 もし特段の事情がなければ、Webpack Encoreではなくピュアなwebpack構成でプロジェクトを進めると後々潰しが効きやすくなると思います。