Upgrading Webpack 3.x to 5.37.0 for Legacy Vue CLI 2.x Projects
Development build speed improves by 60% after upgrading webpack from 3.x to 5.37.0 for large Vue CLI 2.x codebases, while production bundling speed sees a 61% performance lift.
Upgrade Steps
- Upgrade core webpack packages to version 5.37.0. Key breaking changes post-upgrade include updated import syntax for
webpack-mergeand modified local development server startup paarmeters. - Run
yarn upgrade-interactive --latestto update all project dependencies to their latest compatible versions, resolving incompatibilities between old loaders/plugins and webpack 5. - Fix babel version mismatch: Legacy
babel-core@6.xonly works withbabel-loader@7, runyarn add -D babel-loader@7to install the compatible loader version. - Replace the deprecated
extract-text-webpack-pluginwithmini-css-extract-pluginto handle CSS extraction in production builds.
Configuration File Updates
webpack.base.config.js
'use strict'
const path = require('path')
const buildUtils = require('./utils')
const projectConfig = require('../config')
const { VueLoaderPlugin } = require('vue-loader')
function getAbsolutePath(dir) {
return path.join(__dirname, '..', dir)
}
module.exports = {
devtool: 'eval-source-map',
context: path.resolve(__dirname, '../'),
entry: {
main: './src/main.js'
},
output: {
path: projectConfig.build.assetsRoot,
filename: '[name].js',
publicPath: process.env.NODE_ENV === 'production'
? projectConfig.build.assetsPublicPath
: projectConfig.dev.assetsPublicPath
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': getAbsolutePath('src'),
}
},
module: {
rules: [
{
test: /\.vue$/,
use: 'vue-loader'
},
{
test: /\.js$/,
use: 'babel-loader',
exclude: /node_modules/,
include: getAbsolutePath('src')
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10 * 1024
}
},
generator: {
filename: buildUtils.getAssetRelativePath('img/[name].[hash:7].[ext]')
}
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: buildUtils.getAssetRelativePath('media/[name].[hash:7].[ext]')
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: buildUtils.getAssetRelativePath('fonts/[name].[hash:7].[ext]'),
publicPath: '../../'
}
}
]
},
node: {
global: false
},
plugins: [new VueLoaderPlugin()]
}
utils.js
'use strict'
const path = require('path')
const projectConfig = require('../config')
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
const pkgMeta = require('../package.json')
exports.getAssetRelativePath = function (_path) {
const assetsSubDir = process.env.NODE_ENV === 'production'
? projectConfig.build.assetsSubDirectory
: projectConfig.dev.assetsSubDirectory
return path.posix.join(assetsSubDir, _path)
}
exports.cssLoaders = function (options = {}) {
const cssLoader = {
loader: 'css-loader',
options: { sourceMap: options.sourceMap }
}
const postcssLoader = {
loader: 'postcss-loader',
options: { sourceMap: options.sourceMap }
}
function buildLoaderChain(loaderName, loaderOpts = {}) {
const loaderList = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]
if (loaderName) {
loaderList.push({
loader: `${loaderName}-loader`,
options: { ...loaderOpts, sourceMap: options.sourceMap }
})
}
if (options.extract) {
return [
{
loader: MiniCssExtractPlugin.loader,
options: { publicPath: '../../' }
}
].concat(loaderList)
}
return [{ loader: 'vue-style-loader' }].concat(loaderList)
}
return {
css: buildLoaderChain(),
postcss: buildLoaderChain(),
less: buildLoaderChain('less')
}
}
exports.styleLoaders = function (options) {
const outputRules = []
const loaderMap = exports.cssLoaders(options)
for (const ext in loaderMap) {
outputRules.push({
test: new RegExp(`\\.${ext}$`),
use: loaderMap[ext]
})
}
return outputRules
}
exports.createNotificationHandler = () => {
const notifier = require('node-notifier')
return (severity, errors) => {
if (severity !== 'error') return
const error = errors[0]
const filePath = error.file?.split('!').pop()
notifier.notify({
title: pkgMeta.name,
message: `${severity}: ${error.name}`,
subtitle: filePath || '',
icon: path.join(__dirname, 'logo.png')
})
}
}
webpack.dev.conf.js
'use strict'
const buildUtils = require('./utils')
const webpack = require('webpack')
const projectConfig = require('../config')
const { merge } = require('webpack-merge')
const path = require('path')
const baseConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
const portfinder = require('portfinder')
const HOST = process.env.HOST
const PORT = process.env.PORT ? Number(process.env.PORT) : undefined
const devConfig = merge(baseConfig, {
mode: 'development',
module: {
rules: buildUtils.styleLoaders({ sourceMap: projectConfig.dev.cssSourceMap, usePostCSS: true })
},
devtool: projectConfig.dev.devtool,
devServer: {
clientLogLevel: 'warning',
historyApiFallback: {
rewrites: [
{ from: /.*/, to: path.posix.join(projectConfig.dev.assetsPublicPath, 'index.html') },
],
},
hot: true,
contentBase: false,
compress: true,
host: HOST || projectConfig.dev.host,
port: PORT || projectConfig.dev.port,
open: projectConfig.dev.autoOpenBrowser,
overlay: projectConfig.dev.errorOverlay ? { warnings: false, errors: true } : false,
publicPath: projectConfig.dev.assetsPublicPath,
proxy: projectConfig.dev.proxyTable,
quiet: true,
watchOptions: {
poll: projectConfig.dev.poll,
}
},
plugins: [
new webpack.DefinePlugin({
'process.env': require('../config/dev.env'),
BASEURL: '"//test.com"',
}),
new webpack.HotModuleReplacementPlugin(),
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
inject: true,
chunkSortMode: 'auto'
}),
new CopyWebpackPlugin({
patterns: [
{
from: path.resolve(__dirname, '../static'),
to: projectConfig.dev.assetsSubDirectory,
globOptions: {
dot: true,
gitignore: true,
ignore: ['.*'],
}
},
]
})
]
})
module.exports = new Promise((resolve, reject) => {
portfinder.basePort = process.env.PORT || projectConfig.dev.port
portfinder.getPort((err, availablePort) => {
if (err) return reject(err)
process.env.PORT = availablePort
devConfig.devServer.port = availablePort
devConfig.plugins.push(new FriendlyErrorsPlugin({
compilationSuccessInfo: {
messages: [`Application running at: http://${devConfig.devServer.host}:${availablePort}`],
},
onErrors: projectConfig.dev.notifyOnErrors
? buildUtils.createNotificationHandler()
: undefined
}))
resolve(devConfig)
})
})
webpack.prod.conf.js
'use strict'
const path = require('path')
const buildUtils = require('./utils')
const webpack = require('webpack')
const projectConfig = require('../config')
const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
const TerserWebpackPlugin = require('terser-webpack-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const buildEnv = require('../config/prod.env')
const runtimeEnvVars = process.env.BUILD_ENV === "production"
? { BASEURL: '"//test.com"' }
: { BASEURL: '"//test.com"' }
const prodConfig = merge(baseConfig, {
mode: 'production',
module: {
rules: buildUtils.styleLoaders({
sourceMap: projectConfig.build.productionSourceMap,
extract: true,
usePostCSS: true
})
},
devtool: projectConfig.build.productionSourceMap ? projectConfig.build.devtool : false,
output: {
path: projectConfig.build.assetsRoot,
filename: buildUtils.getAssetRelativePath('js/[name].[hash:7].js'),
chunkFilename: buildUtils.getAssetRelativePath('js/[name].[hash:7].js'),
clean: true
},
optimization: {
minimize: true,
minimizer: [new TerserWebpackPlugin(), new OptimizeCSSAssetsPlugin()],
runtimeChunk: { name: 'runtime' },
concatenateModules: true,
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'all',
priority: -10
},
asyncVendors: {
test: /[\\/]node_modules[\\/]/,
minChunks: 2,
chunks: 'async',
name: 'async-vendors'
}
},
},
moduleIds: 'deterministic'
},
plugins: [
new webpack.DefinePlugin({
'process.env': buildEnv,
...runtimeEnvVars,
}),
new MiniCssExtractPlugin({
filneame: buildUtils.getAssetRelativePath('css/[name].[contenthash].css'),
chunkFilename: buildUtils.getAssetRelativePath('css/[name].[contenthash].css')
}),
new HtmlWebpackPlugin({
filename: projectConfig.build.index,
template: 'index.html',
inject: true,
scriptLoading: 'blocking',
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
},
chunksSortMode: 'auto'
}),
new CopyWebpackPlugin({
patterns: [
{
from: path.resolve(__dirname, '../static'),
to: projectConfig.build.assetsSubDirectory,
globOptions: {
dot: true,
gitignore: true,
ignore: ['.*'],
}
},
]
})
]
})
if (projectConfig.build.productionGzip) {
const CompressionWebpackPlugin = require('compression-webpack-plugin')
prodConfig.plugins.push(
new CompressionWebpackPlugin({
filename: '[path][base].gz[query]',
algorithm: 'gzip',
test: /\.(js|css|json|txt|html|ico|svg)(\?.*)?$/i,
threshold: 10240,
minRatio: 0.8,
deleteOriginalAssets: false
})
)
}
if (projectConfig.build.bundleAnalyzerReport) {
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
prodConfig.plugins.push(new BundleAnalyzerPlugin())
}
module.exports = prodConfig