Skip to main content

Webpack

Webpack Workflow

Webpack workflow:

Webpack Workflow

Webpack Configuration Intellisense

Enable webpack configuration types intellisense:

npm i -D webpack webpack-cli webpack-dev-server

Enable devServer type intellisense:

# Add `devServer` type to `webpack.Configuration`
npm i -D @types/webpack-dev-server
/** @type {import('webpack').Configuration} */
module.exports = {
entry: {
main: './src/index.ts',
},
output: {
filename: devMode ? '[name].js' : '[name].[contenthash].js',
path: path.resolve(__dirname, 'build'),
},
mode: devMode ? 'development' : 'production',
devServer: {
hot: true,
open: true,
port: 2333,
},
}

Webpack Hot Module Replacement

HMR:

  • 使用 WDS 托管静态资源, 同时以 Runtime 方式注入 HMR 客户端代码 (HMR Runtime).
  • 浏览器加载页面后, 与 WDS 建立 WebSocket 连接.
  • Webpack 监听到文件变化后, 增量构建发生变更的模块, 并通过 WebSocket 发送 hash 事件.
  • 浏览器接收到 hash 事件后, 请求 manifest ([hash].hot-update.json) 资源文件, 确认增量变更范围.
  • 浏览器加载发生变更的增量模块.
  • 浏览器中注入的 HMR Runtime 触发变更模块的 module.hot.accept 回调, 执行代码变更逻辑.

Hot Module Replacement

module.hot.accept 有两种调用模式:

  • 无参调用模式 module.hot.accept(): 当前文件修改后, 重头执行当前文件代码.
  • 回调调用模式 module.hot.accept(path, callback): 常用模式, 监听模块变更, 执行代码变更逻辑.
// 该模块修改后, `console.log('bar')` 会重新执行
console.log('bar')
module.hot.accept()
import component from './component'

let demoComponent = component()
document.body.appendChild(demoComponent)

if (module.hot) {
module.hot.accept('./component', () => {
const nextComponent = component()
document.body.replaceChild(nextComponent, demoComponent)
demoComponent = nextComponent
})
}

react-refresh-webpack-plugin/vue-loader/style-loader 利用 module.hot.accept 实现了 HMR (forceUpdate), 无需开发者编写热模块更新逻辑.

Webpack Watch Options

echo fs.notify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

Webpack Resolve Path Options

const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin')

module.exports = {
resolve: {
alias: {
'#': path.resolve(__dirname, '/'),
'~': path.resolve(__dirname, 'src'),
'@': path.resolve(__dirname, 'src'),
'~@': path.resolve(__dirname, 'src'),
'vendor': path.resolve(__dirname, 'src/vendor'),
'~component': path.resolve(__dirname, 'src/components'),
'~config': path.resolve(__dirname, 'config'),
},
extensions: ['.tsx', '.ts', '.jsx', '.js'],
plugins: [new TsconfigPathsPlugin({ configFile: './tsconfig.json' })],
},
}

get baseUrland paths from tsconfig.json:

const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin')

module.exports = {
resolve: {
plugins: [new TsconfigPathsPlugin({ configFile: './tsconfig.json' })],
},
}

jsconfig.json for vscode resolve path:

{
"compilerOptions": {
// This must be specified if "paths" is set
"baseUrl": ".",
// Relative to "baseUrl"
"paths": {
"*": ["*", "src/*"]
}
}
}
{
"compilerOptions": {
"target": "es2017",
"allowSyntheticDefaultImports": false,
"baseUrl": "./",
"paths": {
"Config/*": ["src/config/*"],
"Components/*": ["src/components/*"],
"Ducks/*": ["src/ducks/*"],
"Shared/*": ["src/shared/*"],
"App/*": ["src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

Webpack Flag Options

  • --progress
  • --colors
  • -p

Webpack Devtool Source Map Configuration

DevtoolBuildRebuildProductionQuality
(none) / falsefastestfastestyesbundle
evalfastfastestnogenerated
eval-cheap-source-mapokfastnotransformed
eval-cheap-module-source-mapslowfastnolines only
eval-source-mapslowestoknolines + rows

Webpack Cache Configuration

Webpack Build Cache

cache is set to type: 'memory' in development mode and disabled in production mode. cache: true is an alias to cache: { type: 'memory' }.

Accelerate second build time:

module.exports = {
cache: {
type: 'filesystem',
},
}

Webpack Browser Cache

Webpack Library Configuration

const path = require('node:path')

module.exports = {
entry: {
'bod-cli.min': path.join(__dirname, './src/index.js'),
'bod-cli': path.join(__dirname, './src/index.js'),
},
output: {
path: path.join(__dirname, './dist'),
filename: '[name].[contenthash].js',
library: 'bod',
libraryExport: 'default',
libraryTarget: 'esm',
globalObject: 'this',
},
}

Webpack Loader Configuration

Webpack Babel Loader

const config = {
test: /\.(js|mjs|jsx|ts|tsx)$/,
include: path.resolve('src'),
use: [
'thread-loader',
{
loader: require.resolve('babel-loader'),
},
],
options: {
customize: require.resolve('babel-preset-react-app/webpack-overrides'),
plugins: [
[
require.resolve('babel-plugin-named-asset-import'),
{
loaderMap: {
svg: {
ReactComponent: '@svgr/webpack?-svgo,+titleProp,+ref![path]',
},
},
},
],
['lodash'],
],
cacheDirectory: true,
cacheCompression: false,
compact: isEnvProduction,
},
}

Webpack CSS Loader

  • style-loader 将 CSS 动态注入到 DOM 中 (document.createElement('style')), 导致 DOM 重新渲染.
  • production 下需利用 Webpack 将 CSS 提前打包 (mini-css-extract-plugin):
    • 优先加载 critical CSS in <head>.
    • Lazy loading non-critical CSS.
    • Split up non-initial page CSS.
  • Next.js 不允许 :global(.global-class): modules.mode 设置为 pure.
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

const devMode = process.env.NODE_ENV !== 'production'

module.exports = {
module: {
rules: [
{
test: /.s?css$/,
exclude: /node_modules$/,
use: [
devMode ? 'style-loader' : MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
modules: {
compileType: 'module',
localIdentName: '[local]__[hash:base64:5]',
},
},
},
'sass-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [['autoprefixer']],
},
},
},
],
},
],
},
optimization: {
minimizer: [
// `...`,
new CssMinimizerPlugin(),
],
},
}

Webpack Static Assets Loader

  • ImageMin Loader
  • asset/resource emits separate file and exports the URL (file-loader).
  • asset/inline exports data URI of the asset (url-loader).
  • asset/source exports source code of the asset (raw-loader).
  • asset automatically chooses between exporting data URI and separate file (url-loader with asset size limit, default 8kb).
const config = {
rules: [
{
test: /\.(png|jpg|gif|jpeg|webp|svg|eot|ttf|woff|woff2)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 4 * 1024, // 4kb
},
},
},
],
}

Webpack Resource Assets

const path = require('node:path')

module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
assetModuleFilename: 'images/[hash][ext][query]',
},
module: {
rules: [
{
test: /\.png/,
type: 'asset/resource',
},
{
test: /\.html/,
type: 'asset/resource',
generator: {
filename: 'static/[hash][ext][query]',
},
},
],
},
}
import mainImage from './images/main.png'

img.src = mainImage // '/dist/151cfcfa1bd74779aadb.png'

Webpack Inline Assets

const path = require('node:path')
const svgToMiniDataURI = require('mini-svg-data-uri')

module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.svg/,
type: 'asset/inline',
generator: {
dataUrl: (content) => {
content = content.toString()
return svgToMiniDataURI(content)
},
},
},
],
},
}
import metroMap from './images/metro.svg'

block.style.background = `url(${metroMap})`
// => url(data:image/svg+xml;base64,PHN2ZyB4bW0iaHR0cDo...vc3ZnPgo=)

Webpack Source Assets

const path = require('node:path')

module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.txt/,
type: 'asset/source',
},
],
},
}
import exampleText from './example.txt'

block.textContent = exampleText // 'Hello world'

Webpack Thread Loader

const config = {
rules: [
{
loader: 'thread-loader',
// loaders with equal options will share worker pools
options: {
// the number of spawned workers, defaults to (number of cpus - 1) or
// fallback to 1 when require('os').cpus() is undefined
workers: 2,

// number of jobs a worker processes in parallel
// defaults to 20
workerParallelJobs: 50,

// additional node.js arguments
workerNodeArgs: ['--max-old-space-size=1024'],

// Allow to respawn a dead worker pool
// respawning slows down the entire compilation
// and should be set to false for development
poolRespawn: false,

// timeout for killing the worker processes when idle
// defaults to 500 (ms)
// can be set to Infinity for watching builds to keep workers alive
poolTimeout: 2000,

// number of jobs the poll distributes to the workers
// defaults to 200
// decrease of less efficient but more fair distribution
poolParallelJobs: 50,

// name of the pool
// can be used to create different pools with elseWise identical options
name: 'my-pool',
},
},
// your expensive loader (e.g. babel-loader)
],
}
const threadLoader = require('thread-loader')

threadLoader.warmup(
{
// pool options, like passed to loader options
// must match loader options to boot the correct pool
},
[
// modules to load
// can be any module, i. e.
'babel-loader',
'babel-preset-es2015',
'sass-loader',
]
)

Webpack Web Worker Loader

Worker Loader:

npm i -D worker-loader
module.exports = {
module: {
rules: [
{
test: /\.worker\.js$/,
use: { loader: 'worker-loader' },
},
],
},
}

Webpack Optimization

  • CDN.
  • 服务器端渲染.
  • 提取公共库.
  • 代码压缩.
  • 代码分割: Chunks.
  • 代码分割: 按需加载.
  • 优化构建速度:
    • 缩小文件搜索范围:
      • 优化 loader 配置: include/exclude.
      • 优化 module.noParse 配置: 忽略对部分没采用模块化的文件的递归解析处理.
      • 优化 resolve.modules 配置: 第三方模块.
      • 优化 resolve.alias 配置.
      • 优化 resolve.mainFields 配置.
      • 优化 resolve.extensions 配置: 后缀列表.
    • 减少打包文件:
      • 提取公共代码.
      • 动态链接 DllPlugin.
      • externals.
      • Tree shaking.
    • 缓存:
    • 多进程:
      • thread-loader.
  • JD Webpack optimization guide.

Common Libraries

{
"externals": {
"moment": "window.moment",
"antd": "window.antd",
"lodash": "window._",
"react": "window.React",
"react-dom": "window.ReactDOM"
}
}

Common Chunks

const config = new webpack.optimize.CommonsChunkPlugin({
name: string, // or
names: [string],
// The chunk name of the commons chunk.
// An existing chunk can be selected by passing a name of an existing chunk.
// If an array of strings is passed this is equal to
// invoking the plugin multiple times for each chunk name.
// If omitted and `options.async` or `options.children`
// is set all chunks are used, otherwise `options.filename`
// is used as chunk name.
// When using `options.async` to create common chunks
// from other async chunks you must specify an entry-point
// chunk name here instead of omitting the `option.name`.

filename: string,
// The filename template for the commons chunk.
// Can contain the same placeholders as `output.filename`.
// If omitted the original filename is not modified
// (usually `output.filename` or `output.chunkFilename`).
// This option is not permitted if you're using `options.async` as well,
// see below for more details.

minChunks: number | Number.POSITIVE_INFINITY | fn,
// (module, count) => boolean,
// The minimum number of chunks which need to contain a module
// before it's moved into the commons chunk.
// The number must be greater than or equal 2
// and lower than or equal to the number of chunks.
// Passing `Infinity` creates the commons chunk, but moves no modules into it.
// By providing a `function` you can add custom logic.
// (Defaults to the number of chunks)

chunks: [string],
// Select the source chunks by chunk names.
// The chunk must be a child of the commons chunk.
// If omitted all entry chunks are selected.

children: boolean,
// If `true` all children of the commons chunk are selected

deepChildren: boolean,
// If `true` all descendants of the commons chunk are selected

async: boolean | string,
// If `true` a new async commons chunk is created
// as child of `options.name` and sibling of `options.chunks`.
// It is loaded in parallel with `options.chunks`.
// Instead of using `option.filename`,
// it is possible to change the name of the output file by providing
// the desired string here instead of `true`.

minSize: number,
// Minimum size of all common module before a commons chunk is created.
})

Code Minimization

const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const TerserPlugin = require('terser-webpack-plugin')

const isEnvProduction = process.env.NODE_ENV === 'production'
const isEnvProductionProfile
= isEnvProduction && process.argv.includes('--profile')
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false'

module.exports = {
module: {
rules: [
{
test: /\.(js|mjs|jsx|ts|tsx)$/,
include: path.resolve('src'),
use: [
'thread-loader',
{
loader: require.resolve('babel-loader'),
},
],
},
{
test: /.s?css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
},
],
},
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
parallel: true,
terserOptions: {
parse: {
ecma: 8,
},
compress: {
ecma: 5,
warnings: false,
drop_console: true,
comparisons: false,
inline: 2,
},
mangle: {
safari10: true,
},
keep_classnames: isEnvProductionProfile,
keep_fnames: isEnvProductionProfile,
output: {
ecma: 5,
comments: false,
ascii_only: true,
},
},
}),
new CssMinimizerPlugin(),
],
},
}

Code Splitting

Huge bundle downside:

  • Cache invalid: one line code make whole cache invalid.
  • Useless code: only use 1/N of bundle.js.

Code splitting methods:

  • require.ensure([], () => {});.
  • async/await import.
  • React.Suspense and React.lazy.
  • Route-based code splitting.
  • vendor.[hash].chunk.js (document.createElement('script') promise): splitting vendor and application code is to enable long term caching techniques Since vendor code tends to change less often than the actual application code, browser will be able to cache them separately, and won't re-download them each time the app code changes.

Split chunks configuration:

  • chunks:
    • async: 只提取异步加载的模块出来打包到一个文件中.
    • initial: 提取同步加载和异步加载模块, 分别打包到不同的文件中.
    • all: 不管异步加载还是同步加载的模块都提取出来, 打包到一个文件中.
  • minSize: 超过 minSize 才会被提取.
  • maxSize: 超过 maxSize 会被进一步分割.
  • minChunks: 引用次数 >= minChunks 值才被提取.
  • maxAsyncRequests: 最大的按需 (异步) 加载次数 (default: 6).
  • maxInitialRequests: 入口文件加载最大数 (default: 4).
  • automaticNameDelimiter: 文件名分割符.
  • name: chunk 文件名.
  • cacheGroups: 配置提取模块的方案, 里面每一项代表一个提取模块的方案.
    • priority: 值越大优先级越大.
    • test: 匹配模块路径或名称.
    • reuseExistingChunk: true / false.
    • enforce: true / false.
module.exports = {
optimization: {
runtimeChunk: true,
splitChunks: {
chunks: 'async',
minSize: 30000,
maxSize: 200000,
minChunks: 1,
maxAsyncRequests: 6,
maxInitialRequests: 4,
automaticNameDelimiter: '-',
cacheGroups: {
vendors: {
name: 'chunk-vendors',
priority: -10,
chunks: 'initial',
test: /[\\/]node_modules[\\/]/,
},
common: {
name: 'chunk-common',
priority: -20,
chunks: 'initial',
minChunks: 2,
reuseExistingChunk: true,
},
element: {
name: 'element-ui',
priority: 0,
chunks: 'all',
test: /[\\/]element-ui[\\/]/,
},
api: {
name: 'api',
priority: 0,
test: /[\\/]api[\\/]/,
},
subApi: {
name: 'subApi',
priority: 10,
minChunks: 2,
test: /[\\/]api[\\/]subApi[\\/]/,
},
mixin: {
name: 'mixin',
priority: 0,
test: /[\\/]mixin[\\/]/,
},
},
},
},
}

Next.js granular chunking configuration:

module.exports = {
optimization: {
splitChunks: {
chunks: chunk => !/^polyfills|main|pages\/_app$/.test(chunk.name),
cacheGroups: {
framework: {
chunks: 'all',
name: 'framework',
test(module) {
const resource = module.nameForCondition?.()
return resource
? topLevelFrameworkPaths.some(pkgPath =>
resource.startsWith(pkgPath)
)
: false
},
priority: 40,
enforce: true,
},
lib: {
test(module: {
size: Function
nameForCondition: Function
}): boolean {
return (
module.size() > 160000
&& /node_modules[/\\]/.test(module.nameForCondition() || '')
)
},
name(module: {
type: string
libIdent?: Function
updateHash: (hash: crypto.Hash) => void
}): string {
const hash = crypto.createHash('sha1')
if (isModuleCSS(module)) {
module.updateHash(hash)
} else {
if (!module.libIdent) {
throw new Error(
`Encountered unknown module type: ${module.type}.`
)
}
hash.update(module.libIdent({ context: dir }))
}

return hash.digest('hex').substring(0, 8)
},
priority: 30,
minChunks: 1,
reuseExistingChunk: true,
},
},
maxInitialRequests: 25,
minSize: 20000,
},
},
}

Tree Shaking

Webpack tree shaking includes:

  • usedExports Optimization: Remove unused export variables from modules, thereby further eliminating related side-effect-free statements. In lib.js, variable b is unused, so related code is removed from the final output.
  • sideEffects Optimization: Remove modules from the module graph where export variables are not used. In util.js, no export variables are used and entire module are side-effect-free. so util.js module is removed from the final output.
  • DCE (Dead Code Elimination) Optimization: Remove dead code by by general minification tools. In bootstrap.js, the console.log('bad') statement will not execute, so related code is removed from the final output.
// index.js
import { a } from './lib'
import { c } from './util'
import './bootstrap'

console.log(a)

// lib.js
export const a = 1
export const b = 2

// util.js
export const c = 3
export const d = 4

// bootstrap.js
console.log('bootstrap')

if (false)
console.log('bad')
else
console.log('good')

Write tree-shakable code:

  • 避免无意义的赋值.
  • 尽量不写带有副作用的代码: 诸如编写了立即执行函数, 在函数里又使用了外部变量等.
  • 如果对 ES6 语义特性要求不是特别严格, 可以开启 babel 的 loose 模式 etc. 是否真的要不可枚举 class 的属性 (babel 将 Class 转化为 ES5 过程中会产生 Side Effect, 导致 Tree Shaking 失效).
  • 禁止 Babel 将模块导入导出语句转译成 CommonJS 形式.
    • @babel/preset-env: always { "modules": false }.
    • Babel 作为编译器不应该处理 modules 类型的转换.
    • Webpack 要依赖 esm 模块进行 tree shaking.
  • 如果是开发 JavaScript 库, 使用 rollup (ES6 module export + code flow static analysis), 并且提供 ES6 module 的版本, 入口文件地址设置到 package.json 的 module 字段.
  • 如果 JavaScript 库开发中, 难以避免的产生各种副作用代码, 可以将功能函数或者组件, 打包成单独的文件或目录, 以便于用户可以通过目录去加载. 如有条件, 也可为自己的库开发单独的 webpack-loader, 便于用户按需加载.
  • 优化导出粒度, 保持导出值颗粒度和原子性: export { foo, bar } better than export default alls.
  • 使用支持 Tree Shaking 的包: lodash-es or babel-plugin-lodash.

Building Caches

const config = new HardSourceWebpackPlugin({
// Either an absolute path or relative to webpack options.context.
cacheDirectory: 'node_modules/.cache/hard-source/[confighash]',
// Either a string of object hash function given a webpack config.
configHash: (webpackConfig) => {
// node-object-hash on npm can be used to build this.
return require('node-object-hash')({ sort: false }).hash(webpackConfig)
},
// Either false, a string, an object, or a project hashing function.
environmentHash: {
root: process.cwd(),
directories: [],
files: ['package-lock.json', 'yarn.lock'],
},
// An object.
info: {
// 'none' or 'test'.
mode: 'none',
// 'debug', 'log', 'info', 'warn', or 'error'.
level: 'debug',
},
// Clean up large, old caches automatically.
cachePrune: {
// Caches younger than `maxAge` are not considered for deletion. They must
// be at least this (default: 2 days) old in milliseconds.
maxAge: 2 * 24 * 60 * 60 * 1000,
// All caches together must be larger than `sizeThreshold` before any
// caches will be deleted. Together they must be at least this
// (default: 50 MB) big in bytes.
sizeThreshold: 50 * 1024 * 1024,
},
})

Webpack 5

const config = {
cache: {
type: 'memory',
},
}
const config = {
cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename],
},
},
}

Webpack Perf Profiling

const SpeedMeasurePlugin = require('speed-measure-webpack-plugin')

const smp = new SpeedMeasurePlugin()

const webpackConfig = smp.wrap({
plugins: [new MyPlugin(), new MyOtherPlugin()],
})
npx webpack --mode production --profile --json > stats.json

Commit Linter

{
"husky": {
"hooks": {
"commit-msg": "commitlint -e -V",
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"src/**/*.{js,jsx, ts, tsx}": ["eslint --fix", "git add"],
"src/**/*.{css, scss}": ["stylelint --fix", "git add"]
}
}

Webpack Plugins

Webpack HTML Plugins

Webpack JavaScript Plugins

Webpack CSS Plugins

Webpack Images Plugins

Webpack Building Work Plugins

Webpack Bundles UI Plugins

Webpack DLL Plugins

Webpack 5 support out of box cache.

Webpack Misc Plugins

  • PreLoad plugin
  • PreFetch plugin
  • Define Plugin
  • Provide Plugin
  • Webpack Merge

Webpack Custom Plugin

module.exports = {
plugins: [
function () {
this.hooks.done.tap('done', (stats) => {
if (
stats.compilation.errors
&& stats.compilation.errors.length
&& !process.argv.includes('--watch')
) {
// Process build errors.
process.exit(1)
}
})
},
],
}
const childProcess = require('node:child_process')
const HtmlWebpackPlugin = require('html-webpack-plugin')

const branch = childProcess
.execSync('git rev-parse --abbrev-ref HEAD')
.toString()
.replace(/\s+/, '')
const version = branch.split('/')[1]
const scripts = [
'https://cdn.bootcss.com/react-dom/16.9.0-rc.0/umd/react-dom.production.min.js',
'https://cdn.bootcss.com/react/16.9.0/umd/react.production.min.js',
]

class HotLoad {
apply(compiler) {
compiler.hooks.beforeRun.tap('UpdateVersion', (compilation) => {
compilation.options.output.publicPath = `./${version}/`
})

compiler.hooks.compilation.tap('HotLoadPlugin', (compilation) => {
HtmlWebpackPlugin.getHooks(compilation).alterAssetTags.tapAsync(
'HotLoadPlugin',
(data, cb) => {
scripts.forEach(src => [
data.assetTags.scripts.unshift({
tagName: 'script',
voidTag: false,
attributes: { src },
}),
])
cb(null, data)
}
)
})
}
}

module.exports = HotLoad

Typed webpack plugin from laravel-mix/:

const readline = require('node:readline')
const chalk = require('chalk')
const Table = require('cli-table3')
const _ = require('lodash')
const stripAnsi = require('strip-ansi')
const { formatSize } = require('webpack/lib/SizeFormatHelpers')
const { version } = require('../../package.json')

/**
* @typedef {object} BuildOutputOptions
* @property {boolean} clearConsole console cleared
* @property {boolean} showRelated show related
*/

/**
* @typedef {object} StatsAsset
* @property {string} name name
* @property {number} size size
* @property {StatsAsset[]|{}} related related
*/

/**
* @typedef {object} StatsData
* @property {StatsAsset[]} assets assets
*/

class BuildOutputPlugin {
/**
*
* @param {BuildOutputOptions} options
*/
constructor(options) {
this.options = options
this.patched = false
}

/**
* Apply the plugin.
*
* @param {import('webpack').Compiler} compiler
*/
apply(compiler) {
if (process.env.NODE_ENV === 'test')
return

compiler.hooks.done.tap('BuildOutputPlugin', (stats) => {
if (stats.hasErrors())
return false

if (this.options.clearConsole)
this.clearConsole()

const data = stats.toJson({
assets: true,
builtAt: true,
hash: true,
performance: true,
relatedAssets: this.options.showRelated,
})

this.heading(`Laravel Mix v${version}`)

console.log(
chalk.green.bold(`✔ Compiled Successfully in ${data.time}ms`)
)

if (data.assets.length)
console.log(this.statsTable(data))
})
}

/**
* Print a block section heading.
*
* @param {string} text
*/
heading(text) {
console.log()

console.log(chalk.bgBlue.white.bold(this.section(text)))

console.log()
}

/**
* Create a block section.
*
* @param {string} text
*/
section(text) {
const padLength = 3
const padding = ' '.repeat(padLength)

text = `${padding}${text}${padding}`

const line = ' '.repeat(text.length)

return `${line}\n${text}\n${line}`
}

/**
* Generate the stats table.
*
* @param {StatsData} data
* @returns {string} return
*/
statsTable(data) {
const assets = this.sortAssets(data)

const table = new Table({
head: [chalk.bold('File'), chalk.bold('Size')],
colWidths: [35],
colAligns: ['right'],
style: {
head: [],
compact: true,
},
})

for (const asset of assets)
table.push([chalk.green(asset.name), formatSize(asset.size)])

this.extendTableWidth(table)
this.monkeyPatchTruncate()

return table.toString()
}

/**
*
* @param {StatsData} data
*/
sortAssets(data) {
let assets = data.assets

assets = _.flatMap(assets, asset => [
asset,
...(Array.isArray(asset.related) ? asset.related : []),
])

assets = _.orderBy(assets, ['name', 'size'], ['asc', 'asc'])

return assets
}

/**
* Clear the entire screen.
*/
clearConsole() {
const blank = '\n'.repeat(process.stdout.rows)
console.log(blank)

readline.cursorTo(process.stdout, 0, 0)
readline.clearScreenDown(process.stdout)
}

/**
* Extend the width of the table
*
* Currently only increases the file column size
*
* @param {import('cli-table3').Table} table
* @param {number|null} targetWidth
* @param {number} maxWidth
*/
extendTableWidth(
table,
targetWidth = null,
maxWidth = Number.POSITIVE_INFINITY
) {
targetWidth = targetWidth === null ? process.stdout.columns : targetWidth

if (!targetWidth)
return

const tableWidth = this.calculateTableWidth(table)
const fileColIncrease = Math.min(
targetWidth - tableWidth,
maxWidth - tableWidth
)

if (fileColIncrease <= 0)
return

// @ts-expect-error Should error
table.options.colWidths[0] += fileColIncrease
}

monkeyPatchTruncate() {
if (this.patched)
return

this.patched = true

// @ts-expect-error Should error
const utils = require('cli-table3/src/utils')
const oldTruncate = utils.truncate

/**
*
* @param {string} str
* @param {number} desiredLength
* @param {string} truncateChar
*/
utils.truncate = (str, desiredLength, truncateChar) => {
if (stripAnsi(str).length > desiredLength)
str = `${str.substr(-desiredLength + 2)}`

return oldTruncate(str, desiredLength, truncateChar)
}
}

/**
* Calculate the width of the CLI Table
*
* `table.width` does not report the correct width
* because it includes ANSI control characters
*
* @internal
* @param {import('cli-table3').Table} table
*/
calculateTableWidth(table) {
const firstRow = table.toString().split('\n')[0]

return stripAnsi(firstRow).length
}
}

module.exports = BuildOutputPlugin

Webpack Migrate to 5

Migrate 5 Guide:

Make sure there's no webpack deprecation warnings.

node --trace-deprecation node_modules/webpack/bin/webpack.js

Webpack References