Webpack
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回调, 执行代码变更逻辑.

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
| Devtool | Build | Rebuild | Production | Quality |
|---|---|---|---|---|
| (none) / false | fastest | fastest | yes | bundle |
| eval | fast | fastest | no | generated |
| eval-cheap-source-map | ok | fast | no | transformed |
| eval-cheap-module-source-map | slow | fast | no | lines only |
| eval-source-map | slowest | ok | no | lines + 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 caching guide.
- Use
[contenthash]and long-term browser cache to improve second access time.
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.
- 优先加载 critical CSS in
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/resourceemits separate file and exports the URL (file-loader).asset/inlineexports data URI of the asset (url-loader).asset/sourceexports source code of the asset (raw-loader).assetautomatically chooses between exporting data URI and separate file (url-loaderwith asset size limit, default8kb).
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
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.
- 缓存:
- 持久化缓存.
babel缓存cacheDirectory: true.cache-loader.
- 多进程:
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/Nofbundle.js.
Code splitting methods:
require.ensure([], () => {});.- async/await
import. React.SuspenseandReact.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:
usedExportsOptimization: Remove unused export variables from modules, thereby further eliminating related side-effect-free statements. Inlib.js, variablebis unused, so related code is removed from the final output.sideEffectsOptimization: Remove modules from the module graph where export variables are not used. Inutil.js, no export variables are used and entire module are side-effect-free. soutil.jsmodule is removed from the final output.- DCE (Dead Code Elimination) Optimization:
Remove dead code by by general minification tools.
In
bootstrap.js, theconsole.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 thanexport default alls. - 使用支持
Tree Shaking的包:lodash-esorbabel-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
- UglifyJS Terser Plugin
- JavaScript Obfuscator
- Circular Dependency Plugin
- TypeScript React Components Properties Parser
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
Make sure there's no webpack deprecation warnings.
node --trace-deprecation node_modules/webpack/bin/webpack.js