Printable

AutomaticPrefetchPlugin

The AutomaticPrefetchPlugin discovers all modules from the previous compilation upfront while watching for changes, trying to improve the incremental build times. Compared to PrefetchPlugin which discovers a single module upfront.

webpack.config.js

export default {
  // ...
  plugins: [new webpack.AutomaticPrefetchPlugin()],
};

BannerPlugin

Adds a banner to the top of each generated chunk.

import webpack from "webpack";

new webpack.BannerPlugin(banner);
// or
new webpack.BannerPlugin(options);

Options

{
  banner: string | function, // the banner as string or function, it will be wrapped in a comment
  raw: boolean, // if true, banner will not be wrapped in a comment
  entryOnly: boolean, // if true, the banner will only be added to the entry chunks
  test: string | RegExp | [string, RegExp], // Include all modules that pass test assertion.
  include: string | RegExp | [string, RegExp], // Include all modules matching any of these conditions.
  exclude: string | RegExp | [string, RegExp], // Exclude all modules matching any of these conditions.
  footer?: boolean, // if true, the banner will be placed at the end of the compilation
  stage?: number, // the stage of the compilation in which the banner should be injected
}

Usage

import webpack from "webpack";

// string
new webpack.BannerPlugin({
  banner: "hello world",
});

// function
new webpack.BannerPlugin({
  banner: (yourVariable) => `yourVariable: ${yourVariable}`,
});

// It will add the banner message after minimizers and any asset manipulation
new webpack.BannerPlugin({
  raw: true,
  banner: "/* banner is a string */",
  stage: webpack.Compilation.PROCESS_ASSETS_STAGE_REPORT,
});

Placeholders

Since webpack 2.5.0, placeholders are evaluated in the banner string:

import webpack from "webpack";

new webpack.BannerPlugin({
  banner:
    "fullhash:[fullhash], chunkhash:[chunkhash], name:[name], base:[base], query:[query], file:[file]",
});

ChunksWebpackPlugin

GitHub Workflow Status (branch) Coverage Status Npm downloads Ko-fi

The chunks-webpack-plugin creates HTML files with entry points and chunks relations to serve your webpack bundles. It is suitable with multi-page applications that contain multiple entry points.

Since webpack 4, SplitChunksPlugin offers the possibility to optimizes all chunks. It can be particularly powerful, because it means that chunks can be shared even between async and non-async chunks. See the webpack documentation of splitChunks.chunks for details.

splitChunks.chunks option can be set to automatically generate new chunks associated with an entry point. For example, entry points a.js and b.js share common code with the file vendors~a~b.js.

With multiple entry points, it can be difficult to identify relation between the auto-generated chunks and entry points.

chunks-webpack-plugin parses the webpack compilation entry points to get all files associated with the entry points. Then, it generates HTML files which include all assets filtered by an entry point and thechunks-manifest.json file.

Zero configuration

It works without configuration. For advanced usage, see the using configuration section.

Installation

chunks-webpack-plugin is available on npm as chunks-webpack-plugin and as chunks-webpack-plugin on GitHub.

npm install chunks-webpack-plugin --save-dev
yarn add chunks-webpack-plugin --dev

[!WARNING] Plugin chunks-webpack-plugin@10 is ESM only. [!NOTE] Minimum supported Node.js version is 16.20.0 and Webpack >=5.10.3.

Example

The project includes a minimalist example in the ./example directory. Run the npm run build:example command to execute the Webpack example and see the plugin's implementation in action.

Basic usage

chunks-webpack-plugin will generate two HTML files for each entry point. Each filename contains the entry point name, the {{entry}} placeholder is automatically replaced.

  • {{entry}}-styles.html: contains all HTML <link> tags
  • {{entry}}-scripts.html: contains all HTML <script> tags

First, let's add the plugin to the webpack configuration.

webpack.config.js

import ChunksWebpackPlugin from 'chunks-webpack-plugin';

export default {
  plugins: [new ChunksWebpackPlugin()]
};

HTML files are built in the output path directory with the rest of the webpack compilation.

Now you can include the generated HTML files into your HTML page templates. You can do it with e.g. Twig.

main-styles.html

<link rel="stylesheet" href="main.css" />

main-scripts.html

<script defer src="main.js"></script>

Using a configuration

You can pass a configuration object to chunks-webpack-plugin to override the default settings.

filename

Type:

type filename = string;

Default: '[name]-[type].html'

Tells the plugin whether to personalize the filename of the generated files. Files are processed by the webpack compilation and generated in the output path directory. The placeholder [name] is automatically replaced by entry points names and [type] by styles|scripts.

new ChunksWebpackPlugin({
  filename: 'templates/[name]-[type].html'
});

[!NOTE] The filename can contain directories, which will be created automatically.

templateStyle

Type:

type templateStyle = (name: string, entryName: string) => string;

Default:

(name) => `<link rel="stylesheet" href="${name}" />`;

Tells the plugin whether to personalize the default template for the HTML <style> tags. For example, add additional attributes or a CDN prefix.

export default {
  plugins: [
    new ChunksWebpackPlugin({
      templateStyle: (name) => `<link rel="stylesheet" href="https://cdn.domain.com${name}" />`
    })
  ]
};

templateScript

Type:

type templateScript = (name: string, entryName: string) => string;

Default:

(name) => `<script defer src="${name}"></script>`;

Tells the plugin whether to personalize the default template for the HTML <script> tags. For example, add additional attributes or a CDN prefix.

export default {
  plugins: [
    new ChunksWebpackPlugin({
      templateScript: (name) => `<script defer src="https://cdn.domain.com${name}"></script>`
    })
  ]
};

generateChunksManifest

Type:

type generateChunksManifest = boolean;

Default: false

Tells the plugin whether to generate the chunks-manifest.json. The file contains the list of all chunks grouped by entry points. See the chunks-manifest.json example.

export default {
  plugins: [
    new ChunksWebpackPlugin({
      generateChunksManifest: true
    })
  ]
};

generateChunksFiles

Type:

type generateChunksFiles = boolean;

Default: true

Tells the plugin whether to generate the HTML files.

export default {
  plugins: [
    new ChunksWebpackPlugin({
      generateChunksFiles: false
    })
  ]
};

[!WARNING] When set to false, HTML files will not be generated. It can only be useful together with generateChunksManifest option set to true for custom generation of the HTML files.


Multiple entrypoints example

Multiple entrypoints example

Example of the webpack configuration with multiple entry points which share common code with the splitChunks option.

import ChunksWebpackPlugin from 'chunks-webpack-plugin';
import path from 'node:path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export default {
  entry: {
    home: 'home.js',
    news: 'news.js'
  },
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, './dist')
  },
  plugins: [new ChunksWebpackPlugin()],
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  }
};

The plugin will generate all files in the output path directory:

home-styles.html

<link rel="stylesheet" href="vendors~home~news.css" />
<link rel="stylesheet" href="home.css" />

home-scripts.html

<script defer src="vendors~home~news.js"></script>
<script defer src="home.js"></script>

news-styles.html

<link rel="stylesheet" href="vendors~home~news.css" />
<link rel="stylesheet" href="news.css" />

news-scripts.html

<script defer src="vendors~home~news.js"></script>
<script defer src="news.js"></script>

License

chunks-webpack-plugin is licensed under the MIT License.

Created with ♥ by @yoriiis.

ContextExclusionPlugin

Context refers to dynamic expressions in import() or require() such as require('./locale/' + name + '.json').

The ContextExclusionPlugin allows you to exclude context. Provide RegExp as an argument when initializing the Plugin to exclude all context that matches it.

webpack.config.js

export default {
  plugins: [new webpack.ContextExclusionPlugin(/dont/)],
};

ContextReplacementPlugin

Context refers to dynamic expressions in import() or require() such as require('./locale/' + name + '.json'). When encountering such an expression, webpack infers the directory ('./locale/') and a regular expression (/^.*\.json$/). Since the name is not known at compile time, webpack includes every file as module in the bundle.

The ContextReplacementPlugin allows you to override the inferred information. There are various ways to configure the plugin:

Usage

new webpack.ContextReplacementPlugin(
  resourceRegExp: RegExp,
  newContentResource?: string,
  newContentRecursive?: boolean,
  newContentRegExp?: RegExp
)

If the resource (directory) matches resourceRegExp, the plugin replaces the default resource, recursive flag or generated regular expression with newContentResource, newContentRecursive or newContentRegExp respectively. If newContentResource is relative, it is resolved relative to the previous resource.

Here's a small example to restrict module usage:

new webpack.ContextReplacementPlugin(/moment[/\\]locale$/, /de|fr|hu/);

The moment/locale context is restricted to files matching /de|fr|hu/. Thus only those locales are included (see this issue for more information).

Content Callback

new webpack.ContextReplacementPlugin(
  resourceRegExp: RegExp,
  newContentCallback: (data) => void
);

The newContentCallback function is given a data object of the ContextModuleFactory and is expected to overwrite the request attribute of the supplied object.

Using this callback we can dynamically redirect requests to a new location:

new webpack.ContextReplacementPlugin(/^\.\/locale$/, (context) => {
  if (!/\/moment\//.test(context.context)) return;

  Object.assign(context, {
    regExp: /^\.\/\w+/,
    request: "../../locale", // resolved relatively
  });
});

Other Options

The newContentResource and newContentCreateContextMap parameters are also available:

new webpack.ContextReplacementPlugin(
  resourceRegExp: RegExp,
  newContentResource: string,
  newContentCreateContextMap: object // mapping runtime-request (userRequest) to compile-time-request (request)
);

These two parameters can be used together to redirect requests in a more targeted way. The newContentCreateContextMap allows you to map runtime requests to compile requests in the form of an object:

new ContextReplacementPlugin(/selector/, "./folder", {
  "./request": "./request",
  "./other-request": "./new-request",
});

CopyPlugin

Copies files and directories into output.path as part of the build. This is the plugin behind output.copy, and webpack applies it for you when that option is set — reach for the plugin directly when you need one of the two options output.copy does not carry, concurrency and stage.

5.111.0+
import webpack from "webpack";

new webpack.CopyPlugin(options);

Options

{
  patterns: (string | object)[], // what is copied, see output.copy
  concurrency?: number, // how many files are read at the same time
  stage?: number, // the processAssets stage the files are copied at
}
  • patterns ((string | object)[]): the files to copy. A pattern is a from plus context, filename, globOptions, info, preservePermissions, preserveTimestamps, to and transform — all documented under output.copy, which takes the same patterns.
  • concurrency (number = 100): the maximum number of files read at the same time.
  • stage (number = webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL): the processAssets stage the files are copied at. At the default stage a copied file is minimized and compressed like every other asset; a later stage emits it after the taps that would have done so.

Usage

import webpack from "webpack";

export default {
  // ...
  plugins: [
    new webpack.CopyPlugin({
      patterns: [
        "static",
        { from: "img", to: "images" },
        { from: "licenses/*.txt", to: "licenses" },
      ],
      concurrency: 50,
    }),
  ],
};

Keeping a file out of the minimizer

A prebuilt file is already minimized, and running the minimizer over it again costs time for nothing. Mark it as minimized in its asset info and the minimizer leaves it alone:

import webpack from "webpack";

export default {
  // ...
  plugins: [
    new webpack.CopyPlugin({
      patterns: [
        {
          from: "*.min.js",
          context: "vendor",
          to: "vendor",
          info: { minimized: true },
        },
      ],
    }),
  ],
};

Merging copied assets

One source file becomes one asset, so combining several into one is a second pass over what the copy emitted — which any plugin can do. Copied assets carry copied: true in their asset info, which is how you pick them out:

compilation.hooks.processAssets.tap(
  {
    name: "MergeCopiedAssetsPlugin",
    stage: webpack.Compilation.PROCESS_ASSETS_STAGE_DERIVED,
  },
  () => {
    const assets = compilation
      .getAssets()
      .filter((asset) => asset.info.copied && /^licenses\//.test(asset.name))
      .toSorted((a, b) => (a.name < b.name ? -1 : 1));
    if (assets.length === 0) return;

    const merged = assets
      .map((asset) => `/* ${asset.name} */\n${asset.source.source()}`)
      .join("\n");

    compilation.emitAsset(
      "THIRD_PARTY_LICENSES.txt",
      new webpack.sources.RawSource(merged),
      { copied: true },
    );
    for (const asset of assets) compilation.deleteAsset(asset.name);
  },
);

DefinePlugin

The DefinePlugin replaces variables in your code with other values or expressions at compile time. This can be useful for allowing different behavior between development builds and production builds. If you perform logging in your development build but not in the production build you might use a global constant to determine whether logging takes place. That's where DefinePlugin shines, set it and forget it rules for development and production builds.

new webpack.DefinePlugin({
  // Definitions...
});

Usage

Each key passed into DefinePlugin is an identifier or multiple identifiers joined with ..

  • If the value is a string it will be used as a code fragment.
  • If the value isn't a string, it will be stringified (including functions).
  • If the value is an object all keys are defined the same way.
  • If you prefix typeof to the key, it's only defined for typeof calls.

The values will be inlined into the code allowing a minification pass to remove the redundant conditional.

new webpack.DefinePlugin({
  PRODUCTION: JSON.stringify(true),
  VERSION: JSON.stringify("5fa3b9"),
  BROWSER_SUPPORTS_HTML5: true,
  TWO: "1+1",
  "typeof window": JSON.stringify("object"),
  "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
});
console.log(`Running App version ${VERSION}`);
if (!BROWSER_SUPPORTS_HTML5) require("html5shiv");
if (!PRODUCTION) {
  console.log("Debug info");
}

if (PRODUCTION) {
  console.log("Production log");
}

After passing through webpack with no minification results in:

if (!true) {
  console.log("Debug info");
}
if (true) {
  console.log("Production log");
}

and then after a minification pass results in:

console.log("Production log");

Feature Flags

Enable/disable features in production/development build using feature flags.

new webpack.DefinePlugin({
  NICE_FEATURE: JSON.stringify(true),
  EXPERIMENTAL_FEATURE: JSON.stringify(false),
});

Service URLs

Use a different service URL in production/development builds:

new webpack.DefinePlugin({
  SERVICE_URL: JSON.stringify("https://dev.example.com"),
});

Runtime values via runtimeValue

(fn: (context: { module: NormalModule, key: string, version: string | undefined }) => CodeValuePrimitive, options?: true | string[] | RuntimeValueOptions) => RuntimeValue

It is possible to define variables with values that rely on files and will be re-evaluated when such files change in the file system. This means webpack will rebuild when such watched files change.

There're two arguments for webpack.DefinePlugin.runtimeValue function:

  • The first argument is the generator function above: it receives { module, key, version } and returns the value to be assigned to the definition.
  • The second argument could either be an array of file paths to watch for or a true to flag the module as uncacheable. Since 5.26.0, it can also take an object argument with the following properties:
    • fileDependencies?: string[] A list of files the function depends on.
    • contextDependencies?: string[] A list of directories the function depends on.
    • missingDependencies?: string[] A list of not existing files the function depends on.
    • buildDependencies?: string[] A list of build dependencies the function depends on.
    • version?: string | () => string A version of the function.
import path from "node:path";
import { fileURLToPath } from "node:url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const fileDep = path.resolve(__dirname, "sample.txt");

new webpack.DefinePlugin({
  BUILT_AT: webpack.DefinePlugin.runtimeValue(Date.now, {
    fileDependencies: [fileDep],
  }),
});

The value of BUILT_AT would be the time at which the 'sample.txt' was last updated in the file system, e.g. 1597953013291.

DiagnosticsWebpackPlugin

npm node tests coverage discussion size

This plugin only supports webpack >= 5.106.0 and Node.js >= 22.12.0.

This plugin runs linters, type checkers and other diagnostic tools over your sources during the webpack build and reports what they find as webpack errors and warnings.

It replaces eslint-webpack-plugin and stylelint-webpack-plugin: one plugin, one place to configure how problems are reported, and one pass over your project. Today it runs ESLint, Stylelint, oxlint, Biome and TypeScript; more linters and diagnostic tools are meant to be added the same way.

Getting Started

To begin, you'll need to install diagnostics-webpack-plugin:

npm install diagnostics-webpack-plugin --save-dev

or

yarn add -D diagnostics-webpack-plugin

or

pnpm add -D diagnostics-webpack-plugin

[!NOTE]

Install the tools you want to run as well — the plugin only requires the ones you enable. It supports eslint >= 9, stylelint >= 17, oxlint >= 1, @biomejs/biome >= 2 and typescript >= 5:

npm install eslint stylelint oxlint @biomejs/biome typescript --save-dev

Then add the plugin to your webpack configuration and enable a check for each language you want inspected:

import DiagnosticsPlugin from "diagnostics-webpack-plugin";

export default {
  // ...
  plugins: [
    new DiagnosticsPlugin({
      checks: [
        { use: "eslint", extensions: ["js", "mjs"] },
        { use: "stylelint", extensions: ["css", "scss"] },
      ],
    }),
  ],
  // ...
};

The package ships an ECMAScript build next to a CommonJS one, so a CommonJS configuration works just as well:

const DiagnosticsPlugin = require("diagnostics-webpack-plugin");

Options

The plugin options have three layers:

LayerWhere it goesWhat it covers
PluginTop level onlyHow the plugin schedules its work, for every check at once.
SharedTop level or in a checks entryWhich files are linted and how problems are reported. An entry overrides what it sets.
CheckIn a checks entryOptions only that tool understands, plus everything its own Node.js API accepts.

The options are checked against their schema from webpack's own validate hook, so a mistake is reported when webpack validates the rest of your configuration, and validate: false turns the check off along with webpack's.

Every check to run is an entry in checks, named by its use. The list may name the same tool more than once, so one instance can inspect two file sets under different configurations.

Every option

OptionLayerWhat it decides
checksPluginWhich checks run, and the options only each of them understands.
contextPluginThe folder every relative files and exclude pattern is resolved against.
lintOnStartPluginWhether the first compilation checks everything it covers.
cacheSharedWhether the tool keeps a cache of its own between runs.
cacheLocationSharedWhere that cache is written.
excludeSharedWhat is left out.
extensionsSharedWhich extensions a named folder is walked for.
filesSharedWhat to check: naming it checks every file it matches, built or not.
fixSharedWhether the tool writes back what it can fix.
formatterSharedHow results are turned into the message that is reported.
outputReportSharedA file the same results are written to.
readsAcrossFilesSharedWhether a file's results can turn on another, so a rebuild lints importers.
reportAsSharedWhat the build carries: an error, a warning, a log line, or nothing.
resourceQueryExcludeSharedWhich module queries are left out.
threadsSharedHow wide the work is spread.
configType, eslintPathCheckESLint's own.
stylelintPathCheckStylelint's own.
oxlintPath, configFile, argsCheckoxlint's own.
biomePath, command, configFile, argsCheckBiome's own.
typescriptPath, configFile, compilerOptions, build, diagnosticOptions, ignoreDiagnosticsCheckTypeScript's own.

Anything else written in a checks entry is handed to the tool itself, so its own Node.js API options go next to these.

new DiagnosticsPlugin({
  // Plugin options
  context: "src",
  // Shared options, every check uses them unless it says otherwise
  reportAs: "warning", // report every check's results as warnings
  exclude: ["node_modules", "vendor"],
  // The checks to run, each with the options only it understands
  checks: [
    { use: "eslint", extensions: ["js", "ts"], fix: true },
    { use: "stylelint", extensions: ["css", "scss"], threads: true },
  ],
});

Plugin options

checks

  • Type:
type checks = (string | ({ use: string | CheckAdapter } & Options))[];
  • Default: none — this is the one option the plugin requires

The checks to run. An entry is the name of a built-in check, or an object naming it under use next to the options it takes, so these two are the same thing:

new DiagnosticsPlugin({ checks: ["eslint", { use: "eslint" }] });

Naming the same tool twice runs it twice, which is how one build checks two file sets under two configurations:

new DiagnosticsPlugin({
  checks: [
    { use: "eslint", files: "src", fix: true },
    { use: "eslint", files: "scripts", reportAs: "warning" },
  ],
});

use also takes an adapter of your own rather than a built-in name — see Adding a check.

context

  • Type:
type context = string;
  • Default: compiler.context

Base directory for linting. Every relative files and exclude pattern is resolved against it.

lintOnStart

  • Type:
type lintOnStart = boolean;
  • Default: true

Whether the first compilation lints every file it covers. Leave it alone and a build lints everything while a watch run lints everything once and then only what webpack rebuilds.

Set it to false to start a watch run quiet: nothing is linted until you touch a file, and only the modules webpack rebuilds are reported. A build is nothing but a first compilation, so it lints either way — the option cannot silence one.

Running the checks in one mode only

There is no option for this and none is needed: plugins ignores a falsy entry, so && decides whether the plugin is there at all. A configuration function is handed the --env values, and webpack-cli states the mode in them:

ValueSet by
WEBPACK_SERVEwebpack serve
WEBPACK_WATCHwebpack watch and webpack --watch
WEBPACK_BUILD, WEBPACK_BUNDLEwebpack, a build that runs once
import DiagnosticsPlugin from "diagnostics-webpack-plugin";
import { defineConfig } from "webpack";

export default defineConfig((env) => ({
  plugins: [
    // Both values are read because each command sets only its own: `serve` sets
    // `WEBPACK_SERVE` and `--watch` sets `WEBPACK_WATCH`, so asking for one of
    // them misses the other way of watching. Read `WEBPACK_BUILD` instead to
    // check a build that runs once and leave a watch run alone.
    (env.WEBPACK_SERVE || env.WEBPACK_WATCH) &&
      new DiagnosticsPlugin({ checks: ["eslint", "stylelint"] }),
  ],
}));

This is worth doing when something else already lints the same files — a CI job running eslint ., or an editor — and the release build need not pay for it twice.

Two configurations at once

A client and a server configuration lint the same sources twice. Where two compilers run a check configured the same way over the same files — the whole of it for a check reading from the file system, whatever they both built for one reading the module graph — the second joins the run the first is making rather than starting its own, and both compilations report what it finds and watch what it read.

Over three hundred files with the typescript check, two compilers build one program rather than two: 1.52 s rather than 1.79 s, and 248 MB rather than 298 MB. What a compiler checks alone it still checks alone, and a check configured differently in the second configuration runs on its own.

Shared options

These can be set at the top level, where they apply to every check, or inside one check, where they apply to that check alone.

cache

  • Type:
type cache = boolean;
  • Default: true

The cache is enabled by default to decrease execution time.

An ESLint check doing typed linting keeps its results in a cache of its own instead of ESLint's: a cache keyed on one file's own contents cannot answer for a rule that read another, so a file whose import changed would be reported from before the change. Each result is kept against the file and everything it imports, however indirectly, together with the TypeScript config, global declarations and the ESLint config it was linted under, and a later build — a watch rebuild or a fresh webpack run — reuses it while all of that is the same. It is written next to cacheLocation, as <cacheLocation>.typed-results.json. With cache: false, every build lints every file.

cacheLocation

  • Type:
type cacheLocation = string;
  • Default: node_modules/.cache/diagnostics-webpack-plugin/.<tool>cache, and the directory node_modules/.cache/diagnostics-webpack-plugin/typescript for the TypeScript check

Specify the path to the cache location. Can be a file or a directory.

files

  • Type:
type files = string | string[];
  • Default: options.context

Specify directories, files, or globs. Must be relative to options.context. Directories are traversed recursively looking for files matching options.extensions. File and glob patterns ignore options.extensions.

Naming them says what to check, so every file they match is checked whether or not webpack built it — a module nothing imports yet, or one reached through a loader webpack resolves differently, is checked all the same. Leave it unset and a check reads whatever it reads by itself: ESLint the modules webpack built, Stylelint a walk of the context.

In a watch run the folder a check takes its files from is watched, so a file you add there is checked without anything else having to change — and for the typescript check, so is every folder the tsconfig.json include covers. A folder is only watched when the whole of it can be: webpack rebuilds on a change anywhere under one, so a folder holding what the build writes (output.path) or what the check itself leaves out (exclude, node_modules by default) is left alone rather than turning every write into a rebuild. Pointing files at your sources rather than leaving it at the project root is what makes it watchable.

extensions

  • Type:
type extensions = string | string[];
  • Default: 'js' for ESLint, ['css', 'scss', 'sass'] for Stylelint

Specify file extensions that should be checked.

exclude

  • Type:
type exclude = string | string[];
  • Default: 'node_modules', plus output.path for Stylelint

Specify the files/directories to exclude. Must be relative to options.context.

threads

  • Type:
type threads = boolean | number | "auto";
  • Default: "auto"

How many threads a check spreads its work over. "auto" takes one fewer than the machine has, leaving webpack the thread it builds on; a number asks for that many; false, 0 or 1 keeps the work on webpack's own thread.

A check that threads its own work is asked to, and one that cannot is run in a pool of workers — so the option means the same thing whatever is being checked, and a check added later gets it for nothing. ESLint threads its own work from 9.34.0 under flat config; Stylelint threads none of its own, so it is pooled. TypeScript reads a program whole rather than a file at a time, so it is run on one worker: what a thread buys it is webpack's own thread back.

Prefer "auto" over a count. A check sizes "auto" against the machine, where a fixed number can end up competing with webpack for the same cores: over three hundred modules on four of them, "auto" took about a fifth off the build while asking for three was no better than asking for none. A count above what the machine has is held to it, since asking for sixteen threads on four cores took twice as long as asking for none — the ceiling cannot save a number written against the tool itself, such as ESLint's concurrency, which is passed through as written.

Anything written against the tool itself wins — ESLint's own concurrency, say — and a check configured with a function, such as a formatter written in the configuration, is run on webpack's thread, because a function cannot be handed to a worker.

readsAcrossFiles

  • Type:
type readsAcrossFiles = boolean;
  • Default: read from the tool's configuration

Whether what a check reports of one file can turn on another — a type the file imports, an import cycle through it. A rebuild otherwise lints only what webpack rebuilt and reports every other file from its last run, which is wrong for such a check: an edit to one file can change what is true of the files importing it. So a rebuild of a check that reads across files lints the files that changed and every file importing one of them, however indirectly, and keeps the rest.

Which file imports which is read with the project's own TypeScript, through the tsconfig.json nearest each file, so an import type counts though a bundler drops it before webpack sees it — and this holds for every check, whatever tool it runs. A change that could reach files some other way — a declaration file nothing imports, a declare global, a config file, an import that resolves to nothing (a stylesheet, an image or another asset webpack bundles aside) — lints every file again, as does a project without TypeScript. There, an ESLint check with cache: true written down keeps ESLint's own cache, which answers for a file whose import changed from before the change.

A file changed while webpack watches reaches its importers only when what it shows them changes: the declarations it emits and what it imports. Edit the body of a function or a comment and only that file is linted again — told apart by ts.transpileDeclaration (TypeScript 5.5 and later), which needs no type checker and so answers only for a file whose exports say their own types, and only from the second edit of a file on, the first being what it is told from. Over three hundred modules in a chain, editing one halfway along took a typed-linting rebuild from 1.1–1.4 s to 0.16–0.2 s.

Left out, each check reads it from its tool's configuration:

CheckReads across files when
ESLintparserOptions.project or projectService is set — typed linting
oxlint--type-aware, --type-check or --import-plugin, or the config turns them on
Biomealways: which rules read the project is Biome's to say from one release to the next
Stylelintnever
TypeScriptdoes not apply: the program is checked whole

Written down, it answers for the check whatever its configuration says — false for a Biome project using none of the rules that read other files, true for a check of your own that does not say:

new DiagnosticsPlugin({
  checks: [{ use: "biome", readsAcrossFiles: false }],
});

resourceQueryExclude

  • Type:
type resourceQueryExclude = RegExp | RegExp[] | string | string[];
  • Default: []

Specify the resource query to exclude. A string is read as the source of a regular expression, so "raw" and /raw/ mean the same thing. Only affects checks that read the module graph, such as ESLint.

fix

  • Type:
type fix = boolean;
  • Default: false

Will enable the autofix feature of the tool.

Be careful: this option will modify source files.

formatter

  • Type:
type formatter = string | ((results: LintResult[]) => string);
  • Default: the tool's own default formatter

Accepts the name of a formatter the tool ships, or a function that receives its results and returns the output as a string.

See the ESLint formatters and the Stylelint formatter option.

Errors and warnings

Every check reports its errors as webpack errors and its warnings as webpack warnings, which is what fails the build. reportAs overrides that.

reportAs

  • Type:
type reportAs = Severity | { errors?: Severity; warnings?: Severity };
type Severity = "error" | "warning" | "log" | false;
  • Default: unset — each result stays at the severity the check gave it

What a check reports its results as. One value covers its errors and its warnings alike; an object sets them apart, and a severity the object leaves out keeps its own:

ValueEffect
unsetErrors fail the build, warnings do not.
"error"Everything fails the build, warnings included.
"warning"Nothing fails the build; errors are reported as warnings.
"log"The terminal alone, through webpack's log.
falseNothing is reported. An outputReport is still written.
{ warnings: false }The errors alone, still failing the build.
{ warnings: "error" }Warnings fail the build too, and errors keep failing it.
{ errors: "warning" }Errors stop failing the build, and warnings stay warnings.
new DiagnosticsPlugin({
  reportAs: { warnings: false }, // the errors alone
  checks: [{ use: "eslint" }],
});

"log" is for a check nobody should be stopped by yet. Its results reach webpack's log — the terminal, and stats.logging — rather than compilation.errors or compilation.warnings, so the build carries neither, stats.hasWarnings() stays false and a dev server overlays nothing. An outputReport is written either way.

new DiagnosticsPlugin({
  // the whole project is linted, and only the new check is advisory
  checks: [{ use: "eslint" }, { use: "typescript", reportAs: "log" }],
});

A check nothing can be reported from is one a watch rebuild does not wait for: every reportAs that leaves both errors and warnings off the compilation — "log", false, or an object spelling both out — with no outputReport to write. The first build still waits, since it is what tells the watcher which files the check reads. From the rebuild on, the check is run once the build is over rather than beside it, and what it finds is printed through webpack's infrastructure log rather than through stats.logging.

The work is moved out of the rebuild rather than off the machine: an edit arriving while such a check is still running waits for the rest of it, since a check holds the thread the build runs on. A report a newer rebuild has already overtaken is dropped, and a check never has two of its runs going at once — the next starts when the last has reported.

ignoreDiagnostics

  • Type:
type ignoreDiagnostics =
  IgnoreOne | IgnoreOne[] | ((message: Message) => boolean);

type IgnoreOne = number | string | IgnoreMatch;

interface IgnoreMatch {
  file?: string | undefined;
  code?: string | undefined;
  severity?: "error" | "warning" | undefined;
}

interface Message {
  file?: string | undefined;
  code?: string | undefined;
  severity: "error" | "warning";
  text: string;
}
  • Default: unset

What not to report. A linter's rule can be turned off in its own configuration and a TypeScript diagnostic cannot, so this is where one thing a check found is left out whichever check found it — for adopting a check on a project that is not clean yet, without turning the check off altogether.

Written asLeaves out
2307The TypeScript code, as tsc prints it after TS.
"no-unused-vars"The rule or code a check names.
{ file, code, severity }What all three match; one left out matches everything.
[…]Anything the list matches.
(message) => booleanWhatever the function answers true for.
new DiagnosticsPlugin({
  checks: [
    // TS2307 is `Cannot find module`, TS7016 an untyped dependency
    { use: "typescript", ignoreDiagnostics: [2307, 7016] },
    // a rule that has not been dealt with in one folder yet
    {
      use: "eslint",
      ignoreDiagnostics: { code: "no-unused-vars", file: "src/legacy/**" },
    },
  ],
});

A file is a glob matched against the path relative to context, as exclude is. Something a check reported of no file in particular — a configuration it could not read — is not a file's to leave out, so a match naming one keeps it.

What is left out is left out of everything the check reports, an outputReport included, and of the counts a formatter prints. A check shipped outside this package answers for its own results, so it reads this option once it says how (see Adding a check).

outputReport

  • Type:
type outputReport =
  | boolean
  | {
      filePath?: string | undefined;
      formatter?: (string | ((results: LintResult[]) => string)) | undefined;
    };
  • Default: false

Write the results to a file, for example a checkstyle xml file for use for reporting on Jenkins CI.

  • filePath: path to the output report file, relative to output.path unless absolute.
  • formatter: a different formatter for the output file; the default/configured formatter is used when none is passed in.

Set at the top level, every check appends its report to the same file. Set it inside a checks entry to give that check a file of its own.

new DiagnosticsPlugin({
  checks: [
    {
      use: "eslint",
      outputReport: { filePath: "eslint.json", formatter: "json" },
    },
    {
      use: "stylelint",
      outputReport: { filePath: "stylelint.json", formatter: "json" },
    },
  ],
});

ESLint

Run with { use: "eslint" }. It lints the files webpack builds, so only the modules that end up in the bundle are checked.

Alongside the shared options you can pass any ESLint Node.js API option — they are handed to the ESLint class as they are.

threads reaches ESLint as its own concurrency from 9.34.0 under flat config, and an older ESLint or an eslintrc one is pooled instead. Typed linting — a configuration with parserOptions.project or projectService — is the exception: every thread builds the whole TypeScript program again, and from cold on each rebuild, so it is linted on webpack's thread, where the program stays warm. Write concurrency yourself and that is what ESLint is given, whatever threads says. Threaded by ESLint, a check is handed every file at once when webpack is done building them rather than in batches while it builds: ESLint starts its workers and loads the configuration in each of them for every run, which over 1,500 files took a build from 13 s and 45 s of CPU to 8.5 s and 17 s. Note that webpack spells this idea parallelism, and uses the word concurrency for something else again — bounded work on one thread.

A rebuild lints only the files webpack rebuilt and reports the rest from the previous run, so lintOnStart is only worth setting to start a watch run quiet.

Typed linting is the exception, and it is read from your configuration rather than asked of you. Where parserOptions.project or parserOptions.projectService is set, what a rule reports of one file can turn on another — a type it imported, a declaration file something regenerated — so a file that did not itself change is not one whose last result still holds. A rebuild then lints the files that changed and every file importing one of them, however indirectly — see readsAcrossFiles — and ESLint's own cache gives way to one keyed on everything a file imports, since ESLint's is keyed on each file's own contents and would answer for a type it never read. Over three hundred modules, an edit near the bottom of the import tree took a rebuild from 2.0 s to 0.35 s, and one halfway up to 1.2 s. Across separate runs, a thousand-file project that took 10 s every time now takes 4 s once nothing it imports has changed, as fast as eslint-webpack-plugin's cache that can go stale.

configType

  • Type:
type configType = "flat" | "eslintrc";
  • Default: flat

Specify the type of configuration to use with ESLint.

  • flat is the current standard configuration format.
  • eslintrc is the legacy configuration format and has been officially deprecated.

The new configuration format is explained in its own documentation.

eslintrc needs an ESLint that still reads one: ESLint 9 does, ESLint 10 removed it, and the option stays for as long as this plugin supports 9. The check loads ESLint the same way wherever it runs it, in the build's own thread and in a worker of a pool alike, and the ESLint 9 job in CI checks both.

eslintPath

  • Type:
type eslintPath = string;
  • Default: eslint

Path to the eslint instance that will be used for linting.

If the eslintPath is a folder like the official ESLint, or you specify a formatter option, you don't have to install eslint.

Suppressions

Bulk suppressions are supported: enable ESLint's own applySuppressions, and point suppressionsLocation at the file if it is not the default eslint-suppressions.json.

new DiagnosticsPlugin({
  checks: [{ use: "eslint", applySuppressions: true }],
});

[!IMPORTANT]

ESLint resolves the suppressions file, and every path recorded inside it, against its own cwd — not against the plugin's context. Where the two differ, pass cwd to the check as well:

new DiagnosticsPlugin({
  context: "src",
  checks: [
    { use: "eslint", applySuppressions: true, cwd: import.meta.dirname },
  ],
});

Suppressions need ESLint 9.24 or later. ESLint 10 takes both options itself; below that they reach its CLI alone, so the plugin applies the suppressions after linting instead — the same file, the same paths, the same result.

Stylelint

Run with { use: "stylelint" }, and requires stylelint >= 17. It lints every file matching files and extensions on disk, whether or not webpack imported it, so a stylesheet nothing imports yet is still checked.

Alongside the shared options you can pass any Stylelint option — they are handed to stylelint.lint() as they are.

stylelintPath

  • Type:
type stylelintPath = string;
  • Default: stylelint

Path to the stylelint instance that will be used for linting.

oxlint

Run with { use: "oxlint" }, and requires oxlint >= 1. It lints the files webpack builds, as the ESLint check does, and reads whatever .oxlintrc.json oxlint finds for itself.

new DiagnosticsPlugin({ checks: ["eslint", "oxlint"] });

oxlint is a binary behind a Node entry rather than a library, so this check runs it and reads the JSON it answers with. Two things follow. Its severities are oxlint's own — a rule set to "error" in .oxlintrc.json is reported as a webpack error and one set to "warn" as a warning, and reportAs moves them from there as it does for every check. And the options this check does not name are not guessed at: write them as args, the flags oxlint itself documents.

Type-aware rules and the import plugin — import/no-cycle reports a file for what the files it imports import — answer for a file from others, so where args or the config turns them on, a rebuild lints again every file a change reaches. See readsAcrossFiles.

oxlintPath

  • Type:
type oxlintPath = string;
  • Default: oxlint

Path to the oxlint instance that will be used for linting.

configFile

  • Type:
type configFile = string;
  • Default: unset, leaving oxlint to find its own

Path to the .oxlintrc.json to lint with.

args

  • Type:
type args = string[];
  • Default: []

Arguments passed to oxlint as they are, for the flags this check does not name — ["--deny", "correctness"], say. Not --format: the check asks for JSON and formats the results itself, and oxlint declines being asked twice.

Biome

Run with { use: "biome" }, and requires @biomejs/biome >= 2. It checks the files webpack builds and reads whatever biome.json Biome finds for itself, reporting at Biome's own severities.

new DiagnosticsPlugin({ checks: ["biome"] });

Like the oxlint check this runs a binary and reads the JSON it answers with, which Biome calls an experimental reporter — a release of its own may move the shape, and it is the only reporter carrying the severities this check needs.

Biome's project and types rules — noImportCycles, noFloatingPromises — answer for a file from the files it imports, and which rules those are is not something its config says, so a rebuild lints again every file a change reaches. A project using none of them can write readsAcrossFiles: false to lint only what webpack rebuilt.

biomePath

  • Type:
type biomePath = string;
  • Default: @biomejs/biome

Path to the @biomejs/biome instance that will be used for checking.

command

  • Type:
type command = "lint" | "check";
  • Default: "lint"

Which of Biome's commands to run. "lint" runs the linter; "check" adds its formatting and assist diagnostics, so a badly formatted file is reported as well as one breaking a rule.

configFile

  • Type:
type configFile = string;
  • Default: unset, leaving Biome to find its own

Path to the biome.json to check with.

args

  • Type:
type args = string[];
  • Default: []

Arguments passed to Biome as they are, for the flags this check does not name. Not --reporter: the check asks for JSON and formats the results itself.

TypeScript

Run with { use: "typescript" }, and requires typescript >= 5. It type checks the program a tsconfig.json describes, so every file that config includes is checked whether or not webpack built it, and a type error in a module nothing imports yet is still reported. Emit is forced off: webpack writes the output.

new DiagnosticsPlugin({
  checks: [{ use: "typescript", configFile: "tsconfig.build.json" }],
});

Project references are read rather than built unless build says otherwise, which is what a tsconfig.json listing only references needs.

Two things differ from the linters. A diagnostic belongs to the program rather than to one file, so there is nothing to report a single file from. And extensions only decides which files make the check run at all; what is checked is whatever the config file includes.

A program is read whole, so threads buys it webpack's own thread back rather than a share of the work: the check runs on one worker while webpack builds, and is read where it is reported. Over a three-hundred-file solution beside three hundred modules that took the build from 2149 ms to 1920 ms and the module graph from 2004 ms to 385 ms, and an edit-to-report in watch from 259 ms to 203 ms. A small project pays the thread's start without much to hide behind it; threads: false keeps the work where webpack is.

While webpack watches, the program is kept and handed to the build after it, so a rebuild type checks what the change reaches rather than the project over again — about 30 ms rather than 300 over three hundred files, for the memory the program holds on to (some 50 MB there). A change to the config file, or to the compiler options given here, starts a new one. A rebuild for a file the program neither reads nor would pick up — a stylesheet, an image — reports what the last run found without asking the program at all.

Between runs, what the program found is kept in the cache: a .tsbuildinfo for each config file under node_modules/.cache/diagnostics-webpack-plugin/typescript, which the first program of the next run starts from, checking again only the files whose content changed and what they reach. Over three hundred modules a second webpack run went from 4.7 s and 12 s of CPU to 2.0 s and 6 s. A mode that writes keeps to the config file's own incremental instead, and cache: false starts every run from nothing.

Alongside the shared options you can pass any compiler option — they override what the config file sets, as --strict would on the command line.

skipLibCheck is the one most worth knowing about. The check follows the config file, so unless it says otherwise every declaration file the program reads is type checked too — TypeScript's own lib.*.d.ts and every package's types. Over three hundred modules that was 2.3 s of a 3.2 s check; with skipLibCheck: true it was 0.55 s. fork-ts-checker-webpack-plugin turns it on whatever the config file says, which is most of why it looks faster on a first build:

new DiagnosticsPlugin({
  checks: [{ use: "typescript", skipLibCheck: true }],
});

typescriptPath

  • Type:
type typescriptPath = string;
  • Default: typescript

Path to the typescript instance that will be used for checking.

configFile

  • Type:
type configFile = string;
  • Default: the nearest tsconfig.json at or above context

Path to the tsconfig.json that describes the program.

compilerOptions

  • Type:
type compilerOptions = object;
  • Default: unset

Compiler options overriding the ones the config file sets. The same as writing them at the top level of the check, and useful when a name collides with one of the plugin's own.

build

  • Type:
type build = boolean;
  • Default: false

Build the projects the config file references before reporting, the way tsc -b does. A tsconfig.json that only lists references describes an empty program of its own, so without this a solution reports nothing at all; with it, every project in the graph is checked in the order its references say.

new DiagnosticsPlugin({
  checks: [{ use: "typescript", build: true }],
});

A reference is read through the declarations the referenced project publishes, so building one is how the next is checked at all — the build therefore writes each project's .d.ts and its .tsbuildinfo, and nothing else. The JavaScript stays webpack's to write: a later tsc -b of your own still emits it. mode says so in full, and can say otherwise.

The cost is a tsc -b pass rather than the kept program the check uses otherwise. Over five projects and 205 files: 573 ms with nothing built yet, 4–8 ms when the solution is up to date, 258 ms after one file changed, and around 200 ms on every rebuild for as long as an error stands, since a project that fails is never up to date. Pair it with reportAs: "log" if you would rather a watch rebuild did not wait for that.

mode

  • Type:
type mode = "readonly" | "write-tsbuildinfo" | "write-dts" | "write-references";
  • Default: "write-dts" under build, "readonly" otherwise

What the check writes as well as reports. A check reports, so by default a program writes nothing at all and a solution writes only what the next project has to read.

ModeWhat reaches the disk
readonlyNothing.
write-tsbuildinfoOnly .tsbuildinfo, which a later build resumes from.
write-dts.tsbuildinfo, .d.ts and .d.ts.map.
write-referencesEverything the compiler emits, the JavaScript included.
new DiagnosticsPlugin({
  checks: [{ use: "typescript", build: true, mode: "write-references" }],
});

readonly under build is not the compiler's noEmit, which tsc -b refuses with TS6310: the build runs and its output is kept where only that build can read it, so a project still finds the declarations of the one it references while your working tree is left alone.

diagnosticOptions

  • Type:
interface diagnosticOptions {
  syntactic?: boolean | undefined;
  semantic?: boolean | undefined;
  declaration?: boolean | undefined;
  global?: boolean | undefined;
}
  • Default: every kind reported

Which kinds of diagnostic to report. Turning one off asks TypeScript for less rather than dropping what it answered, so it is also the one filter that saves the work — semantic: false is what makes a check that only wants syntax errors cheap.

declaration is asked of a project compiled with declaration or composite, and a rebuild works it out again only for the files a change reached — the ones TypeScript type checks again — rather than emitting the declarations of the whole program each time: over 300 files, a rebuild went from 360–490 ms to 150 ms.

new DiagnosticsPlugin({
  // webpack's own parser reports a syntax error in a file it builds
  checks: [{ use: "typescript", diagnosticOptions: { syntactic: false } }],
});

What the tsconfig.json itself is wrong about is reported whatever is turned off here: with the config file misread, nothing below it would be answering the right question. build reports the whole of what it finds either way, since a solution is built rather than asked kind by kind.

fork-ts-checker-webpack-plugin spells this option the same way, with semantic alone on by default.

Making it faster

Most of what a check costs is its tool's, and a few settings outside the plugin decide how much of it is paid again. Each was measured on four cores.

skipLibCheck for the TypeScript check. Type checking every declaration file the program reads is most of a first build; see TypeScript.

cacheStrategy: "content" where CI restores the cache. ESLint keys its cache on each file's time and size by default, and a fresh checkout gives every file a new time, so a cache restored in CI answers for nothing. Keyed on what the files hold, it does:

new DiagnosticsPlugin({
  checks: [{ use: "eslint", cacheStrategy: "content" }],
});

Over 1,500 files, a build after touching every one of them took 9.9 s with the default and 5.9 s with content, the same as a build with nothing touched. Typed linting keeps its results against what files hold already.

NODE_COMPILE_CACHE for Node 22.1 and later. ESLint, typescript-eslint and TypeScript are a lot of JavaScript for Node to compile on every start; the environment variable keeps what it compiled on disk:

NODE_COMPILE_CACHE=node_modules/.cache/node-compile webpack

A typed-linting build of three hundred modules that found its results in the cache went from 2.34–2.60 s and 3.7–4.0 s of CPU to 2.18–2.20 s and 3.5–3.6 s.

Adding a check

A use may also be an adapter of its own rather than a built-in name, so a check can ship as its own package without an entry in this one:

new DiagnosticsPlugin({
  checks: [
    { use: require("diagnostics-webpack-plugin-typescript"), strict: true },
  ],
});

Such an adapter is an object with a name, and a create returning the five functions the plugin drives it through — what to lint, what came back, which results are errors and which warnings, how to format them, and what to release afterwards. It splits its results by their own severity and nothing else; reportAs is applied to what it returns:

module.exports = {
  name: "made-up",
  // "modules" lints the files webpack built, "glob" every file matching `files`
  filesSource: "glob",
  // Merged under the options the user passes, and under the shared options
  defaults: { extensions: ["ts"] },
  async create({ key, options, compilation }) {
    return {
      lintFiles: async (files) => runTheTool(files),
      getResults: async (results) => results,
      splitResults: (results) => ({ errors: results, warnings: [] }),
      getFormatter: async (formatter) => async (results) => format(results),
      cleanup: async () => {},
    };
  },
};

label, filesSource, defaults, defaultExclude and schema are optional; the plugin fills in the defaults of a module-scanning check that excludes node_modules.

An adapter whose results come per file says which file with resultPath(result), and a rebuild then lints only what changed. One whose tool reads other files to answer for one says so with readsAcrossFiles(compiler, options), and the plugin lints again every file a change reaches through its own import graph — or through the adapter's affectedFiles(compiler, files, changed), where it knows better. An adapter is handed files in batches while webpack builds, so that its tool starts early; one whose tool pays for every run it starts — workers, configuration — says lintsOnce(options) and is handed them all at once instead.

Migrating

Both plugins become one, and every option they had is still here. What changed is where an option is written and how the four that decided severity are spelled.

Where an option goes. context, lintOnStart and checks are the plugin's own and stay at the top level. Everything else is shared: write it at the top level to cover every check, or inside a checks entry to cover that one. configType, eslintPath and stylelintPath belong to a single check and go in its entry.

Severity is one option. emitError, emitWarning, failOnError, failOnWarning and quiet are reportAs, because reporting a result as a webpack error is what fails the build:

WasIs
quiet: true, emitWarning: falsereportAs: { warnings: false }
emitError: falsereportAs: { errors: false }
emitError: false and emitWarning: falsereportAs: false
failOnError: truethe default
failOnError: falsereportAs: "warning"
failOnWarning: truereportAs: { warnings: "error" }

The build is no longer aborted from inside the plugin. A result reported as a webpack error fails the build the way every other webpack error does — stats.hasErrors() is true and the CLI exits non-zero — and the assets are still written. Nothing about severity depends on mode any more.

Requirements. Node >= 22.12, webpack >= 5.106, and ESLint 9 or 10 / Stylelint 17 for whichever checks you run.

From eslint-webpack-plugin

-const ESLintPlugin = require("eslint-webpack-plugin");
+const DiagnosticsPlugin = require("diagnostics-webpack-plugin");

 module.exports = {
   plugins: [
-    new ESLintPlugin({ extensions: ["js"], fix: true }),
+    new DiagnosticsPlugin({
+      checks: [{ use: "eslint", extensions: ["js"], fix: true }],
+    }),
   ],
 };

Every option eslint-webpack-plugin accepted, and where it is now:

OptionNow
cacheUnchanged, shared.
cacheLocationUnchanged, shared. The default moved to node_modules/.cache/diagnostics-webpack-plugin/.eslintcache.
configTypeUnchanged, in the eslint entry.
contextUnchanged, top level.
emitErrorreportAs, see the table above.
emitWarningreportAs, see the table above.
eslintPathUnchanged, in the eslint entry.
excludeUnchanged, shared.
extensionsUnchanged, shared. Still defaults to js.
failOnErrorreportAs. It defaulted to on outside development mode; the default no longer depends on mode.
failOnWarningreportAs, see the table above.
filesUnchanged, shared.
fixUnchanged, shared.
formatterUnchanged, shared.
lintDirtyModulesOnlylintOnStart, inverted: lintDirtyModulesOnly: true is lintOnStart: false. Top level.
outputReportUnchanged, shared. It is still written even when reportAs is false.
quietreportAs: { warnings: false }.
resourceQueryExcludeUnchanged, shared.

Any other option is passed to ESLint itself, as before.

From stylelint-webpack-plugin

-const StylelintPlugin = require("stylelint-webpack-plugin");
+const DiagnosticsPlugin = require("diagnostics-webpack-plugin");

 module.exports = {
   plugins: [
-    new StylelintPlugin({ extensions: ["css"], threads: true }),
+    new DiagnosticsPlugin({
+      checks: [{ use: "stylelint", extensions: ["css"], threads: true }],
+    }),
   ],
 };

Every option stylelint-webpack-plugin accepted, and where it is now:

OptionNow
cacheUnchanged, shared.
cacheLocationUnchanged, shared. The default moved to node_modules/.cache/diagnostics-webpack-plugin/.stylelintcache.
contextUnchanged, top level.
emitErrorreportAs, see the table above.
emitWarningreportAs, see the table above.
excludeUnchanged, shared.
extensionsUnchanged, shared. Still defaults to css, scss and sass.
failOnErrorreportAs. It defaulted to on in every mode, and the default is still to fail on an error.
failOnWarningreportAs, see the table above.
filesUnchanged, shared.
formatterUnchanged, shared.
lintDirtyModulesOnlylintOnStart, inverted: lintDirtyModulesOnly: true is lintOnStart: false. Top level.
outputReportUnchanged, shared. It is still written even when reportAs is false.
quietreportAs: { warnings: false }.
stylelintPathUnchanged, in the stylelint entry.
threadsShared now, and "auto" by default rather than off. Every check honours it.

Any other option is passed to Stylelint itself, as before. Two more things changed for Stylelint alone:

  • Stylelint 17 or later is required. stylelint-webpack-plugin accepted 13 through 17; the merged plugin drops the older majors rather than carrying their compatibility branches forward. Stylelint 17 itself needs Node >= 20.19.
  • Errors and warnings are no longer swapped. failOnError: false used to report errors as webpack warnings, and failOnWarning: true to report warnings as webpack errors. Each result now keeps its own severity unless reportAs says otherwise — which is what those two spellings in the table above do, explicitly.

fix is a documented option now rather than one passed through to Stylelint unnamed. resourceQueryExclude is shared but has no effect here: it reads the query of a module webpack built, and Stylelint is given the files matching files instead.

Running both

The two plugins become one instance, and options they had in common are written once:

 module.exports = {
   plugins: [
-    new ESLintPlugin({ context: "src", failOnError: true, extensions: ["js"] }),
-    new StylelintPlugin({ context: "src", failOnError: true, extensions: ["css"] }),
+    new DiagnosticsPlugin({
+      context: "src",
+      checks: [
+        { use: "eslint", extensions: ["js"] },
+        { use: "stylelint", extensions: ["css"] },
+      ],
+    }),
   ],
 };

From fork-ts-checker-webpack-plugin

The typescript check does what that plugin does — type check the program a tsconfig.json describes, rather than the files webpack happens to build — and is written as one entry in checks:

-const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin");
+const DiagnosticsPlugin = require("diagnostics-webpack-plugin");

 module.exports = {
   plugins: [
-    new ForkTsCheckerWebpackPlugin({
-      typescript: { configFile: "tsconfig.build.json", build: true },
-    }),
+    new DiagnosticsPlugin({
+      checks: [
+        { use: "typescript", configFile: "tsconfig.build.json", build: true },
+      ],
+    }),
   ],
 };

The check runs on a worker thread rather than in a process of its own. That plugin forks one, which is why it has a memoryLimit and a profile; a thread shares the build's heap and its copy of TypeScript, so neither is needed here. The program is kept between rebuilds either way. What that costs and saves is under the TypeScript check.

Over a generated 300-file project, three runs of each:

BuildEdit to report, watchingPeak memory, process tree
fork-ts-checker-webpack-plugin@9.1.02232 ms138 ms429–488 MB
Here2227 ms131 ms269–301 MB

The time is the same within the run-to-run spread; the memory is not, and that is the thread rather than anything clever.

It is a drop-in, and was checked as one against a project neither of us wrote — the fork-ts-checker-webpack-plugin example that ts-loader ships, which is React and TSX through ts-loader. Swapping the plugin for { use: "typescript" } and nothing else reported the same TS2322 at the same line and column. Three things to expect while doing it:

  • webpack must be 5.106 or newer, which is what the plugin requires at all.
  • npm refuses to install alongside ESLint 8. The eslint peer here is optional but ranged ^9 || ^10, and npm resolves an optional peer it can see, so a project pinned to 8 needs its ESLint updated — or --legacy-peer-deps — even when no ESLint check is configured.
  • fork-ts-checker-notifier-webpack-plugin has nothing to listen to. It taps that plugin's own hooks, so it goes when that plugin does.
fork-ts-checker-webpack-pluginHere
asyncNot an option: the check runs on a worker thread while webpack builds (threads), and a check the compilation carries nothing of — reportAs "log" or false, with no outputReport — is run after a watch rebuild rather than during it.
typescript.configFileconfigFile, in the entry — see the TypeScript check.
typescript.contextcontext, the plugin's own.
typescript.buildbuild.
typescript.configOverwritecompilerOptions, or any compiler option written at the top of the entry.
typescript.typescriptPathtypescriptPath.
typescript.modemode. The same four values, and the same meaning; the default under build is write-dts rather than write-tsbuildinfo, because a referenced project is read through the declarations it publishes.
typescript.memoryLimit, profileNothing to set — the check is not a forked process.
skipLibCheck: true, forcedWhatever the config file says. Write skipLibCheck: true in the entry to skip type checking declaration files the way that plugin does.
typescript.diagnosticOptionsdiagnosticOptions, spelled the same way. Every kind is reported here unless you turn one off; there, only semantic is on to begin with.
issue.include, issue.excludeignoreDiagnostics, which takes the same match of a file, a code and a severity, or a function of your own. It says what to leave out, so an include is written as the exclude of everything else.
formatterformatter. Unset, TypeScript's own formatter is used, with color and the source line.
loggerWebpack's logger is what the plugin writes to; reportAs: "log" is what sends results there rather than onto the compilation.
devServerNothing to set: results reach the compilation, so a dev server overlays them, and reportAs: "log" keeps them off it.

Running it next to a linter is one plugin rather than two, with one place to say how what they find is reported:

new DiagnosticsPlugin({
  checks: [{ use: "eslint" }, { use: "typescript" }],
});

Changelog

Changelog

Contributing

We welcome all contributions!

If you're new here, please take a moment to review our contributing guidelines.

CONTRIBUTING

License

MIT

DllPlugin

The DllPlugin and DllReferencePlugin provide means to split bundles in a way that can drastically improve build time performance. The term "DLL" stands for Dynamic-link library which was originally introduced by Microsoft.

DllPlugin

This plugin is used in a separate webpack configuration exclusively to create a dll-only-bundle. It creates a manifest.json file, which is used by the DllReferencePlugin to map dependencies.

  • context (optional): context of requests in the manifest file (defaults to the webpack context.)
  • format (boolean = false): If true, manifest json file (output) will be formatted.
  • name: name of the exposed dll function (TemplatePaths: [fullhash], [chunkhash], [contenthash], & [name] )
  • path: absolute path to the manifest json file (output)
  • entryOnly (boolean = true): if true, only entry points will be exposed
  • type: type of the dll bundle
new webpack.DllPlugin(options);

Creates a manifest.json which is written to the given path. It contains mappings from require and import requests to module ids. It is used by the DllReferencePlugin.

Combine this plugin with output.library option to expose (aka, put into the global scope) the dll function.

DllReferencePlugin

This plugin is used in the primary webpack config, it references the dll-only-bundle(s) to require pre-built dependencies.

  • context: (absolute path) context of requests in the manifest (or content property)
  • extensions: Extensions used to resolve modules in the dll bundle (only used when using 'scope').
  • manifest : an object containing content and name or a string to the absolute path of the JSON manifest to be loaded upon compilation
  • content (optional): the mappings from request to module id (defaults to manifest.content)
  • name (optional): an identifier where the dll is exposed (defaults to manifest.name) (see also externals)
  • scope (optional): prefix which is used for accessing the content of the dll
  • sourceType (optional): how the dll is exposed (libraryTarget)
new webpack.DllReferencePlugin(options);

References a dll manifest file to map dependency names to module ids, then requires them as needed using the internal __webpack_require__ function.

Modes

This plugin can be used in two different modes, scoped and mapped.

Scoped Mode

The content of the dll is accessible under a module prefix. i.e. with scope = 'xyz' a file abc in the dll can be access via require('xyz/abc').

Mapped Mode

The content of the dll is mapped to the current directory. If a required file matches a file in the dll (after resolving), then the file from the dll is used instead.

Because this happens after resolving every file in the dll bundle, the same paths must be available for the consumer of the dll bundle. i.e. if the dll contains lodash and the file abc, import _ from "lodash"/require('lodash') and import abs from "./abc"/require('./abc') will be used from the dll, rather than building them into the main bundle.

Usage

webpack.vendor.config.js

import path from "node:path";
import { fileURLToPath } from "node:url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

new webpack.DllPlugin({
  context: import.meta.dirname,
  name: "[name]_[fullhash]",
  path: path.join(__dirname, "manifest.json"),
});

webpack.app.config.js

import path from "node:path";
import { fileURLToPath } from "node:url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

new webpack.DllReferencePlugin({
  context: __dirname,
  manifest: path.join(__dirname, "manifest.json"),
  scope: "xyz",
  sourceType: "commonjs2",
});

Examples

Vendor and User

Two separate example folders. Demonstrates scope and context.

References

Source

Tests

EnvironmentPlugin

The EnvironmentPlugin is shorthand for using the DefinePlugin on process.env keys.

Usage

The EnvironmentPlugin accepts either an array of keys or an object mapping its keys to their default values.

new webpack.EnvironmentPlugin(["NODE_ENV", "DEBUG"]);

This is equivalent to the following DefinePlugin application:

new webpack.DefinePlugin({
  "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
  "process.env.DEBUG": JSON.stringify(process.env.DEBUG),
  // since webpack 5.103.0
  "import.meta.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
  "import.meta.env.DEBUG": JSON.stringify(process.env.DEBUG),
});

Usage with default values

Alternatively, the EnvironmentPlugin supports an object, which maps keys to their default values. The default value for a key is taken if the key is undefined in process.env.

new webpack.EnvironmentPlugin({
  NODE_ENV: "development", // use 'development' unless process.env.NODE_ENV is defined
  DEBUG: false,
});

Example:

Let's investigate the result when running the previous EnvironmentPlugin configuration on a test file entry.js:

if (process.env.NODE_ENV === "production") {
  console.log("Welcome to production");
}
if (process.env.DEBUG) {
  console.log("Debugging output");
}

When executing NODE_ENV=production webpack in the terminal to build, entry.js becomes this:

if ("production" === "production") {
  // <-- 'production' from NODE_ENV is taken
  console.log("Welcome to production");
}
if (false) {
  // <-- default value is taken
  console.log("Debugging output");
}

Running DEBUG=false webpack yields:

if ("development" === "production") {
  // <-- default value is taken
  console.log("Welcome to production");
}
if ("false") {
  // <-- 'false' from DEBUG is taken
  console.log("Debugging output");
}

Use Case: Git Version

The following EnvironmentPlugin configuration provides process.env.GIT_VERSION (such as "v5.4.0-2-g25139f57f") and process.env.GIT_AUTHOR_DATE (such as "2020-11-04T12:25:16+01:00") corresponding to the last Git commit of the repository:

import childProcess from "node:child_process";

function git(command) {
  return childProcess.execSync(`git ${command}`, { encoding: "utf8" }).trim();
}

new webpack.EnvironmentPlugin({
  GIT_VERSION: git("describe --always"),
  GIT_AUTHOR_DATE: git("log -1 --format=%aI"),
});

DotenvPlugin

The third-party DotenvPlugin (dotenv-webpack) allows you to expose (a subset of) dotenv variables:

// .env
DB_HOST=127.0.0.1
DB_PASS=foobar
S3_API=mysecretkey
new Dotenv({
  path: "./.env", // Path to .env file (this is the default)
  safe: true, // load .env.example (defaults to "false" which does not use dotenv-safe)
});

EvalSourceMapDevToolPlugin

This plugin enables more fine grained control of source map generation. It is also enabled automatically by certain settings of the devtool configuration option.

new webpack.EvalSourceMapDevToolPlugin(options);

This plugin wraps every module in an eval() call with its source map appended as a data URL, so no .map files are emitted. It accepts the same options object as the SourceMapDevToolPlugin, but the options that describe a separate source map file — filename, fileContext and publicPath — have nothing to name here and are ignored, as is a function-valued append.

Options

The following options are supported:

type Matcher =
  | string
  | RegExp
  | ((name: string) => boolean)
  | (string | RegExp | ((name: string) => boolean))[];

A string matches by prefix, a RegExp by test, and a function is called with the name and returns whether it matches.

  • test (Matcher): Generate source maps for modules whose path matches the given value. There is no default: all modules get source maps unless test, include or exclude restrict them. It matches the module's own path, not the name of the chunk it ends up in, so it cannot be used to exclude a whole entry point or bundle.
  • include (Matcher): Include source maps for module paths that match the given value.
  • exclude (Matcher): Exclude modules that match the given value from source map generation.
  • append (string): Appends the given value to the original asset. Usually the #sourceMappingURL comment. [url] is replaced with a URL to the source map file.
  • ignoreList (Matcher): Decide whether to ignore source files that match the specified value in source maps.
  • module (boolean): Indicates whether loaders should generate source maps (defaults to true).
  • moduleFilenameTemplate (string): See output.devtoolModuleFilenameTemplate.
  • columns (boolean): Indicates whether column mappings should be used (defaults to true).
  • namespace (string): Namespace prefix to allow multiple webpack roots in the devtools. See output.devtoolNamespace.
  • noSources = false (boolean): Prevents the source file content from being included in the source map.
  • sourceRoot (string): Provide a custom value for the sourceRoot property in source maps.
  • debugIds (boolean): If true, unique ids will be emitted in source and sourcemaps which streamlines identifying sourcemaps across different builds. See the TC39 sourcemap debug ID proposal for more details.

Examples

The following examples demonstrate some common use cases for this plugin.

Basic Use Case

You can use the following code to replace the configuration option devtool: eval-source-map with an equivalent custom plugin configuration:

export default {
  // ...
  devtool: false,
  plugins: [new webpack.EvalSourceMapDevToolPlugin({})],
};

Exclude Vendor Maps

The following code would exclude source maps for any module coming from node_modules:

new webpack.EvalSourceMapDevToolPlugin({
  exclude: /node_modules/,
});

HashedModuleIdsPlugin

This plugin will cause hashes to be based on the relative path of the module, generating a four character string as the module id.

new webpack.ids.HashedModuleIdsPlugin({
  // Options...
});

Options

This plugin supports the following options:

  • context: The context directory (absolute path) for creating names.

  • hashFunction: The hashing algorithm to use, defaults to 'md4'. All functions from Node.JS' crypto.createHash are supported.

  • hashDigest: The encoding to use when generating the hash, defaults to 'base64'. All encodings from Node.JS' hash.digest are supported.

    5.102.1+

    In addition to standard Node.js encodings, this plugin also supports 'base64url', which is useful for generating URL-safe hash values. The custom bases ('base26', 'base32', 'base36', 'base49', 'base52', 'base58', 'base62') are only available for output.hashDigest.

  • hashDigestLength: The prefix length of the hash digest to use, defaults to 4. Note that some generated ids might be longer than specified here, to avoid module id collisions.

Usage

Here's an example of how this plugin might be used:

import path from "node:path";
import { fileURLToPath } from "node:url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

new webpack.ids.HashedModuleIdsPlugin({
  context: __dirname,
  hashFunction: "sha256",
  hashDigest: "hex",
  hashDigestLength: 20,
});

Using URL-safe encoding (5.102.1+):

import path from "node:path";
import { fileURLToPath } from "node:url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

new webpack.ids.HashedModuleIdsPlugin({
  context: __dirname,
  hashFunction: "md4",
  hashDigest: "base64url", // URL-safe encoding
  hashDigestLength: 8,
});

HotModuleReplacementPlugin

Enables Hot Module Replacement, otherwise known as HMR.

Basic Usage

Enabling HMR is straightforward; the plugin takes no options.

new webpack.HotModuleReplacementPlugin();

IgnorePlugin

IgnorePlugin prevents the generation of modules for import or require calls matching the regular expressions or filter functions.

Using regular expressions

  • resourceRegExp: A RegExp to test the resource against.
  • contextRegExp: (optional) A RegExp to test the context (directory) against.
new webpack.IgnorePlugin({ resourceRegExp, contextRegExp });

Using filter functions

  • checkResource (resource, context) A Filter function that receives resource and context as arguments, must return boolean.
new webpack.IgnorePlugin({
  checkResource(resource) {
    // do something with resource
    return true | false;
  },
});

Example of ignoring Moment Locales

As of moment 2.18, all locales are bundled together with the core library (see this GitHub issue).

The resourceRegExp parameter passed to IgnorePlugin is not tested against the resolved file names or absolute module names being imported or required, but rather against the string passed to require or import within the source code where the import is taking place. For example, if you're trying to exclude node_modules/moment/locale/*.js, this won't work:

-new webpack.IgnorePlugin({ resourceRegExp: /moment\/locale\// });

Rather, because moment imports with this code:

import(`./locale/${name}`);

// or

require(`./locale/${name}`);

...your first regexp must match that './locale/' string. The second contextRegExp parameter is then used to select specific directories from where the import took place. The following will cause those locale files to be ignored:

new webpack.IgnorePlugin({
  resourceRegExp: /^\.\/locale$/,
  contextRegExp: /moment$/,
});

...which means "any require statement matching './locale' from any directories ending with 'moment' will be ignored.

Internal webpack plugins

This is a list of plugins which are used by webpack internally.

Categories of internal plugins:

environment

Plugins affecting the environment of the compiler.

NodeEnvironmentPlugin

webpack.node.NodeEnvironmentPlugin()

Applies Node.js style filesystem to the compiler.

compiler

Plugins affecting the compiler

MemoryCachePlugin

MemoryCachePlugin()

Adds a cache to the compiler, where modules are cached in memory.

ProgressPlugin

ProgressPlugin(handler)

Hook into the compiler to extract progress information. The handler must have the signature (percentage: number, message: string) => void. Percentage is called with a value between 0 and 1, where 0 indicates the start and 1 the end.

RecordIdsPlugin

RecordIdsPlugin()

Saves and restores module and chunk ids from records.

entry

Plugins, which add entry chunks to the compilation.

EntryPlugin

EntryPlugin(context, entry, options)

Adds an entry chunk on compilation. The chunk is named options.name and contains only one module (plus dependencies). The module is resolved from entry in context (absolute path).

PrefetchPlugin

PrefetchPlugin(context, request)

Prefetches request and dependencies to enable a more parallel compilation. It doesn't create any chunk. The module is resolved from request in context (absolute path).

output

JsonpTemplatePlugin

web/JsonpTemplatePlugin()

Chunks are wrapped into JSONP-calls. A loading algorithm is included in entry chunks. It loads chunks by adding a <script> tag.

It takes no options and sets output.chunkLoading to 'jsonp'. It is configured via the output options:

output.chunkLoadingGlobal is the JSONP function.

output.publicPath is used as path for loading the chunks.

output.chunkFilename is the filename under that chunks are expected.

NodeTemplatePlugin

node/NodeTemplatePlugin(options)

Chunks are wrapped into Node.js modules exporting the bundled modules. The entry chunks loads chunks by requiring them.

options.asyncChunkLoading (boolean) selects 'async-node' instead of 'require' as output.chunkLoading.

LibraryTemplatePlugin

LibraryTemplatePlugin(name, target)

The entries chunks are decorated to form a library name of type type.

WebWorkerTemplatePlugin

webworker/WebWorkerTemplatePlugin()

Chunks are loaded by importScripts. Else it's similar to JsonpTemplatePlugin.

It takes no options and sets output.chunkLoading to 'import-scripts'.

EvalDevToolModulePlugin

EvalDevToolModulePlugin(options)

Decorates the module template by wrapping each module in a eval annotated with a //# sourceURL= comment (options.sourceUrlComment, defaults to //# sourceURL=[url]).

SourceMapDevToolPlugin

SourceMapDevToolPlugin(options)

Decorates the templates by generating a SourceMap for each chunk.

options.filename the filename template of the SourceMap. [fullhash], [name], [id], [file] and [base] are replaced. If this option is missing, the SourceMap will be inlined as DataUrl.

options.append, options.moduleFilenameTemplate, options.fallbackModuleFilenameTemplate and the remaining options are described on the SourceMapDevToolPlugin page.

HotModuleReplacementPlugin

HotModuleReplacementPlugin()

Add support for hot module replacement. Decorates the templates to add runtime code. Adds module.hot API.

It takes no options and reads the following output options:

output.hotUpdateChunkFilename the filename for hot update chunks.

output.hotUpdateMainFilename the filename for the hot update manifest.

output.hotUpdateGlobal the global function name for loading hot updates.

source

Plugins affecting the source code of modules.

APIPlugin

Makes the __webpack_* variables (__webpack_public_path__, __webpack_require__, __webpack_modules__, __webpack_chunk_load__, __webpack_hash__, __webpack_nonce__, ...) accessible in modules and maps require.onError to the runtime's error handler (__webpack_require__.oe).

CompatibilityPlugin

Ensures compatibility with other module loaders by rewriting legacy patterns: browserify-style require delegators, user code that declares its own __webpack_require__, and hashbang lines.

ConstPlugin

Tries to evaluate expressions in if (...) statements and ternaries to replace them with true/false for further possible dead branch elimination using hooks fired by the parser.

There are multiple optimizations in production mode regarding dead branches:

  • The ones performed by Terser
  • The ones performed by webpack

Webpack will try to evaluate conditional statements. If it succeeds then the dead branch is removed. Webpack can't do constant folding unless the compiler knows it. For example:

import { calculateTax } from "./tax";

const FOO = 1;
if (FOO === 0) {
  // dead branch
  calculateTax();
}

In the above example, webpack is unable to prune the branch, but Terser does. However, if FOO is defined using DefinePlugin, webpack will succeed.

It is important to mention that import { calculateTax } from './tax'; will also get pruned because calculateTax() call was in the dead branch and got eliminated.

ProvidePlugin

ProvidePlugin(definitions)

For each name: request entry in definitions, if name is used in a module it is filled by a module loaded by import ... from <request>/import(<request>)/require(<request>).

NodeStuffPlugin

NodeStuffPlugin(options)

Provide stuff that is normally available in Node.js modules.

It also ensures that module is filled with some Node.js stuff if you use it.

RequireJsStuffPlugin

Provide stuff that is normally available in require.js.

require[js].config is removed. require.version is 0.0.0. requirejs.onError is mapped to require.onError.

NodeSourcePlugin

node/NodeSourcePlugin()

This plugin is a no-op kept for backward compatibility. webpack 5 no longer polyfills Node.js core modules or globals such as process and Buffer.

NodeTargetPlugin

node/NodeTargetPlugin()

The plugins should be used if you run the bundle in a Node.js environment.

If ensures that native modules are loaded correctly even if bundled.

AMDPlugin

dependencies/AMDPlugin(options)

Provides AMD-style define and require to modules. Also bind require.amd, define.amd and webpack_amd_options## to the options passed as parameter.

CommonJsPlugin

dependencies/CommonJsPlugin

Provides CommonJs-style require to modules.

RequireContextPlugin

dependencies/RequireContextPlugin()

Provides require.context. Alternative requests for files are derived from the configured resolve options (modules, extensions, mainFiles, enforceExtension).

RequireEnsurePlugin

dependencies/RequireEnsurePlugin()

Provides require.ensure.

RequireIncludePlugin

dependencies/RequireIncludePlugin()

Provides require.include.

DefinePlugin

DefinePlugin(definitions)

Define constants for identifier.

definitions is an object.

optimize

Note that all plugins under webpack.optimize namespace should only be used when mode set to 'none'. Otherwise you might get into trouble where plugins are applied twice.

LimitChunkCountPlugin

optimize/LimitChunkCountPlugin(options)

Merge chunks limit chunk count is lower than options.maxChunks.

options.chunkOverhead and options.entryChunkMultiplicator are still accepted but no longer have any effect.

Chunks that reduce the total size the most are merged first. If multiple combinations are equal the minimal merged size wins.

MergeDuplicateChunksPlugin

optimize/MergeDuplicateChunksPlugin()

Chunks with the same modules are merged.

RemoveEmptyChunksPlugin

optimize/RemoveEmptyChunksPlugin()

Chunks that contain no modules (and are neither runtime nor entry chunks) are removed.

MinChunkSizePlugin

optimize/MinChunkSizePlugin({ minChunkSize })

Merges chunks until each chunk has the minimum size of minChunkSize.

ModuleConcatenationPlugin

See the ModuleConcatenationPlugin page for details.

FlagIncludedChunksPlugin

optimize/FlagIncludedChunksPlugin()

Adds chunk ids of chunks which are included in the chunk. This eliminates unnecessary chunk loads.

RealContentHashPlugin

optimize/RealContentHashPlugin()

When optimization.realContentHash option is enabled, webpack will apply RealContentHashPlugin to compiler internally.

Hook

RealContentHashPlugin provides a updateHash 5.8.0+ hook for customizing hash updating:

import webpack from "webpack";

const { RealContentHashPlugin } = webpack.optimize;
// ...
compiler.hooks.compilation.tap("MyPlugin", (compilation) => {
  const hooks = RealContentHashPlugin.getCompilationHooks(compilation);
  hooks.updateHash.tap("MyPlugin", (content, oldHash) => {
    // you can calculate the hash here as you wish
  });
});

LimitChunkCountPlugin

While writing your code, you may have already added many code split points to load stuff on demand. After compiling you might notice that some chunks are too small - creating larger HTTP overhead. LimitChunkCountPlugin can post-process your chunks by merging them.

new webpack.optimize.LimitChunkCountPlugin({
  // Options...
});

Options

The following options are supported:

maxChunks

number

Limit the maximum number of chunks using a value greater than or equal to 1. Using 1 will prevent any additional chunks from being added as the entry/main chunk is also included in the count.

webpack.config.js

import webpack from "webpack";

export default {
  // ...
  plugins: [
    new webpack.optimize.LimitChunkCountPlugin({
      maxChunks: 5,
    }),
  ],
};

minChunkSize

Keeping chunk size above the specified limit is no longer a feature of this plugin. Use MinChunkSizePlugin instead.

ManifestPlugin

5.103.0+

The built-in ManifestPlugin generates one asset manifest for your build. Port from WebpackManifestPlugin.

webpack.config.js

new webpack.ManifestPlugin({
  // options...
});

Options

entrypoints

boolean = true

Enables generation of the entrypoints section in the manifest.

When entrypoints is enabled and one entry uses dependOn

webpack.config.js

export default {
  entry: {
    entry1: {
      import: "./src/file_1.js",
      dependOn: "entry2",
    },
    entry2: "./src/file_2.js",
  },
  plugins: [
    new webpack.ManifestPlugin({
      filename: "manifest.json",
    }),
  ],
};

The resulting manifest will contain the parent/child relationship map

manifest.json

{
  "entrypoints": {
    "entry1": {
      "imports": ["entry1.js"],
      "parents": ["entry2"]
    },
    "entry2": {
      "imports": ["entry2.js"]
    }
  }
}

filename

string = manifest.json

Specifies the filename of the output file on disk. By default the plugin will emit manifest.json inside the output.path directory.

filter

(item: ManifestItem) => boolean

Allows filtering the files which make up the manifest.

generate

(manifest: ManifestObject) => ManifestObject

Receives the manifest object, modifies it, and returns the modified manifest.

prefix

string = '[publicpath]'

Specifies a path prefix for all keys in the manifest.

serialize

(manifest: ManifestObject) => string = (manifest) => JSON.stringify(manifest, null, 2)

Receives the manifest object and returns the manifest string.

Use serialize to output other formats, for example YAML:

new webpack.ManifestPlugin({
  serialize(manifest) {
    return YAML.stringify(manifest, 4);
  },
});

Plugins

Webpack has a rich plugin interface. Most of the features within webpack itself use this plugin interface. This makes webpack flexible.

The plugins below are grouped by what they do. Most ship with webpack itself and are used as new webpack.SomePlugin(); the ones whose page lives under a *-webpack-plugin name are separate packages to install first.

Sources and modules

NameDescription
DefinePluginAllow global constants configured at compile time
EnvironmentPluginShorthand for using the DefinePlugin on process.env keys
ProvidePluginUse modules without having to use import/require
IgnorePluginExclude certain modules from bundles
NormalModuleReplacementPluginReplace resource(s) that matches a regexp
ContextReplacementPluginOverride the inferred context of a require expression
ContextExclusionPluginExclude matching contexts so webpack does not generate modules for them
VirtualUrlPluginCreate modules whose content is generated at build time, with no file on disk

Output and assets

NameDescription
BannerPluginAdd a banner to the top of each generated chunk
CopyPluginCopy files and directories into the output directory
ManifestPluginGenerate an asset manifest mapping source filenames to emitted output files
ChunksWebpackPluginCreate HTML files with entrypoints and chunks relations to serve your bundles
SvgChunkWebpackPluginGenerate SVG sprites optimized by SVGO based on your entry point dependencies

Chunks and optimization

NameDescription
SplitChunksPluginExtract shared and vendor code into separate chunks — configured through optimization.splitChunks
ModuleConcatenationPluginScope hoisting — on by default in production via optimization.concatenateModules
MergeDuplicateChunksPluginMerge chunks that contain the same modules
LimitChunkCountPluginLimit the maximum number of chunks by merging them
MinChunkSizePluginKeep chunk size above the specified limit
MinimizerPluginMinify the JS/CSS/HTML/JSON/etc in your project, and compress what it emits
HashedModuleIdsPluginHash-based module IDs — use optimization.moduleIds: 'deterministic' instead, which is the production default
NoEmitOnErrorsPluginSkip the emitting phase when there are compilation errors
AutomaticPrefetchPluginResolve all modules from the previous compilation upfront to speed up watch rebuilds
PrefetchPluginResolve and build a module ahead of its first import to shorten the critical build path
DllPluginSplit bundles into a pre-built vendor bundle — largely replaced by the filesystem cache

Module Federation

NameDescription
ModuleFederationPluginShare modules between independently built applications at runtime

Development and diagnostics

NameDescription
HotModuleReplacementPluginEnable Hot Module Replacement (HMR)
SourceMapDevToolPluginEnables a more fine grained control of source maps
EvalSourceMapDevToolPluginEnables a more fine grained control of eval source maps
ProgressPluginReport compilation progress
ProfilingPluginRecord plugin execution timings to a Chrome profile file
WatchIgnorePluginIgnore matching paths in watch mode so they do not trigger rebuilds
DiagnosticsWebpackPluginRun linters and other diagnostic tools over your sources during the build

For more third-party plugins, see the list from awesome-webpack.

MergeDuplicateChunksPlugin

Merges chunks that contain the same modules. This plugin is enabled by default as part of webpack's internal optimizations. Exporting it publicly allows developers to configure the stage at which merging occurs — useful when chunk merging needs to run after SplitChunksPlugin.

new webpack.optimize.MergeDuplicateChunksPlugin({
  // Options...
});

Options

stage

number

Controls the optimization stage at which duplicate chunk merging runs. By default, merging occurs before SplitChunksPlugin. Setting a stage greater than 10 (the stage of SplitChunksPlugin) causes merging to run after it; a value of 10 still runs before it because user plugins are applied first. The built-in instance (stage -10) stays active, so this adds a second merging pass.

webpack.config.js

import webpack from "webpack";

export default {
  // ...
  plugins: [
    new webpack.optimize.MergeDuplicateChunksPlugin({
      stage: 11, // run after SplitChunksPlugin (stage 10)
    }),
  ],
};

MinChunkSizePlugin

Keep chunk size above the specified limit by merging chunks that are smaller than the minChunkSize.

new webpack.optimize.MinChunkSizePlugin({
  minChunkSize: 10000, // Minimum number of characters
});

MinimizerWebpackPlugin

npm node tests cover discussion size

This plugin minifies your assets in a webpack build. It ships with several built-in minimizers covering JavaScript, JSON, HTML, CSS, and images — pick one with the minify option and target the right files with test.

JavaScript minimizers:

  • terser — MinimizerPlugin.terserMinify (default). The same JavaScript-based minifier that webpack uses out of the box; produces small, well-tested output and supports the full set of extractComments modes.
  • uglify-js — MinimizerPlugin.uglifyJsMinify. ES5-only minifier, useful when you specifically need UglifyJS-compatible output. Requires npm install --save-dev uglify-js.
  • @swc/core — MinimizerPlugin.swcMinify. A very fast Rust-based JavaScript/TypeScript minifier. Requires npm install --save-dev @swc/core.
  • esbuild — MinimizerPlugin.esbuildMinify. An extremely fast JS bundler/minifier; legal comments are always preserved (no extractComments support). Requires npm install --save-dev esbuild.

JSON minimizer:

  • JSON.stringify — MinimizerPlugin.jsonMinify. Built in (no extra dependency); supports space and replacer options.

HTML minimizers:

  • webpack's own — webpack.html.syntax.htmlMinify. Ships with webpack (>= 5.111.0), so it needs no extra dependency, and it is the one that minifies what a document nests: an inline <style>, every style="", a <script>, an <svg> subtree. See webpack's own minimizers.
  • html-minifier-terser — MinimizerPlugin.htmlMinifierTerser. JavaScript-based, no native dependency. Requires npm install --save-dev html-minifier-terser.
  • @swc/html — MinimizerPlugin.swcMinifyHtml (full HTML documents) and MinimizerPlugin.swcMinifyHtmlFragment (HTML fragments, e.g. <template> content). Very fast Rust-based platform for the Web. Requires npm install --save-dev @swc/html.
  • @minify-html/node — MinimizerPlugin.minifyHtmlNode. A Rust HTML minifier optimised for speed and effectiveness. Requires npm install --save-dev @minify-html/node.

CSS minimizers:

  • webpack's own — webpack.css.syntax.cssMinify. Ships with webpack (>= 5.111.0), so it needs no extra dependency, and it minifies the CSS a stylesheet or a document nests. See webpack's own minimizers.
  • cssnano — MinimizerPlugin.cssnanoMinify. Built on top of PostCSS. Requires npm install --save-dev cssnano postcss.
  • csso — MinimizerPlugin.cssoMinify. A CSS minifier with structural optimisations. Requires npm install --save-dev csso.
  • clean-css — MinimizerPlugin.cleanCssMinify. A widely-used CSS optimiser. Requires npm install --save-dev clean-css.
  • esbuild — MinimizerPlugin.esbuildMinifyCss. Very fast CSS minification using esbuild's CSS loader. Requires npm install --save-dev esbuild.
  • lightningcss — MinimizerPlugin.lightningCssMinify. A Rust-based CSS parser, transformer, and minifier. Requires npm install --save-dev lightningcss.
  • @swc/css — MinimizerPlugin.swcMinifyCss. A very fast Rust-based CSS minifier. Requires npm install --save-dev @swc/css.

Image minimizers:

  • sharp — MinimizerPlugin.sharpMinify. Re-encodes an image as the format its name already claims (avif, gif, heif, jp2, jpeg, png, tiff, webp), and can resize, rotate, flip, grayscale, blur or sharpen on the way — from the options or from the asset's own name. Requires npm install --save-dev sharp.
  • svgo — MinimizerPlugin.svgoMinify. Minifies SVG, including an asset/inline SVG that no test can match. Requires npm install --save-dev svgo.
  • imagemin — MinimizerPlugin.imageminMinify. Runs the imagemin plugins you name. Requires npm install --save-dev imagemin plus each plugin. Pair it with MinimizerPlugin.imageminGenerate under generate when a plugin converts the format, since only that can rename the asset.
  • @napi-rs/image — MinimizerPlugin.napiRsImageMinify. Rust codecs with no system dependency; recompresses png losslessly with oxipng and jpeg with mozjpeg, and can resize, turn, mirror, grayscale, invert or blur on the way — from the options or from the asset's own name. Requires npm install --save-dev @napi-rs/image.

These only minify — they never change an image's format or name; see Images.

Transport encodings:

  • zlib and anything shaped like it — MinimizerPlugin.compress. Compresses an asset so a server can serve it under Content-Encoding. Takes algorithm — a zlib function's name (gzip, brotliCompress, deflate, zstdCompress, …) or one of your own — and compressionOptions for it, and needs no extra dependency. Give it to minify to compress an asset in place, or to generate as an asset generator to write the compressed file beside the original; either way it puts itself after the minimizers, through a getStage of its own.

All of the non-default minimizers are declared as optional peer dependencies — install only the ones you actually use. One plugin instance covers several languages at once: give minify an array and each minimizer is offered only the assets its own filter accepts, so JS, CSS, HTML and JSON share a single worker pool (see Examples).

Getting Started

Webpack v5 comes with the latest minimizer-webpack-plugin out of the box. If you are using Webpack v5 or above and wish to customize the options, you will still need to install minimizer-webpack-plugin. Using Webpack v4, you have to install terser-webpack-plugin v4 (minimizer-webpack-plugin is only published for Webpack v5+).

To begin, you'll need to install minimizer-webpack-plugin:

npm install minimizer-webpack-plugin --save-dev

or

yarn add -D minimizer-webpack-plugin

or

pnpm add -D minimizer-webpack-plugin

Then add the plugin to your webpack configuration. For example:

webpack.config.js

const MinimizerPlugin = require("minimizer-webpack-plugin");

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [new MinimizerPlugin()],
  },
};

Finally, run webpack using the method you normally use (e.g., via CLI or an npm script).

Note about source maps

Works only with source-map, inline-source-map, hidden-source-map and nosources-source-map values for the devtool option.

Why?

  • eval wraps modules in eval("string") and the minimizer does not handle strings.
  • cheap has no column information and the minimizer generates only a single line, which leaves only a single mapping.

Using supported devtool values enable source map generation.

Options

test

Type:

type test = string | RegExp | (string | RegExp)[];

Default: /\.m?js(\?.*)?$/i

Test to match files against.

webpack.config.js

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new MinimizerPlugin({
        test: /\.js(\?.*)?$/i,
      }),
    ],
  },
};

include

Type:

type include = string | RegExp | (string | RegExp)[];

Default: undefined

Files to include.

webpack.config.js

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new MinimizerPlugin({
        include: /\/includes/,
      }),
    ],
  },
};

exclude

Type:

type exclude = string | RegExp | (string | RegExp)[];

Default: undefined

Files to exclude.

webpack.config.js

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new MinimizerPlugin({
        exclude: /\/excludes/,
      }),
    ],
  },
};

parallel

Type:

type parallel = boolean | number;

Default: true

Use multi-process parallel running to improve the build speed.

Default number of concurrent runs: os.cpus().length - 1 or os.availableParallelism() - 1 (if this function is supported).

Note

Parallelization can speedup your build significantly and is therefore highly recommended.

Warning

If you use Circle CI or any other environment that doesn't provide the real available count of CPUs then you need to explicitly set up the number of CPUs to avoid Error: Call retries were exceeded (see #143, #202).

boolean

Enable/disable multi-process parallel running.

webpack.config.js

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new MinimizerPlugin({
        parallel: true,
      }),
    ],
  },
};

number

Enable multi-process parallel running and set number of concurrent runs.

webpack.config.js

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new MinimizerPlugin({
        parallel: 4,
      }),
    ],
  },
};

minify

Type:

type minifyFn = (
  input: Record<string, string | Buffer>,
  sourceMap: import("@jridgewell/trace-mapping").SourceMapInput | undefined,
  minifyOptions: {
    module?: boolean | undefined;
    ecma?: import("terser").ECMA | undefined;
  },
  extractComments:
    | boolean
    | "all"
    | "some"
    | RegExp
    | ((
        astNode: any,
        comment: {
          value: string;
          type: "comment1" | "comment2" | "comment3" | "comment4";
          pos: number;
          line: number;
          col: number;
        },
      ) => boolean)
    | {
        condition?:
          | boolean
          | "all"
          | "some"
          | RegExp
          | ((
              astNode: any,
              comment: {
                value: string;
                type: "comment1" | "comment2" | "comment3" | "comment4";
                pos: number;
                line: number;
                col: number;
              },
            ) => boolean)
          | undefined;
        filename?: string | ((fileData: any) => string) | undefined;
        banner?:
          string | boolean | ((commentsFile: string) => string) | undefined;
      }
    | undefined,
) => Promise<{
  code: string;
  map?: import("@jridgewell/trace-mapping").SourceMapInput | undefined;
  errors?: (string | Error)[] | undefined;
  warnings?: (string | Error)[] | undefined;
  extractedComments?: string[] | undefined;
}>;

interface minimizer {
  implementation: minifyFn;
  options?: Record<string, any>;
}

type minify = minifyFn | (minifyFn | minimizer)[] | minimizer;
// An empty array is nothing to minify.

Default: MinimizerPlugin.terserMinify

Which minimizer runs, and the options it runs with. By default the plugin uses terser; overriding it is also how you test an unpublished version or a fork. The default stands whether or not a generate was configured too, and so does test's: the JavaScript minifier and the names it reads are what this plugin is.

Nothing minifying is an empty array rather than a missing value, for an instance whose whole job is its generate:

new MinimizerPlugin({
  test: /.*/,
  minify: [],
  generate: {
    implementation: MinimizerPlugin.compress,
    options: { algorithm: "gzip" },
    type: "asset",
    filename: "[path][base].gz",
  },
});

false is not accepted. A list with nothing in it needs no guard at any of the places that run the minimizers — each simply does nothing — while a second kind of value does.

Warning

Always use require inside minify function when parallel option enabled.

object

The form to prefer: the minimizer and its own options in one place.

webpack.config.js

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new MinimizerPlugin({
        minify: {
          implementation: MinimizerPlugin.swcMinify,
          options: { mangle: false },
        },
      }),
    ],
  },
};

options reaches the minify function as its third argument, and what it takes is that minimizer's own: Terser's minify options for the default one, and the tables in webpack's own minimizers for cssMinify and htmlMinify.

implementation may also name a module path rather than hold the function itself — a string, or { path, export } for a named export, the way sass-loader takes its implementation. A worker then requires the minimizer, which is what the default terser is named by; one written as a function is handed over as source for the worker to rebuild with new Function:

new MinimizerPlugin({
  minify: {
    // Or the string on its own, where the module exports the function itself.
    implementation: {
      path: require.resolve("./my-minifier"),
      export: "minify",
    },
  },
});

The saving is per task, so it needs every minimizer of that task to be nameable: one written as a function anywhere in the list leaves the whole task on the source path, since that function has no other way across. So does a function anywhere in what the task carries — an extractComments callback, or a minimizer's own options holding one — because a required minimizer is handed the payload as it is, and a structured clone throws on a function.

filter(name, info) states which assets this minimizer is offered — return false to decline one, and anything else (undefined included) to accept. It answers for a filter property on the minimizer function itself, which is what the built-ins carry, so setting it here is how you narrow one of them without wrapping it.

new MinimizerPlugin({
  minify: {
    implementation: MinimizerPlugin.sharpMinify,
    options: { encodeOptions: { jpeg: { quality: 80 } } },
    filter: (name) => !name.includes("do-not-touch"),
  },
});

Two keys are filled in before a minimizer sees them, and only when options does not set them itself: ecma, from output.environment, and module, from the asset's own javascriptModule info or its .mjs / .cjs extension. Setting either yourself wins, including module: false.

The default minimizer's options, written out
module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new MinimizerPlugin({
        minify: {
          implementation: MinimizerPlugin.terserMinify,
          options: {
            ecma: undefined,
            parse: {},
            compress: {},
            mangle: true, // Note `mangle.properties` is `false` by default.
            module: false,
            // Deprecated
            output: null,
            format: null,
            toplevel: false,
            nameCache: null,
            ie8: false,
            keep_classnames: undefined,
            keep_fnames: false,
            safari10: false,
          },
        },
      }),
    ],
  },
};

array

Several minimizers are an array of objects, rather than one object holding two lists that have to line up. Each asset is dispatched to the minimizers whose filter accepts it, and when more than one accepts the same asset the output of each is fed to the next (the chain semantic). Warnings, errors and extracted comments from all of them are merged.

webpack.config.js

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new MinimizerPlugin({
        minify: [
          {
            implementation: MinimizerPlugin.terserMinify,
            options: { mangle: false },
          },
          { implementation: MinimizerPlugin.swcMinify },
        ],
      }),
    ],
  },
};

Each entry carries its own filter as well as its own options, which is how the same minimizer runs twice over different assets:

new MinimizerPlugin({
  test: /\.(jpe?g|png)$/i,
  minify: [
    {
      implementation: MinimizerPlugin.sharpMinify,
      options: { encodeOptions: { jpeg: { quality: 60 } } },
      filter: (name) => name.includes("thumb"),
    },
    {
      implementation: MinimizerPlugin.sharpMinify,
      options: { encodeOptions: { jpeg: { quality: 90 } } },
      filter: (name) => !name.includes("thumb"),
    },
  ],
});

This is also what lets one plugin instance and one worker pool handle every asset type: each built-in ships with a filter matching its natural extension, so JS, CSS, HTML and JSON need no second instance. test still defaults to JS only, so widen it to let the other assets reach the dispatcher:

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new MinimizerPlugin({
        test: /\.(?:[cm]?js|css|html?|json)(\?.*)?$/i,
        minify: [
          { implementation: MinimizerPlugin.terserMinify },
          { implementation: MinimizerPlugin.cssnanoMinify },
          { implementation: MinimizerPlugin.htmlMinifierTerser },
          { implementation: MinimizerPlugin.jsonMinify },
        ],
      }),
    ],
  },
};

Pair it with webpack's own cssMinify and htmlMinify and the source a document or a stylesheet nests inside itself is minified too — see Embedded source.

function

A bare function is shorthand for an object with no options. A custom one says what it can do through properties on the function itself — each optional, and each answering a question the plugin would otherwise have to guess at.

webpack.config.js

// Can be async
const minify = (input, sourceMap, minimizerOptions, extractsComments) => {
  // Whatever the `minify` option's `options` holds reaches the third argument
  // You can use `minimizerOptions.myCustomOption`

  // Custom logic for extract comments
  const { map, code } = require("uglify-module") // Or require('./path/to/uglify-module')
    .minify(input, {/* Your options for minification */});

  return { map, code, warnings: [], errors: [], extractedComments: [] };
};

// Used to regenerate `fullhash`/`chunkhash` between different implementation
// Example: you fix a bug in custom minimizer/custom function, but unfortunately webpack doesn't know about it, so you will get the same fullhash/chunkhash
// to avoid this you can provide version of your custom minimizer
// You don't need if you use only `contenthash`
minify.getMinimizerVersion = () => {
  let packageJson;

  try {
    packageJson = require("uglify-module/package.json");
  } catch (error) {
    // Ignore
  }

  return packageJson && packageJson.version;
};

// Restrict the minimizer to the assets it can actually handle. The plugin
// skips assets for which `filter` returns `false` and (when an array of
// minimizers is used) dispatches each asset only to the minimizers that
// accept it. Returning `undefined` is treated as accept.
minify.filter = (name) => /\.[cm]?js(\?.*)?$/i.test(name);

// The languages this minimizer minifies. Source a module embeds in another
// language carries no filename, so `filter` cannot dispatch it and this does
// — a function without it is never handed any.
minify.getTypes = () => ["javascript"];

// Declare this when the minimizer reads the asset's bytes rather than its
// text — an image minimizer. Its `input` values then arrive as a `Buffer` and
// its `code` may be one. Only applied when every minimizer an asset is
// dispatched to declares it, since one that does not could not read the bytes.
minify.supportsBinary = () => true;

// Declare this when the minimizer has to run somewhere other than where
// minification does — it is handed the `Compilation` class so it can name a
// stage rather than a number. Each minimizer runs where it asks, chaining
// through the asset a later one reads back; one asking for nothing runs where
// minifying belongs.
minify.getStage = (compilation) =>
  compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_TRANSFER;

// The name this function's work goes under: the asset it writes is marked
// with it in the asset info, and stats print it. A minimizer declaring
// nothing minified the asset, so `minimized`; a generator declaring nothing
// wrote a new file, so `generated`. It is also what is not run twice — an
// asset already marked with every name a function writes is declined, which
// is how one minified by a child compilation is left alone.
minify.getAssetFlag = () => "compressed";

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new MinimizerPlugin({
        minify: {
          implementation: minify,
          options: { myCustomOption: true },
        },
      }),
    ],
  },
};

A minifier that nests other languages inside what it prints — a document with an inline <style>, a stylesheet with a data: URL — hands each nested body out instead of minifying it itself. It declares which languages it can offer, and the plugin passes renderEmbeddedSource in its options when something else claims one of them:

async function myHtmlMinify(input, sourceMap, minimizerOptions) {
  const [[, code]] = Object.entries(input);
  const { renderEmbeddedSource } = minimizerOptions;

  // Absent when nothing configured claims a language this minifier offers
  const minifyStyle = async (css) => {
    const result = renderEmbeddedSource
      ? await renderEmbeddedSource(css, { type: "css" })
      : undefined;

    // Declining leaves the body exactly as it was written
    return result && typeof result.code === "string" ? result.code : css;
  };

  return { code: await print(code, minifyStyle) };
}

// The languages this minifier can hand out, given the options it will run with
myHtmlMinify.getEmbeddedTypes = (minimizerOptions) => ["css", "javascript"];

renderEmbeddedSource(source, { type, as }) answers { code?, warnings?, errors? } or undefined, and recurses — a body that nests something of its own is reached too. as says which production of type the body is; a style="" is "block-contents" rather than a whole stylesheet. Embedded source has the whole picture.

minimizerOptions

Note

Deprecated, and to be removed in the next major release. Give a minimizer its options in minify instead, which keeps one minimizer's configuration in one place. It keeps working, and setting the options for one minimizer in both places is an error.

Type:

type minimizerOptions = Record<string, any> | Record<string, any>[];

Default: Terser's

Options for the active minimizer, given away from it. With an array of minimizers it may be an array too — each element goes to the minimizer at the same index — or one object every minimizer is handed. It is still the only way to configure the default minimizer without naming it:

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [new MinimizerPlugin({ minimizerOptions: { mangle: false } })],
  },
};

terserOptions is a deprecated alias of it — passing either is equivalent, and minimizerOptions wins if both are set.

generate

Type:

type generateFn = (
  input: Record<string, string | Buffer>,
  sourceMap: undefined,
  generatorOptions: Record<string, any>,
) => Promise<{
  code: string | Buffer;
  filename?: string;
  errors?: (Error | string)[];
  warnings?: (Error | string)[];
}>;

interface generator {
  implementation: generateFn;
  options?: Record<string, any>;
  type?: "import" | "asset";
  filename?: string | ((pathData: any) => string);
  filter?: (name: string) => boolean;
  deleteOriginalAssets?: boolean | ((name: string) => boolean);
  threshold?: number;
  minRatio?: number;
  relatedName?: string | false;
}

type generate =
  | generateFn
  | generateFn[]
  | generator
  | Record<string, generateFn | generateFn[] | generator>;

Default: undefined

Rewrites a module's own bytes as it is built, rather than an asset after the build. That is the one point at which a re-encoding can also rename — return a filename and the asset is emitted under it, with every reference in the bundle following. minify cannot: an asset's name is decided during code generation, so renaming there would leave the bundle pointing at a URL nothing emits.

test, include and exclude select which modules it sees, matched against the module's resource — query and all, so ?as=webp is matchable. A generator runs in the webpack process, since modules build before the worker pool is up, and its answer is cached under the bytes plus the generator and its options.

Two generators ship with the plugin, and they differ in who picks the format. sharpGenerate is told: it takes the target from the request's ?as= or, failing that, from an options.encodeOptions naming exactly one format, and reports an error when neither says which format to write. imageminGenerate is not: its plugins decide, so it reads the format back off the bytes they produced and renames to match, leaving an asset its plugins did not convert under the name it had.

const MinimizerPlugin = require("minimizer-webpack-plugin");

module.exports = {
  module: {
    rules: [
      {
        test: /\.(jpe?g|png)$/i,
        type: "asset/resource",
        // The query has to survive into the name, or two formats of one image
        // collide.
        generator: { filename: "[name][ext][query]" },
      },
    ],
  },
  plugins: [
    new MinimizerPlugin({
      test: /\.(jpe?g|png)$/i,
      generate: MinimizerPlugin.sharpGenerate,
    }),
  ],
};
// `image.jpg` is emitted as `image.webp`, and this import points at it.
import webp from "./image.jpg?as=webp";

Written as an object, generate names its generators, and an asset picks one by name with ?as=:

new MinimizerPlugin({
  test: /\.(jpe?g|png)$/i,
  generate: {
    webp: {
      implementation: MinimizerPlugin.sharpGenerate,
      options: { encodeOptions: { webp: { quality: 90 } } },
    },
    avif: {
      implementation: MinimizerPlugin.sharpGenerate,
      options: { encodeOptions: { avif: { quality: 50 } } },
    },
  },
});
// And `./image.jpg?as=avif` for the other one.
import webp from "./image.jpg?as=webp";

A module naming no preset is left alone, so the same build can import an image unconverted. One naming a preset nothing defines is an error rather than a silent decline, since the name it asked for is what the bundle would point at.

A generator can also be written as an object — on its own, or under a name — which is where its own options live and what lets it read what was emitted instead of a module as it builds:

new MinimizerPlugin({
  test: /\.(jpe?g|png)$/i,
  generate: {
    webp: {
      implementation: MinimizerPlugin.sharpGenerate,
      options: { encodeOptions: { webp: {} } },
      type: "asset",
      // Optional, a template or a function answering with one. Without it the
      // generator's own name for the result is used, which for `sharpGenerate`
      // is the original with its extension replaced.
      filename: "[path][name].webp",
      // Optional. Narrows what this generator reads, on top of `test`.
      filter: (name) => !name.includes("icons/"),
      // Optional, `false` by default: the asset it read stays where it is.
      // Written as a function it is asked per asset.
      deleteOriginalAssets: false,
      // Optional. Skips an asset this small, before the generator is asked.
      threshold: 10240,
      // Optional. Drops the result unless it is this much smaller than what
      // it read, as `generated size / original size`.
      minRatio: 0.8,
      // Optional. The key the new file is recorded under in the original's
      // `related` info, which is how a server asked for one finds the other.
      relatedName: "webp",
    },
  },
});

type decides which of the two things a generator does, and they are not interchangeable — they read different input, at different points in the build:

"import" (the default)"asset"
Readsa module, as it buildsan asset, once it is emitted
Producesthat module's own bytes, renamed with thema new file beside the one it read
Picked by?as=<name> on the importtest / include / exclude, then filter
Reaches a file nothing importsnoyes — copied assets included
Fields it readsimplementation, optionsthose plus filename, filter, deleteOriginalAssets, threshold, minRatio, relatedName
webpack5.111 or newerany supported version

"import" is the only point at which a rename can reach the bundle: the asset is named while its module is built, so every reference follows it. The import that asked for the conversion gets the converted file.

new MinimizerPlugin({
  test: /\.(jpe?g|png)$/i,
  generate: { webp: { implementation: MinimizerPlugin.sharpGenerate } },
});
import url from "./photo.jpg?as=webp"; // url is "photo.webp"

const same = new URL("./photo.jpg?as=webp", import.meta.url); // photo.webp
.hero {
  background: url("./photo.jpg?as=webp"); /* photo.webp */
}
photo.webp    the jpg became this

Those are one asset module between them, so an import, a new URL() and a CSS url() all follow the rename. An asset inlined as a data URI carries no file name, and takes the media type of what it became — data:image/webp;….

"asset" leaves what it read alone and writes another file next to it, so both survive — the <picture> case, where the .webp goes in a srcset you write yourself and the .jpg stays as the fallback. Nothing imports the new file, so ?as= cannot reach it and its preset name selects nothing; the name is only how you address its options. An asset already generated is never generated from again.

new MinimizerPlugin({
  test: /\.(jpe?g|png)$/i,
  generate: {
    webp: { implementation: MinimizerPlugin.sharpGenerate, type: "asset" },
  },
});
import url from "./photo.jpg"; // url is "photo.jpg", unchanged
photo.jpg     still there, unless `deleteOriginalAssets`
photo.webp    generated beside it

filename, filter, deleteOriginalAssets, threshold, minRatio and relatedName describe a file being written beside another, so they belong to "asset" and setting one on an "import" generator is an error rather than a field that quietly does nothing.

Three of them decide whether the new file is worth having. threshold skips an asset too small to bother with, before the generator is asked at all. minRatio drops a result that is not enough smaller than what it read, since a file that saves nothing still costs a request. relatedName records the new file under that key in the original's related info — which is how a server asked for the original finds it — and declines an asset already carrying that key.

A file written under the original's own name has replaced it, so there is nothing beside it to delete and nothing for relatedName to point at; a generator doing that re-encodes an asset in place.

Deleting takes the original file and nothing else. webpack deletes whatever an asset's related names along with it, so a source map, or the file a second generator wrote beside the same original, would go too; the original goes alone instead. Where the generated file took the original's own name there is nothing left to delete, and relatedName is not recorded when the asset that would carry it is being deleted.

A generated file inherits nothing from the one it was read from: what the original's info says about its hashes, its module and where its source came from is true of that file and not of this one. The exception is immutable, and only where filename still derives from the original's name — [name], [base] or [file] — since that is what carried the hash the promise rests on.

When a generator runs is not among them, because it is not the config's to say: the implementation declares it through a getStage of its own, the way it declares everything else about itself — see minify. Compressing has to read the bytes a user downloads, so MinimizerPlugin.compress asks for PROCESS_ASSETS_STAGE_OPTIMIZE_TRANSFER and runs after every minimizer has had its say; one that asks for nothing runs where minifying does. The name its work goes under is the implementation's too, through the same kind of helper: a generator declaring nothing wrote a new file, so generated, while compress says compressed, another encoding of the bytes being no smaller a version of them. Whatever the name, stats print it and the generator declines a file already carrying it, so nothing reads its own output back.

MinimizerPlugin.compress ships with the plugin and is written against that. algorithm says which compression to run — a zlib function's name, or one of your own taking (input, options, callback) — and compressionOptions is what that algorithm is run with, the way terserMinify takes terser's own options. Here it is as an asset generator, which writes the compressed file beside the one it read, so both survive and the URL says which is which:

const MinimizerPlugin = require("minimizer-webpack-plugin");

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new MinimizerPlugin({
        test: /\.(js|css|html|svg)$/i,
        generate: {
          gzip: {
            implementation: MinimizerPlugin.compress,
            options: { algorithm: "gzip" },
            type: "asset",
            filename: "[path][base].gz",
          },
          brotli: {
            implementation: MinimizerPlugin.compress,
            options: {
              algorithm: "brotliCompress",
              compressionOptions: { params: {} },
            },
            type: "asset",
            filename: "[path][base].br",
          },
        },
      }),
    ],
  },
};

Minifying and compressing are then one plugin over one pass of filtering and one cache, and the ordering they need — compress what minification produced — is what stage states rather than what applying two plugins in the right order happens to give. Each algorithm is run at its own maximum by default (zlib's best level, brotli's best quality); name compressionOptions to say otherwise.

Compressing with minifying, and without

The example above does both: minify defaults to terser, so the bundle is minified and the compressed files are written from what minification produced. That is the shape to want — one pass of filtering, one cache, and stage ordering the two — and it needs nothing said about minify at all:

new MinimizerPlugin({
  test: /\.(js|css|html|svg)$/i,
  generate: {
    implementation: MinimizerPlugin.compress,
    options: { algorithm: "gzip" },
    type: "asset",
    filename: "[path][base].gz",
  },
});

Compressing only — an instance that must not touch what it reads — says so with an empty list of minimizers. test is then yours to state too, since the .js default belongs to minifying:

new MinimizerPlugin({
  test: /.*/,
  minify: [],
  generate: {
    implementation: MinimizerPlugin.compress,
    options: { algorithm: "gzip" },
    type: "asset",
    filename: "[path][base].gz",
  },
});

Nothing is minified, no asset is marked minimized, and the bundle keeps the name it would have had without this plugin — an instance that rewrites nothing salts no hash.

It is an ordinary minimizer too, so minify takes it the way it takes terserMinify or swcMinify. There it compresses the asset in place rather than beside it — the shape for a server that says what the encoding is through Content-Encoding while the URL stays as it was:

const MinimizerPlugin = require("minimizer-webpack-plugin");

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new MinimizerPlugin({
        test: /\.js$/i,
        minify: MinimizerPlugin.compress,
        minimizerOptions: { algorithm: "gzip" },
      }),
    ],
  },
};

An array runs its minimizers in order, each one reading what the last produced, so minifying and then compressing in place is one entry after another — and each states its own options. Each runs at the stage it asks for — terser before the hash is taken, compress after — so [contenthash] still names what minification produced:

new MinimizerPlugin({
  test: /\.js$/i,
  minify: [
    { implementation: MinimizerPlugin.terserMinify },
    {
      implementation: MinimizerPlugin.compress,
      options: { algorithm: "brotliCompress" },
    },
  ],
});

ecma is filled in from output.environment unless a generator's options set it, the same way it is for a minimizer's — see minify.

filename is a webpack filename template resolved against the asset read, so [path], [name], [base], [ext] and [query] are available; content hashes are not, because the name derives from one the original already carries.

In watch mode the rename is carried on the module rather than reapplied each build, so a rebuild that does not touch the image keeps pointing at the generated name without running the generator again. Changing the image does run it again, since its answer is cached under the bytes.

The same holds across runs under cache.type: "filesystem", where a module is restored from the pack rather than rebuilt. That restored result is the generator's, so changing a generator or its options has to invalidate the pack, and the plugin adds their identity to cache.version so it does. This needs the plugin to be in the config — plugins or optimization.minimizer — since webpack builds the cache while it applies them; a plugin applied by hand after webpack() returns is too late to reach it, and a changed generator would then be ignored until the cache directory is removed.

Note

Note

generatorOptions is kept as a deprecated way of giving a generator its options — one object for one generator, an array positionally matching an array of them, or keyed by name where generate names its generators. Prefer a generator's own options. Setting both for one generator is an error, and a key naming no generator is an error rather than silently doing nothing.

Note

An "import" generator needs a webpack whose NormalModule processResult hook can be awaited (5.111 or newer), since that is where a rename has to happen. On an older webpack the plugin reports an error rather than silently generating nothing. An "asset" generator does not use that hook and works on any supported webpack.

extractComments

Type:

type extractComments =
  | boolean
  | string
  | RegExp
  | ((
      astNode: any,
      comment: {
        value: string;
        type: "comment1" | "comment2" | "comment3" | "comment4";
        pos: number;
        line: number;
        col: number;
      },
    ) => boolean)
  | {
      condition?:
        | boolean
        | "all"
        | "some"
        | RegExp
        | ((
            astNode: any,
            comment: {
              value: string;
              type: "comment1" | "comment2" | "comment3" | "comment4";
              pos: number;
              line: number;
              col: number;
            },
          ) => boolean)
        | undefined;
      filename?: string | ((fileData: any) => string) | undefined;
      banner?:
        string | boolean | ((commentsFile: string) => string) | undefined;
    };

Default: true

Whether comments shall be extracted to a separate file, (see details).

By default, extract only comments using /^\**!|@preserve|@license|@cc_on/i RegExp condition and remove remaining comments.

If the original file is named foo.js, then the comments will be stored to foo.js.LICENSE.txt.

A minimizer's options.format.comments specifies whether the comment will be preserved - i.e., it is possible to preserve some comments (e.g. annotations) while extracting others, or even preserve comments that have already been extracted.

boolean

Enable/disable extracting comments.

webpack.config.js

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new MinimizerPlugin({
        extractComments: true,
      }),
    ],
  },
};

string

Extract all or some (use the /^\**!|@preserve|@license|@cc_on/i RegExp) comments.

webpack.config.js

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new MinimizerPlugin({
        extractComments: "all",
      }),
    ],
  },
};

RegExp

All comments that match the given expression will be extracted to a separate file.

webpack.config.js

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new MinimizerPlugin({
        extractComments: /@extract/i,
      }),
    ],
  },
};

function

All comments that match the given expression will be extracted to a separate file.

webpack.config.js

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new MinimizerPlugin({
        extractComments: (astNode, comment) => {
          if (/@extract/i.test(comment.value)) {
            return true;
          }

          return false;
        },
      }),
    ],
  },
};

object

Allows you to customize condition for extracting comments, and specify the extracted file name and banner.

webpack.config.js

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new MinimizerPlugin({
        extractComments: {
          condition: /^\**!|@preserve|@license|@cc_on/i,
          filename: (fileData) =>
            // The "fileData" argument contains object with "filename", "basename", "query" and "hash"
            `${fileData.filename}.LICENSE.txt${fileData.query}`,
          banner: (licenseFile) =>
            `License information can be found in ${licenseFile}`,
        },
      }),
    ],
  },
};
condition

Type:

type condition =
  | boolean
  | "all"
  | "some"
  | RegExp
  | ((
      astNode: any,
      comment: {
        value: string;
        type: "comment1" | "comment2" | "comment3" | "comment4";
        pos: number;
        line: number;
        col: number;
      },
    ) => boolean)
  | undefined;

The condition that determines which comments should be extracted.

webpack.config.js

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new MinimizerPlugin({
        extractComments: {
          condition: "some",
          filename: (fileData) =>
            // The "fileData" argument contains object with "filename", "basename", "query" and "hash"
            `${fileData.filename}.LICENSE.txt${fileData.query}`,
          banner: (licenseFile) =>
            `License information can be found in ${licenseFile}`,
        },
      }),
    ],
  },
};
filename

Type:

type filename = string | ((fileData: any) => string) | undefined;

Default: [file].LICENSE.txt[query]

Available placeholders: [file], [query] and [filebase] ([base] for webpack 5).

The file where the extracted comments will be stored.

Default is to append the suffix .LICENSE.txt to the original filename.

Warning

We highly recommend using the .txt extension. Using .js/.cjs/.mjs extensions may conflict with existing assets, which leads to broken code.

webpack.config.js

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new MinimizerPlugin({
        extractComments: {
          condition: /^\**!|@preserve|@license|@cc_on/i,
          filename: "extracted-comments.js",
          banner: (licenseFile) =>
            `License information can be found in ${licenseFile}`,
        },
      }),
    ],
  },
};
banner

Type:

type banner = string | boolean | ((commentsFile: string) => string) | undefined;

Default: /*! For license information please see ${commentsFile} */

The banner text that points to the extracted file and will be added at the top of the original file.

It can be false (no banner), a String, or a function<(string) -> String> that will be called with the filename where the extracted comments have been stored.

The banner will be wrapped in a comment: /*! … */ by default, or whatever the minify function's formatBanner helper returns. An HTML minimizer needs one, since a /*! line is text in a document:

minify.formatBanner = (banner) => `<!-- ${banner} -->`;

The banner goes at the top of the asset; a getBannerPosition helper returning "end" appends it instead, which keeps a document's doctype first:

minify.getBannerPosition = () => "end";

The source of both helpers is part of the cache key and the [chunkhash], but not the values they close over: a helper whose output depends on such a value must make getMinimizerVersion change with it, or a rebuild reuses the earlier banner from the cache.

webpack.config.js

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new MinimizerPlugin({
        extractComments: {
          condition: true,
          filename: (fileData) =>
            // The "fileData" argument contains object with "filename", "basename", "query" and "hash"
            `${fileData.filename}.LICENSE.txt${fileData.query}`,
          banner: (commentsFile) =>
            `My custom banner about license information ${commentsFile}`,
        },
      }),
    ],
  },
};

Embedded source

Source written in one language that reaches the bundle inside another is minified too, which no asset carries and an asset-level minimizer therefore never sees. It needs no option (webpack >= 5.110.0; older versions reach only what an asset nests inside itself).

Such source has no filename, so test / include / exclude and each minimizer's filter — all of which match filenames — cannot dispatch it. It is dispatched by the language it is written in, which every minify function states for itself:

myCssMinifier.getTypes = () => ["css"];

webpack's own cssMinify and htmlMinify declare css and html, and are the two that hand out what they nest. The built-ins declare javascript (terserMinify, uglifyJsMinify, swcMinify, esbuildMinify), css (cssnanoMinify, cssoMinify, cleanCssMinify, esbuildMinifyCss, lightningCssMinify, swcMinifyCss), html (htmlMinifierTerser, swcMinifyHtml, swcMinifyHtmlFragment, minifyHtmlNode) and json (jsonMinify). A language no configured minimizer claims — svg out of the box — is emitted exactly as it was written, and a custom minify function without getTypes is never handed embedded source at all.

What this reaches:

  • CSS and HTML a module embeds in a JavaScript string literal (every exportType but link).
  • The text an asset/source module embeds, and the payload an asset/inline module encodes — the payload before it is encoded, so the encoding covers what came back. A language written as text only: an inline svg is offered, an inline png or jpeg is not, so a raster image that becomes a data: URI is minified by generate rather than here.
  • What a document or a stylesheet nests inside itself: an inline <style>, every style="", a <script> holding JavaScript or JSON, an <svg> subtree, the document an <iframe srcdoc> holds, and the payload of a url() data: URL. This is how an inline <script> is minified by terser at all.

A style="" arrives as css with as: "block-contents" — the same word module.parser.css.as uses. It holds a block's contents rather than a whole stylesheet, so a minifier claiming css is handed one either way and as says which it is. Ignore it at your peril: parsing a declaration list as a stylesheet finds no rule and returns nothing.

A <script type="module"> arrives the same way, as javascript with as: "module": JavaScript has two productions too, and a module script is one wherever it sits. The built-in JavaScript minimizers read it as their own module option, so the engine is never handed the word itself; a minify function of your own claiming javascript is handed it and decides. Ignore it at your peril: a module script read as a classic one is a syntax error the moment it holds a top-level await.

An event handler attribute — onclick="" — arrives as javascript with as: "event-handler": its value is a function body, the production the return cancelling an event is written in. No JavaScript engine here parses one on its own, so each built-in minimizer minifies the function that body belongs to and answers with the body back out of it. A minify function of your own is handed the body as written and reads it however its engine can.

The nested case needs a minifier that can hand its nested bodies out, which it also states for itself — webpack's cssMinify and htmlMinify do:

// The languages this minifier can offer, given the options it will run with.
myHtmlMinifier.getEmbeddedTypes = (minimizerOptions) => ["css", "javascript"];

The option is passed only when the two declarations meet: if nothing configured claims a language this minifier could offer, it is never handed one, and minification is exactly what it always was. When it is, the nested bodies are reached in the same parse that prints — the minifier leaves a marker where each one goes and the answers are put in their place, so nothing is parsed twice.

Note

Source a module embeds in JavaScript is minified during code generation, before the worker pool is up, so parallel does not apply to it. What an asset nests inside itself is minified in the pool with the asset.

Examples

Preserve Comments

Extract all legal comments (i.e. /^\**!|@preserve|@license|@cc_on/i) and preserve /@license/i comments.

webpack.config.js

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new MinimizerPlugin({
        minify: {
          implementation: MinimizerPlugin.terserMinify,
          options: { format: { comments: /@license/i } },
        },
        extractComments: true,
      }),
    ],
  },
};

Remove Comments

If you want to build without comments, use this config:

webpack.config.js

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new MinimizerPlugin({
        minify: {
          implementation: MinimizerPlugin.terserMinify,
          options: { format: { comments: false } },
        },
        extractComments: false,
      }),
    ],
  },
};

uglify-js

UglifyJS is a JavaScript parser, minifier, compressor and beautifier toolkit.

webpack.config.js

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new MinimizerPlugin({
        minify: {
          implementation: MinimizerPlugin.uglifyJsMinify,
          // `options` will be passed to `uglify-js`
          // Link to options - https://github.com/mishoo/UglifyJS#minify-options
          options: {},
        },
      }),
    ],
  },
};

swc

swc is a super-fast compiler written in Rust, producing widely supported JavaScript from modern standards and TypeScript.

Warning

extractComments is supported with @swc/core >= 1.15.30. Only serializable extract conditions are supported: booleans, "some", "all", string patterns, RegExp values without flags, or object conditions that resolve to those forms. Function conditions and flagged regular expressions are not supported.

webpack.config.js

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new MinimizerPlugin({
        minify: {
          implementation: MinimizerPlugin.swcMinify,
          // `options` will be passed to `swc` (`@swc/core`)
          // Link to options - https://swc.rs/docs/config-js-minify
          options: {},
        },
      }),
    ],
  },
};

esbuild

esbuild is an extremely fast JavaScript bundler and minifier.

Warning

The extractComments option is not supported, and all legal comments (i.e. copyright, licenses and etc) will be preserved.

webpack.config.js

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new MinimizerPlugin({
        minify: {
          implementation: MinimizerPlugin.esbuildMinify,
          // `options` will be passed to `esbuild`
          // Link to options - https://esbuild.github.io/api/#minify
          // Note: the `minify` options is true by default (and override other `minify*` options), so if you want to disable the `minifyIdentifiers` option (or other `minify*` options) please use:
          // options: {
          //   minify: false,
          //   minifyWhitespace: true,
          //   minifyIdentifiers: false,
          //   minifySyntax: true,
          // },
          options: {},
        },
      }),
    ],
  },
};

JSON

Uses JSON.stringify() to minify your JSON files during the build process.

webpack.config.js

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      // Keeps original terser plugin to minify JS files
      "...",
      // Will minify JSON files (they can come from copy-webpack-plugin or when you are using asset modules)
      new MinimizerPlugin({
        test: /\.json$/,
        minify: {
          implementation: MinimizerPlugin.jsonMinify,
          // We are supporting `space` and `replacer` options, you can set them below
          options: {},
        },
      }),
    ],
  },
};

HTML

The plugin can minify HTML assets too. Pick one of the bundled HTML minimizers and set test to match your HTML files.

Available HTML minimizers:

  • webpack.html.syntax.htmlMinify — webpack's own, shipped with webpack itself, so it needs no extra dependency. The one that can minify what a document nests — an inline <style>, a <script>, an <svg>. See webpack's own minimizers.
  • MinimizerPlugin.htmlMinifierTerser — uses html-minifier-terser.
  • MinimizerPlugin.swcMinifyHtml — uses @swc/html for full HTML documents (with doctype and <html>/<head>/<body> tags).
  • MinimizerPlugin.swcMinifyHtmlFragment — uses @swc/html for HTML fragments (e.g. content inside <template></template> or partial HTML strings).
  • MinimizerPlugin.minifyHtmlNode — uses @minify-html/node.

The HTML minimizers are optional peer dependencies — install only the one you actually use:

npm install --save-dev html-minifier-terser
# or
npm install --save-dev @swc/html
# or
npm install --save-dev @minify-html/node

Note

HTML assets typically come from plugins like copy-webpack-plugin, html-webpack-plugin, or webpack's asset modules.

Note

Whitespace handling differs between tools (defaults):

  • @swc/html — removes/collapses whitespace only in safe places (around html/body, inside <head>, between <meta>/<script>/<link> etc.).
  • html-minifier-terser — always collapses multiple whitespaces to a single space (never removes entirely); configurable via its options.
  • @minify-html/node — see its whitespace docs.

html-minifier-terser

html-minifier-terser is a JavaScript-based HTML minifier with no native dependency.

webpack.config.js

const MinimizerPlugin = require("minimizer-webpack-plugin");

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      // Keeps the default Terser plugin for JS files
      "...",
      new MinimizerPlugin({
        test: /\.html(\?.*)?$/i,
        minify: {
          implementation: MinimizerPlugin.htmlMinifierTerser,
          // Options - https://github.com/terser/html-minifier-terser#options-quick-reference
          options: {
            collapseWhitespace: true,
            removeComments: true,
          },
        },
      }),
    ],
  },
};

@swc/html — HTML documents

Use swcMinifyHtml for complete HTML documents (i.e. with a doctype and <html>/<head>/<body> tags).

webpack.config.js

const MinimizerPlugin = require("minimizer-webpack-plugin");

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      "...",
      new MinimizerPlugin({
        test: /\.html(\?.*)?$/i,
        minify: {
          implementation: MinimizerPlugin.swcMinifyHtml,
          // Options - https://github.com/swc-project/bindings/blob/main/packages/html/index.ts
          options: {},
        },
      }),
    ],
  },
};

@swc/html — HTML fragments

Use swcMinifyHtmlFragment for partial HTML — for example, content of <template></template> tags or HTML strings that get injected into another document.

webpack.config.js

const MinimizerPlugin = require("minimizer-webpack-plugin");

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      "...",
      new MinimizerPlugin({
        test: /\.template\.html$/i,
        minify: {
          implementation: MinimizerPlugin.swcMinifyHtmlFragment,
          // Options - https://github.com/swc-project/bindings/blob/main/packages/html/index.ts
          options: {},
        },
      }),
    ],
  },
};

Note

The difference between swcMinifyHtml and swcMinifyHtmlFragment is the error reporting — invalid or broken syntax is reported at build time.

@minify-html/node

@minify-html/node is a Rust HTML minifier.

webpack.config.js

const Minimizer = require("minimizer-webpack-plugin");

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      "...",
      new Minimizer({
        test: /\.html(\?.*)?$/i,
        minify: {
          implementation: Minimizer.minifyHtmlNode,
          // Options - https://github.com/wilsonzlin/minify-html#minification
          options: {},
        },
      }),
    ],
  },
};

You can also stack multiple MinimizerPlugin instances to compress different files with different minify functions in the same build (e.g. JS with terserMinify, HTML with htmlMinifierTerser, JSON with jsonMinify).

CSS

The plugin can minify CSS assets too. Pick one of the bundled CSS minimizers and set test to match your CSS files.

Available CSS minimizers:

  • webpack.css.syntax.cssMinify — webpack's own, shipped with webpack itself, so it needs no extra dependency. The one that can minify the CSS a document nests. See webpack's own minimizers.
  • MinimizerPlugin.cssnanoMinify — uses cssnano (via postcss).
  • MinimizerPlugin.cssoMinify — uses csso.
  • MinimizerPlugin.cleanCssMinify — uses clean-css.
  • MinimizerPlugin.esbuildMinifyCss — uses esbuild with the CSS loader.
  • MinimizerPlugin.lightningCssMinify — uses lightningcss.
  • MinimizerPlugin.swcMinifyCss — uses @swc/css.

The CSS minimizers are optional peer dependencies — install only the ones you actually use:

npm install --save-dev cssnano postcss
# or
npm install --save-dev csso
# or
npm install --save-dev clean-css
# or
npm install --save-dev esbuild
# or
npm install --save-dev lightningcss
# or
npm install --save-dev @swc/css

Note

CSS assets typically come from plugins like mini-css-extract-plugin or webpack's asset modules.

cssnano

cssnano runs as a PostCSS plugin.

webpack.config.js

const MinimizerPlugin = require("minimizer-webpack-plugin");

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      // Keeps the default Terser plugin for JS files
      "...",
      new MinimizerPlugin({
        test: /\.css(\?.*)?$/i,
        minify: {
          implementation: MinimizerPlugin.cssnanoMinify,
          // Options - https://cssnano.github.io/cssnano/docs/config-file/
          options: {
            preset: "default",
          },
        },
      }),
    ],
  },
};

csso

csso is a CSS minifier with structural optimisations.

webpack.config.js

const MinimizerPlugin = require("minimizer-webpack-plugin");

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      "...",
      new MinimizerPlugin({
        test: /\.css(\?.*)?$/i,
        minify: {
          implementation: MinimizerPlugin.cssoMinify,
          // Options - https://github.com/css/csso#minifysource-options
          options: {},
        },
      }),
    ],
  },
};

clean-css

clean-css is a widely-used CSS optimiser.

webpack.config.js

const MinimizerPlugin = require("minimizer-webpack-plugin");

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      "...",
      new MinimizerPlugin({
        test: /\.css(\?.*)?$/i,
        minify: {
          implementation: MinimizerPlugin.cleanCssMinify,
          // Options - https://github.com/clean-css/clean-css#constructor-options
          options: {},
        },
      }),
    ],
  },
};

esbuild

esbuild ships with a fast CSS minifier (used via its CSS loader).

webpack.config.js

const MinimizerPlugin = require("minimizer-webpack-plugin");

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      "...",
      new MinimizerPlugin({
        test: /\.css(\?.*)?$/i,
        minify: {
          implementation: MinimizerPlugin.esbuildMinifyCss,
          // Options - https://esbuild.github.io/api/#transform-api
          options: {},
        },
      }),
    ],
  },
};

lightningcss

lightningcss is a Rust-based CSS parser, transformer, and minifier.

webpack.config.js

const MinimizerPlugin = require("minimizer-webpack-plugin");

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      "...",
      new MinimizerPlugin({
        test: /\.css(\?.*)?$/i,
        minify: {
          implementation: MinimizerPlugin.lightningCssMinify,
          // Options - https://lightningcss.dev/transpilation.html
          options: {},
        },
      }),
    ],
  },
};

@swc/css

@swc/css is a Rust-based CSS minifier.

webpack.config.js

const MinimizerPlugin = require("minimizer-webpack-plugin");

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      "...",
      new MinimizerPlugin({
        test: /\.css(\?.*)?$/i,
        minify: {
          implementation: MinimizerPlugin.swcMinifyCss,
          // Options - https://github.com/swc-project/bindings/blob/main/packages/css/index.ts
          options: {},
        },
      }),
    ],
  },
};

webpack's own minimizers

webpack ships a CSS and an HTML minimizer of its own. They need no extra dependency — if you have webpack, you have them — and they are the two that can hand out what a document or a stylesheet nests inside itself, so pairing them with terserMinify and jsonMinify is what minifies embedded source (see Embedded source). They are reached through webpack's public css.syntax and html.syntax exports, and need webpack 5.111.0 or newer.

const MinimizerPlugin = require("minimizer-webpack-plugin");
const { cssMinify } = require("webpack").css.syntax;
const { htmlMinify } = require("webpack").html.syntax;

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new MinimizerPlugin({
        test: /\.(?:[cm]?js|css|html|json)(\?.*)?$/i,
        minify: [
          { implementation: MinimizerPlugin.terserMinify },
          { implementation: cssMinify },
          { implementation: htmlMinify },
          { implementation: MinimizerPlugin.jsonMinify },
        ],
      }),
    ],
  },
};

Each one dispatches itself, so test only has to be wide enough to let the assets through: cssMinify claims *.css and htmlMinify claims *.html through their own filter, and both run in the worker pool.

cssMinifyhtmlMinify
getTypes()csshtml
getEmbeddedTypes()svg, css, html, json, javascriptcss, javascript, json, svg, html
filter/\.css(\?.*)?$/i/\.html(\?.*)?$/i
supportsWorkerThreads()truetrue

Their options are optimization.minimize.css and optimization.minimize.html respectively. environment carries what the target can read ({ browsers, vendorPrefixes }, the CSS entries of output.environment), so a spelling the target could not parse is never reached for.

cssMinify options

Every option is named directly on the object — the per-transform switches sit beside the rest, not nested.

OptionTypeDefaultWhat it does
environment{ browsers?: string[], vendorPrefixes?: boolean }—What the target can read. browsers is a browserslist selection; vendorPrefixes: false leaves prefixes alone.
as"stylesheet" | "block-contents""stylesheet"Which production to read the source as. The plugin sets it — a style="" arrives as "block-contents".
convertLengthUnitsbooleanfalseRewrite a length into a shorter unit it is exactly equal in (16px → 1pc). Off because it earns nothing once compressed.
rewriteCustomPropertiesbooleanfalseShorten a custom property's value like any other (--x:#ffffff → --x:#fff). Off because getPropertyValue() hands that text back.
unusedSymbolsstring[]—Names a whole-project analysis found unused; a bare name is a class, id or @keyframes, a ---prefixed one a custom property.
pseudoClasses{ [name: string]: string }—Write a pseudo-class as a class instead, so a script can apply it where the engine does not.
renderEmbeddedSourcefunction—Owned by the plugin — do not pass it.

Per-transform switches, each on unless set to false:

SwitchWhat it rewrites
colorFallbacksWrites a color the target cannot read as an extra declaration before it, in a spelling it does read.
lowerUnsupportedWrites a spelling the target cannot read as one it can — the same value, said another way.
foldCaseLowercases a name that matches ASCII case-insensitively (at-rule, property, pseudo, function, unit, keyword).
rewriteEscapesWrites an escaped identifier the shortest way that names it.
commentsWhich comments survive: "some" (default), true / "all", false, or a pattern / predicate over the comment's text.
resolveCustomAtRulesInlines what @custom-media / @custom-selector names, and drops the rule that named it.
mergeLonghandsWrites a family of longhands as the one shorthand that sets them.
mergeRulesJoins rules printing the same block, at-rules sharing a prelude, and a @layer block a later sibling reopens.
normalizeQuotesNormalizes quoting of strings, url(), font families and attribute values.
reduceFunctionsComputes a call into the shorter call naming the same value (calc(), transforms, gradients, easings, filters).
removeDeadRulesDrops a rule or declaration nothing can read — an empty rule, one an identical later one supersedes.
rewriteDirSelectorWrites a :dir() the target cannot read as the [dir] attribute selector.
shortenColorsWrites each color in the shortest spelling of the same value.
shortenMediaQueriesWrites a media feature in its range spelling and collapses an and of two into the interval.
shortenNumbersWrites each number in its shortest equal spelling.
shortenSelectorsRewrites a selector into a shorter equal one.
shortenValuesWrites a value the shortest way its property's own grammar allows.

htmlMinify options

OptionTypeDefaultWhat it does
environmentsame as cssMinify—CSS's, not HTML's: handed to the CSS minifier this runs over inline CSS.
cssobject{}The CSS minifier's options for that inline CSS — see below.
collapseWhitespaceboolean | "conservative" | "smart" | "all"falseCollapses each run of whitespace in text, except where an ancestor renders it verbatim; "smart" also drops what sits against a block edge, "all" every edge.
removeEmptyAttributesbooleanfalseDrops an attribute whose empty value leaves it in the state its absence gives.
removeEmptyElementsbooleanfalseDrops an element with no children and no attributes, unless its bare form is meaningful.
mergeStylesbooleanfalsePrints a run of adjacent <style> elements as one sheet.
sortAttributesbooleanfalsePrints attributes commonest name first, ties by name — nothing in HTML reads the order.
sortTokenListsbooleanfalsePrints every token list the DOM reads as a set (class, rel, part, …) in token order.
removeRedundantAttributesboolean | "smart" | "all"falseDrops an attribute whose value is the element's own default; "all" also drops spec defaults a selector can match.
removeImpliedTagsboolean | "smart" | "all""smart"How much of the <html> / <head> / <body> shell may be left out: "smart" only the <html> start tag, true / "all" all six.
minifyConditionalCommentsbooleanfalseMinifies the markup inside a conditional comment's body.
renderEmbeddedSourcefunction—Owned by the plugin — do not pass it.

Per-transform switches, each on unless set to false:

SwitchWhat it rewrites
collapseBooleanAttributesWrites a boolean attribute spelled with its own name (disabled="disabled") as the bare name.
commentsWhich comments survive — same values as CSS's. One a parser or server reads is kept regardless.
normalizeAttributeQuotesDrops or re-picks an attribute value's quotes.
normalizeEnumeratedAttributesFolds an enumerated value to the keyword it names.
normalizeListAttributesNormalizes a space-, comma- or descriptor-separated list (class, rel, srcset, sizes, viewport content).
normalizeNumericAttributesWrites an integer attribute the one way its rules read it.
removeOptionalTagsLeaves out an optional tag other than the shell, which is removeImpliedTags.

The css object takes the CSS minifier's convertLengthUnits, rewriteCustomProperties, unusedSymbols and pseudoClasses, plus the CSS per-transform switches. environment is not among them: htmlMinify runs the CSS minifier over every inline <style> and style="", so environment stays top level, shared by the document and the CSS inside it. Give the two minimizers the same environment and a .css asset and the same declaration inline agree about the target:

const environment = { browsers: ["chrome 100", "safari 15"] };

const options = [
  {},
  // cssMinify
  { environment, convertLengthUnits: true },
  // htmlMinify — `environment` reaches the CSS it nests; `css` carries the rest
  { environment, css: { convertLengthUnits: true } },
  {},
];

Note

Do not pass renderEmbeddedSource yourself. The plugin owns that option: it wires the minimizers to each other with it, and a value you set is replaced. What a minimizer offers and what another claims is what routes embedded source — see Embedded source.

Both minimizers re-resolve webpack when they run, in the worker pool as well as in the main process, so the webpack resolvable from this plugin has to be the one they came from, and at least 5.111.0 — the release that exports them on webpack.css.syntax and webpack.html.syntax. On an older webpack the destructure above yields undefined; a second or older copy in the tree surfaces from inside the minifier as:

Cannot destructure property 'SourceProcessor' of 'webpack.css.syntax' as it is undefined.
Cannot destructure property 'askEmbeddedRenderer' of 'webpack.html.syntax' as it is undefined.

Deduplicate webpack (npm dedupe, or a single version in a workspace root) and it goes away.

Images

The plugin can minify image assets too. Pick one of the bundled image minimizers and set test to match your images.

Available image minimizers:

  • MinimizerPlugin.sharpMinify — uses sharp, re-encoding each image as its own format.
  • MinimizerPlugin.svgoMinify — uses svgo.
  • MinimizerPlugin.imageminMinify — uses imagemin and the plugins you name.
  • MinimizerPlugin.napiRsImageMinify — uses @napi-rs/image, which ships prebuilt codecs.

Converting an image to another format is generate's job rather than a minimizer's, since only that can rename the asset: MinimizerPlugin.sharpGenerate and MinimizerPlugin.imageminGenerate.

The image minimizers are optional peer dependencies — install only the ones you actually use:

npm install --save-dev sharp
# or
npm install --save-dev svgo
# or
npm install --save-dev imagemin imagemin-mozjpeg imagemin-pngquant

Note

On minify these only minify — they never change an image's format or name. An asset's name is decided during code generation, long before a minimizer runs, so a renamed asset would leave the bundle pointing at a URL nothing emits. Converting .png to .webp is what generate is for: it runs while the module builds, early enough to rename.

Note

sharp and imagemin read the asset's bytes rather than its text, so they run in the webpack process rather than in the worker pool — they do their own threading. Minimizers configured beside them keep the pool.

Images that never become files

minify runs over emitted assets, so an image that becomes a data: URI instead of a file — asset/inline, or asset under Rule.parser.dataUrlCondition — never reaches it. An SVG is the exception, since it is text and webpack offers it as embedded source.

Put the same minimizer under generate and it does reach one: a generator runs while the module builds, over the bytes themselves, before anything encodes them. It renames nothing unless it answers with a name, so a minimizer stays a minimizer there.

new MinimizerPlugin({
  test: /\.(jpe?g|png)$/i,
  generate: { implementation: MinimizerPlugin.sharpMinify },
});

Do not list the same image minimizer in both for the same assets — an emitted one would then be re-encoded twice, which costs time and, for a lossy format, quality. generate covers inlined and emitted images alike; minify is the one to keep when nothing is inlined, since it runs after the whole build and caches per asset. An "import" generator needs webpack 5.111 or newer.

Lossless and lossy

Images are optimized in one of two modes:

  1. Lossless — the decoded pixels come back identical, only the encoding is made smaller.
  2. Lossy — quality is traded for a smaller file.

Which one you get with no encodeOptions at all, measured by decoding the output back to pixels and comparing it with the input:

FormatsharpMinifynapiRsImageMinify
aviflossylossy
giflossynot handled
jpeglossylossless
pnglosslesslossless
tifflossynot handled
webplossylossy

So sharpMinify is lossy by default everywhere except PNG — it re-encodes at sharp's own defaults, which are quality settings rather than lossless ones. napiRsImageMinify is lossless by default for both PNG and JPEG, because those two are repacked in place by oxipng and mozjpeg, rewriting the encoded bytes rather than decoding to pixels and encoding again.

svgoMinify rewrites markup rather than pixels. It is lossless in the sense that the rendered image is meant to be unchanged, but its default plugin preset does rewrite paths and drop metadata — see svgo for which of those to turn off.

imageminMinify is whatever its plugins are. imagemin-jpegtran, imagemin-optipng and imagemin-gifsicle are lossless; imagemin-mozjpeg and imagemin-pngquant are lossy. imagemin-svgo can be either.

Asking sharpMinify for lossless output

new MinimizerPlugin({
  test: /\.(png|jpe?g|webp|avif)(\?.*)?$/i,
  minify: {
    implementation: MinimizerPlugin.sharpMinify,
    options: {
      encodeOptions: {
        // https://sharp.pixelplumbing.com/api-output
        jpeg: { quality: 100 },
        webp: { lossless: true },
        avif: { lossless: true },
        // PNG is already lossless at sharp's defaults
        png: {},
      },
    },
  },
});

Asking napiRsImageMinify for lossy output, which is where the savings are:

new MinimizerPlugin({
  test: /\.(png|jpe?g|webp|avif)(\?.*)?$/i,
  minify: {
    implementation: MinimizerPlugin.napiRsImageMinify,
    options: {
      encodeOptions: {
        // Anything below 100 re-encodes rather than repacking
        jpeg: { quality: 80 },
        webp: { quality: 80 },
        avif: { quality: 70 },
        // `png` has no quality setting — it is lossless either way
      },
    },
  },
});

Note

Lossless does not mean "no smaller". On this repository's own PNG fixture napiRsImageMinify returns 59% of the original size without touching a single pixel, and sharpMinify returns 92%.

sharp

sharp re-encodes each image as the format its name already claims, so a .png stays a PNG. It handles avif, gif, heif, jp2, jpeg, png, tiff and webp.

webpack.config.js

const MinimizerPlugin = require("minimizer-webpack-plugin");

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      // Keeps the default Terser plugin for JS files
      "...",
      new MinimizerPlugin({
        test: /\.(png|jpe?g|webp|avif|tiff?|gif)$/i,
        minify: {
          implementation: MinimizerPlugin.sharpMinify,
          options: {
            // Options are keyed by sharp's format name
            // https://sharp.pixelplumbing.com/api-output
            encodeOptions: {
              jpeg: { quality: 80 },
              png: { compressionLevel: 9 },
              webp: { lossless: true },
            },
          },
        },
      }),
    ],
  },
};

sharp can also transform the image while it re-encodes:

new MinimizerPlugin({
  test: /\.(png|jpe?g)$/i,
  minify: {
    implementation: MinimizerPlugin.sharpMinify,
    options: {
      // `enabled` and `unit` ("px" by default, or "percent") are read here;
      // everything else goes to sharp
      // https://sharp.pixelplumbing.com/api-resize
      resize: { width: 800, unit: "px", fit: "inside" },
      // A number of degrees, or "auto" to follow the EXIF orientation
      rotate: "auto",
      flip: false,
      flop: false,
      grayscale: false,
      // A sigma, or true for a fast default
      blur: false,
      sharpen: false,
      encodeOptions: { jpeg: { quality: 80 } },
    },
  },
});

Each of these can also be asked for by an asset's own name — see Query strings in asset names.

svgo

svgo minifies SVG. It also reaches an asset/inline SVG — one that becomes a data: URI rather than a file — which carries no asset name for test to match; see Embedded source.

webpack.config.js

const MinimizerPlugin = require("minimizer-webpack-plugin");

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      "...",
      new MinimizerPlugin({
        test: /\.svg(\?.*)?$/i,
        minify: {
          implementation: MinimizerPlugin.svgoMinify,
          options: {
            // Options - https://github.com/svg/svgo#configuration
            encodeOptions: {
              multipass: true,
              plugins: ["preset-default"],
            },
          },
        },
      }),
    ],
  },
};

imagemin

imagemin runs the plugins you name. Each is a string — "mozjpeg" resolves imagemin-mozjpeg — or a [name, options] pair. Install each plugin yourself.

webpack.config.js

const MinimizerPlugin = require("minimizer-webpack-plugin");

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      "...",
      new MinimizerPlugin({
        test: /\.(png|jpe?g|gif|svg)$/i,
        minify: {
          implementation: MinimizerPlugin.imageminMinify,
          options: {
            plugins: [
              "gifsicle",
              "mozjpeg",
              ["pngquant", { quality: [0.6, 0.8] }],
              "svgo",
            ],
          },
        },
      }),
    ],
  },
};

Note

A plugin that converts — imagemin-webp, imagemin-avif — writes a format the asset's name does not claim. imageminMinify keeps the original and warns instead of writing it, since it cannot rename the asset. Reach for generate with MinimizerPlugin.imageminGenerate to convert: it runs the same plugins while the module builds, which is where the asset can still take the name of the format they wrote.

@napi-rs/image

@napi-rs/image is a set of Rust codecs shipped as prebuilt binaries, so it needs no system library. It handles avif, jpeg, png and webp — the formats it both reads back reliably and makes smaller.

Each format is re-encoded as itself, using the codec that does that best: png goes through oxipng, which rewrites the container losslessly and leaves the pixels untouched, and jpeg through mozjpeg. Decoding either to pixels and re-encoding saves a small fraction as much.

webpack.config.js

const MinimizerPlugin = require("minimizer-webpack-plugin");

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      "...",
      new MinimizerPlugin({
        test: /\.(png|jpe?g|webp|avif)$/i,
        minify: {
          implementation: MinimizerPlugin.napiRsImageMinify,
          options: {
            // Options are keyed by the format's own name
            // https://github.com/Brooooooklyn/Image#usage
            encodeOptions: {
              // `PNGLosslessOptions`
              png: { force: true },
              // `JpegCompressOptions`
              jpeg: { quality: 80 },
              // `AvifConfig`
              avif: { quality: 70, speed: 4 },
              // The quality factor, 0-100
              webp: { quality: 80 },
            },
          },
        },
      }),
    ],
  },
};

Note

png is recompressed losslessly, so it takes no quality setting. Resizing and rotating are not exposed here — use sharp for those.

Query strings in asset names

An asset keeps the query and fragment of the request that made it: output.assetModuleFilename is [hash][ext][query][fragment] by default, so import icon from "./icon.svg?v=2" is emitted as <hash>.svg?v=2.

test, include and exclude are read against both spellings — the whole name and the name with the query and fragment stripped — so all three of these accept that asset:

test: /\.svg$/i; // names the file
test: /\.svg(\?.*)?$/i; // names the query form
test: /\?v=2$/; // names the query itself

exclude rejects on either spelling, so excluding by file name also excludes the queried asset. Every built-in minimizer's own filter reads past the query in the same way.

Asking sharp for something from the import. sharpMinify reads what to do off the asset's name, so one image can be sized, turned or re-encoded where it is imported rather than in the configuration:

import banner from "./banner.png?width=320&quality=80";
QueryShortValueWhat it does
widthwpixels, or autotarget width
heighthpixels, or autotarget height
unitupx (default) or percenthow width and height are read
fit-cover (default), contain, fill, …what to do when both dimensions are given
positionposcentre (default), right top, entropy, …where to crop from
backgroundbga colourwhat contain pads with
without-enlargement-flagnever scale the image up
rotaterotdegrees, or autoturn by an angle, or by the EXIF orientation
flip-flagmirror vertically
flop-flagmirror horizontally
grayscalegrayflagdrop the colour
blur-sigma, or flagblur
sharpen-sigma, or flagsharpen
qualityqa numberencode quality
lossless-flagencode losslessly, where the format has one
effort-a numberhow hard the encoder tries
progressiveprogflaginterlace, where the format has it
ImportWhat comes out
./photo.jpg?w=64&h=64&fit=cover&pos=entropya 64x64 thumbnail, cropped to the busiest part
./photo.jpg?width=16&blur&quality=30a tiny blurred placeholder
./logo.png?width=50&unit=percent&grayscalehalf-size and grey

A flag is on when it is present — ?flip — and reads true/1/yes the same way, false/0/no the other. greyscale and grey spell grayscale, auto on width or height drops one set in the minimizer's options, and a parameter can be spelled in any case.

Every one of these can also be set in the minimizer's options; the name wins where both say something, being the more specific of the two. resize: { enabled: false } still turns resizing off entirely.

Two rules decide what happens to a value:

  • Its shape is checked here, and anything else is ignored. An asset's name carries queries this plugin never put there, so ?v=2 must not become a resize — and neither does ?width=nonsense or ?flip=maybe.
  • Whether a well-formed value is one sharp accepts is sharp's to report, and it answers per format: effort runs to 6 for webp and 9 for avif, quality starts at 0 for png and 1 for jpeg, and a colour is whatever it can parse. A value outside those fails the build with sharp's own message rather than being dropped silently. An option a format has no use for — ?lossless on a jpeg — is simply ignored.

napiRsImageMinify reads the same names, as far as @napi-rs/image goes: width/w, height/h, fit (cover, fill, inside), filter (nearest, triangle, catmull-rom, gaussian, lanczos3), rotate/rot, flip, flop, grayscale, invert, blur, quality/q, lossless and speed. Three differences are worth knowing:

  • rotate takes a quarter turn, not any angle — 90, 180, 270, a negative or a multiple of those, or auto for the EXIF orientation. napi applies one orientation, so mirroring and a turn compose into it (?flip&flop is a half turn); asking for auto alongside an explicit turn warns, because only one of them can be applied.
  • Every value is checked here, rather than left to the library as sharp's are. napi rejects an unknown fit without naming it, and answers a quality of 150 by silently writing a bigger file, so a value it would mishandle is dropped instead.
  • Transforming costs the repack. This minimizer's advantage is rewriting encoded bytes — oxipng saves 99% of a png where decoding and re-encoding saves 9% — so when a query makes a transform necessary, its output is handed back to the repack rather than replacing it. A ?rotate=auto on an image whose EXIF asks for nothing skips the decode entirely.

svgoMinify reads precision (or floatPrecision, 0–10), multipass, pretty and indent, so one SVG can be laid out readably or coarsened without a second configuration:

import icon from "./icon.svg?precision=1";

imageminMinify reads nothing. Its options are a list of already-configured plugins, each with option names of its own, so there is no parameter a query could set that would mean the same thing twice over.

Note

Two assets whose names differ only by their query would be written to the same file, and webpack refuses that. To emit several sizes of one image, give each its own file name and keep the query on the asset name:

module.exports = {
  output: {
    assetModuleFilename: (pathData) => {
      const query = pathData.module.resourceResolveData.query || "";
      const name = query.replace(/^\?/, "-").replace(/[=&]/g, "-");

      return `[name]${name}[ext][query]`;
    },
  },
};

Without the trailing [query] the size is no longer on the name and nothing resizes; without the first half the two sizes collide.

Note

A query cannot pick a format on this path. Resizing needs no new name, which is why it works here; changing the format does, and minify runs after names are decided. Hand ?as=webp to generate instead, which sees the module as it builds and can emit the new format under a new name.

Images beside everything else

minify takes an array, and each minimizer is offered only the assets its own filter accepts, so one plugin instance can cover several languages at once:

const MinimizerPlugin = require("minimizer-webpack-plugin");

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new MinimizerPlugin({
        test: /\.(js|css|svg|png|jpe?g)$/i,
        minify: [
          { implementation: MinimizerPlugin.terserMinify },
          { implementation: MinimizerPlugin.cssnanoMinify },
          { implementation: MinimizerPlugin.svgoMinify },
          {
            implementation: MinimizerPlugin.sharpMinify,
            options: { encodeOptions: { png: { compressionLevel: 9 } } },
          },
        ],
      }),
    ],
  },
};

Custom Minify Function

Override the default minify function - use uglify-js for minification.

webpack.config.js

const minifyWithUglifyJs = (file, sourceMap, minimizerOptions) => {
  const uglifyJsOptions = { ...minimizerOptions };

  // The plugin fills these two in, and `uglify-js` rejects options it does not know
  delete uglifyJsOptions.ecma;
  delete uglifyJsOptions.module;

  if (sourceMap) {
    uglifyJsOptions.sourceMap = { content: sourceMap };
  }

  return require("uglify-js").minify(file, uglifyJsOptions);
};

// Which assets it is offered, and which language it minifies. Without
// `getTypes` it is never handed the JavaScript a document or a module embeds
minifyWithUglifyJs.filter = (name) => /\.[cm]?js(\?.*)?$/i.test(name);
minifyWithUglifyJs.getTypes = () => ["javascript"];

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new MinimizerPlugin({
        minify: {
          implementation: minifyWithUglifyJs,
          // https://github.com/mishoo/UglifyJS2#minify-options
          options: {/* your `uglify-js` package options */},
        },
      }),
    ],
  },
};

A minifier that nests other languages inside its own output hands each body out with renderEmbeddedSource rather than minifying it itself — see minify and Embedded source.

Typescript

With default Terser minify function:

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new MinimizerPlugin({
        minify: {
          implementation: MinimizerPlugin.terserMinify,
          options: { compress: true },
        },
      }),
    ],
  },
};

With built-in minify functions:

import { type JsMinifyOptions as SwcOptions } from "@swc/core";
import { type MinifyOptions as SwcCssOptions } from "@swc/css";
import {
  type FragmentOptions as SwcHtmlFragmentOptions,
  type Options as SwcHtmlOptions,
} from "@swc/html";
import { type OptionsOutput as CleanCssOptions } from "clean-css";
import { type Options as CssnanoOptions } from "cssnano";
import { type CompressOptions as CssoOptions } from "csso";
import { type TransformOptions as EsbuildOptions } from "esbuild";
import { type Options as HtmlMinifierTerserOptions } from "html-minifier-terser";
import { type TransformOptions as LightningCssOptions } from "lightningcss";
import { type MinifyOptions as TerserOptions } from "terser";
import { type MinifyOptions as UglifyJSOptions } from "uglify-js";

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new MinimizerPlugin<SwcOptions>({
        minify: {
          implementation: MinimizerPlugin.swcMinify,
          options: {
            // `swc` options
          },
        },
      }),
      new MinimizerPlugin<UglifyJSOptions>({
        minify: {
          implementation: MinimizerPlugin.uglifyJsMinify,
          options: {
            // `uglif-js` options
          },
        },
      }),
      new MinimizerPlugin<EsbuildOptions>({
        minify: {
          implementation: MinimizerPlugin.esbuildMinify,
          options: {
            // `esbuild` options
          },
        },
      }),

      // Alternative usage:
      new MinimizerPlugin<TerserOptions>({
        minify: {
          implementation: MinimizerPlugin.terserMinify,
          options: {
            // `terser` options
          },
        },
      }),

      // HTML minimizers
      new MinimizerPlugin<HtmlMinifierTerserOptions>({
        test: /\.html(\?.*)?$/i,
        minify: {
          implementation: MinimizerPlugin.htmlMinifierTerser,
          options: {
            // `html-minifier-terser` options
          },
        },
      }),
      new MinimizerPlugin<SwcHtmlOptions>({
        test: /\.html(\?.*)?$/i,
        minify: {
          implementation: MinimizerPlugin.swcMinifyHtml,
          options: {
            // `@swc/html` options
          },
        },
      }),
      new MinimizerPlugin<SwcHtmlFragmentOptions>({
        test: /\.template\.html$/i,
        minify: {
          implementation: MinimizerPlugin.swcMinifyHtmlFragment,
          options: {
            // `@swc/html` fragment options
          },
        },
      }),

      // CSS minimizers
      new MinimizerPlugin<CssnanoOptions>({
        test: /\.css(\?.*)?$/i,
        minify: {
          implementation: MinimizerPlugin.cssnanoMinify,
          options: {
            // `cssnano` options
          },
        },
      }),
      new MinimizerPlugin<CssoOptions>({
        test: /\.css(\?.*)?$/i,
        minify: {
          implementation: MinimizerPlugin.cssoMinify,
          options: {
            // `csso` options
          },
        },
      }),
      new MinimizerPlugin<CleanCssOptions>({
        test: /\.css(\?.*)?$/i,
        minify: {
          implementation: MinimizerPlugin.cleanCssMinify,
          options: {
            // `clean-css` options
          },
        },
      }),
      new MinimizerPlugin<EsbuildOptions>({
        test: /\.css(\?.*)?$/i,
        minify: {
          implementation: MinimizerPlugin.esbuildMinifyCss,
          options: {
            // `esbuild` options (CSS loader)
          },
        },
      }),
      new MinimizerPlugin<LightningCssOptions>({
        test: /\.css(\?.*)?$/i,
        minify: {
          implementation: MinimizerPlugin.lightningCssMinify,
          options: {
            // `lightningcss` options
          },
        },
      }),
      new MinimizerPlugin<SwcCssOptions>({
        test: /\.css(\?.*)?$/i,
        minify: {
          implementation: MinimizerPlugin.swcMinifyCss,
          options: {
            // `@swc/css` options
          },
        },
      }),
    ],
  },
};

Migrating from image-minimizer-webpack-plugin

This plugin does what image-minimizer-webpack-plugin did, for every asset type rather than images alone, so one plugin covers a build instead of two. The options line up like this:

image-minimizer-webpack-pluginhere
minimizerminify, same shape
minimizer.implementationimplementation on that minimizer
minimizer.optionsoptions on that minimizer
minimizer.filtera filter on the minimizer itself
generator[].implementationgenerate
generator[].optionsoptions on that generator
generator[].presetthe key the generator is written under
generator[].typetype on that generator
generator[].filename / .filterfilename / filter on that generator
deleteOriginalAssetsdeleteOriginalAssets on that generator
concurrencyparallel
test / include / excludeunchanged
loadernothing — generate reaches a module without one
severityErrornothing — a failed minimizer is an error

The minimizers and generators keep their names — imageminMinify, imageminGenerate, imageminNormalizeConfig, sharpMinify, sharpGenerate, svgoMinify — so only the plugin they are read off changes. squooshMinify and squooshGenerate are not carried over: @squoosh/lib is unmaintained, and image-minimizer-webpack-plugin already marked both deprecated.

A minimizer carries over as it is written: minimizer becomes minify, with the same { implementation, options } object, and an array of them stays an array.

Before

const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");

module.exports = {
  plugins: [
    new ImageMinimizerPlugin({
      test: /\.(jpe?g|png)$/i,
      minimizer: {
        implementation: ImageMinimizerPlugin.sharpMinify,
        options: { encodeOptions: { jpeg: { quality: 80 } } },
      },
    }),
  ],
};

After

const MinimizerPlugin = require("minimizer-webpack-plugin");

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new MinimizerPlugin({
        test: /\.(jpe?g|png)$/i,
        minify: {
          implementation: MinimizerPlugin.sharpMinify,
          options: { encodeOptions: { jpeg: { quality: 80 } } },
        },
      }),
    ],
  },
};

A generator is where two shapes changed rather than moved. Generators are named here instead of listed, because a name is what ?as= asks for, so an array of two generators becomes an object of two entries. And a generator that produced a file beside the original is written with type: "asset", which is the default there and not here — without it a generator re-encodes the module itself, which is what lets the import be renamed.

Before

const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");

module.exports = {
  plugins: [
    new ImageMinimizerPlugin({
      test: /\.(jpe?g|png)$/i,
      generator: [
        {
          type: "asset",
          preset: "webp",
          implementation: ImageMinimizerPlugin.sharpGenerate,
          options: { encodeOptions: { webp: { quality: 90 } } },
        },
      ],
    }),
  ],
};

After

const MinimizerPlugin = require("minimizer-webpack-plugin");

module.exports = {
  plugins: [
    new MinimizerPlugin({
      test: /\.(jpe?g|png)$/i,
      generate: {
        webp: {
          type: "asset",
          implementation: MinimizerPlugin.sharpGenerate,
          options: { encodeOptions: { webp: { quality: 90 } } },
        },
      },
    }),
  ],
};

Contributing

We welcome all contributions! If you're new here, please take a moment to review our contributing guidelines before submitting issues or pull requests.

CONTRIBUTING

License

MIT

ModuleConcatenationPlugin

In the past, one of webpack’s trade-offs when bundling was that each module in your bundle would be wrapped in individual function closures. These wrapper functions made it slower for your JavaScript to execute in the browser. In comparison, tools like Closure Compiler and RollupJS ‘hoist’ or concatenate the scope of all your modules into one closure and allow for your code to have a faster execution time in the browser.

This plugin will enable the same concatenation behavior in webpack. By default this plugin is already enabled in production mode and disabled otherwise. If you need to override the production mode optimization, set the optimization.concatenateModules option to false. To enable concatenation behavior in other modes, you can add ModuleConcatenationPlugin manually or use the optimization.concatenateModules option:

new webpack.optimize.ModuleConcatenationPlugin();

This concatenation behavior is called “scope hoisting.”

Scope hoisting is specifically a feature made possible by ECMAScript Module syntax. Because of this webpack may fallback to normal bundling based on what kind of modules you are using, and other conditions.

Optimization Bailouts

As the article explains, webpack attempts to achieve partial scope hoisting. It will merge modules into a single scope but cannot do so in every case. If webpack cannot merge a module, the two alternatives are Prevent and Root. Prevent means the module must be in its own scope. Root means a new module group will be created. The following conditions determine the outcome:

ConditionOutcome
Non ES6 ModulePrevent
Imported By Non ImportRoot
Imported From Other ChunkRoot
Imported By Multiple Other Module GroupsRoot
Imported With import()Root
Affected By ProvidePlugin Or Using modulePrevent
Uses HMR (module.hot)Prevent
Using eval()Prevent
Not in any chunkPrevent
In different chunk(s) than the importerRoot
export * from "cjs-module"Prevent

Module Grouping Algorithm

The following pseudo JavaScript explains the algorithm:

function tryToAdd(group, module) {
  if (group.has(module)) {
    return true;
  }
  if (!hasPreconditions(module)) {
    return false;
  }
  const nextGroup = group;
  const result = module.dependents.reduce(
    (check, dependent) => check && tryToAdd(nextGroup, dependent),
    true,
  );
  if (!result) {
    return false;
  }
  for (const dependency of module.dependencies) {
    tryToAdd(group, dependency);
  }
  group.merge(nextGroup);
  return true;
}

for (const module of modules) {
  const group = new ModuleGroup({
    root: module,
  });
  for (const dependency of module.dependencies) {
    tryToAdd(group, dependency);
  }
  if (group.modules.length > 1) {
    orderedModules = topologicalSort(group.modules);
    concatenatedModule = new ConcatenatedModule(orderedModules);
    chunk.add(concatenatedModule);
    for (const groupModule of orderedModules) {
      chunk.remove(groupModule);
    }
  }
}

Debugging Optimization Bailouts

When using the webpack CLI, the --stats-optimization-bailout flag will display bailout reasons. When using the webpack config, add the following to the stats object:

export default {
  // ...
  stats: {
    // Display bailout reasons
    optimizationBailout: true,
  },
};

ModuleFederationPlugin

The ModuleFederationPlugin allows a build to provide or consume modules with other independent builds at runtime.

import webpack from "webpack";

const { ModuleFederationPlugin } = webpack.container;

export default {
  plugins: [
    new ModuleFederationPlugin({
      // options' typings in typescript
      runtime: string | false,
    }),
  ],
};

Options

name

string

The name of the container this build produces. For the default script remote type it is also the global variable the container is assigned to, which is how a consumer reaches it once remoteEntry.js has loaded.

It must be unique among the builds on a page. It is a separate thing from output.uniqueName, which also has to be unique — see Every build needs its own unique name.

filename

string

The filename of the container entry, as a path relative to output.path. Conventionally remoteEntry.js.

This names the file this build emits. It has nothing to do with the filenames of the remotes it consumes — those are whatever each remote chose, and appear in the URLs under remotes.

remotes

string[] object

The containers this build consumes. Each entry maps a request scope — the name you import through — to a container location:

new ModuleFederationPlugin({
  remotes: {
    // request scope        container location
    app1: "app1@http://localhost:3001/remoteEntry.js",
  },
});

which is then imported as:

import Button from "app1/Button";

The two halves of the location string are split at the first @:

PartMeaning
before @ — app1the remote's name, i.e. the global its container is assigned to
after @ — http://localhost:3001/remoteEntry.jsthe URL its filename is served from

So a name appearing "twice" in a config is usually two different things that happen to match: the key is what you import through and can be renamed freely, while the part before @ must match what the remote called itself. They are free to differ:

new ModuleFederationPlugin({
  remotes: {
    // imported as "checkout/…", served by a container that calls itself "shop"
    checkout: "shop@http://localhost:3001/remoteEntry.js",
  },
});

Passing an array instead of an object drops the keys and lets webpack infer each request scope from the location.

For a statically configured remote you do not load anything yourself: webpack emits code that injects the <script>, waits for the global to appear, and reports ScriptExternalLoadError if it does not. Loading the container by hand is only needed for dynamic remotes, where the location is not known at build time.

An entry can also be an object with external (the location) and shareScope, when one remote needs a different share scope from the rest.

remoteType

string

How a remote is fetched, as an externalsType.

When unset it falls back to this plugin's own library.type, but only if that is a valid externals type, and otherwise to 'script' — the name@url form described above. Since library itself defaults to { type: 'var', name }, a config that sets neither gets 'script'.

runtime

Create a new runtime chunk with the specified name.

webpack.config.js

import webpack from "webpack";

const { ModuleFederationPlugin } = webpack.container;

export default {
  plugins: [
    new ModuleFederationPlugin({
      runtime: "my-runtime-name",
    }),
  ],
};

exposes

The modules this container makes available to other builds. The property name is the public name a consumer imports (import Button from "remote/Button"), the value the module to expose:

webpack.config.js

new ModuleFederationPlugin({
  name: "remote",
  exposes: {
    "./Button": "./src/components/Button",
  },
});

An entry can also be an object:

  • import (string): the module to expose.
  • name (string): the name of the chunk webpack generates for it. Without it the chunk is named after the module's internal id, which changes as the build changes; setting it gives the file a stable name that can be referenced statically.
new ModuleFederationPlugin({
  name: "remote",
  exposes: {
    "./Button": {
      import: "./src/components/Button",
      name: "button-chunk",
    },
  },
});

Sharing libraries

With the shared key in the configuration you can define libraries that are shared between your federated modules. The package name is the same as the one found in the dependencies section of your package.json. However, by default webpack will only share the root level of a library.

import webpack from "webpack";

const { ModuleFederationPlugin } = webpack.container;

export default {
  plugins: [
    new ModuleFederationPlugin({
      // adds date-fns as shared module
      shared: ["date-fns"],
    }),
  ],
};

So in your application you could do something like

import { format } from "date-fns";

format(new Date(2014, 1, 11), "MM/dd/yyyy");

and webpack will automatically share date-fns between all your federated modules that define date-fns as a shared library. However, if you want to access something that is not located at the root level of the package, for example date-fns/locale/en-GB/index.js, you need to append / to the package name in your shared configuration:

import webpack from "webpack";

const { ModuleFederationPlugin } = webpack.container;

export default {
  plugins: [
    new ModuleFederationPlugin({
      // adds date-fns as shared module
      // all files of the package will be shared
      shared: ["date-fns/"],
    }),
  ],
};

The / syntax allows you to access all files of a package. However, it should be used only where necessary, because it has an impact on performance especially in development mode.

Specify package versions

There are three ways to specify the versions of shared libraries.

Array syntax

This syntax allows you to share libraries with package name only. This approach is good for prototyping, but it will not allow you to scale to large production environment given that libraries like react and react-dom will require additional requirements.

import webpack from "webpack";

const { ModuleFederationPlugin } = webpack.container;

export default {
  plugins: [
    new ModuleFederationPlugin({
      // adds lodash as shared module
      // version is inferred from package.json
      // requiredVersion is inferred from package.json as well
      // (a warning is emitted if it can't be determined)
      // a shared version that doesn't satisfy it falls back to the local copy
      shared: ["lodash"],
    }),
  ],
};
Object syntax

This syntax provides you more control over each shared library in which you can define package name as the key and version (semver) as the value.

export default {
  plugins: [
    new ModuleFederationPlugin({
      shared: {
        // adds lodash as shared module
        // version is inferred from package.json
        // it will use the highest lodash version that is >= 4.17 and < 5
        lodash: "^4.17.0",
      },
    }),
  ],
};
Object syntax with sharing hints

This syntax allows you to provide additional hints to each shared package where you define the package name as the key, and the value as an object containing hints to modify sharing behavior.

import { readFileSync } from "node:fs";

const deps = JSON.parse(
  readFileSync(new URL("./package.json", import.meta.url)),
).dependencies;

export default {
  plugins: [
    new ModuleFederationPlugin({
      shared: {
        // adds react as shared module
        react: {
          requiredVersion: deps.react,
          singleton: true,
        },
      },
    }),
  ],
};

Sharing hints

eager

boolean

This hint will allow webpack to include the provided and fallback module directly instead of fetching the library via an asynchronous request. In other words, this allows to use this shared module in the initial chunk. Also, be careful that all provided and fallback modules will always be downloaded when this hint is enabled.

import

false | string

The provided module that should be placed in the shared scope. This provided module also acts as fallback module if no shared module is found in the shared scope or version isn't valid. (The value for this hint defaults to the property name.)

packageName

string

The package name that is used to determine required version from description file. This is only needed when the package name can't be automatically determined from request.

requiredVersion

false string

This field specifies the required version of the package. It accepts semantic versioning, such as "^1.2.3". Additionally, it retrieves the version if it's provided as a URL, for instance: "git+ssh://git@github.com:foo/bar.git#v1.0.0".

webpack implements the full range grammar, so a required version can be an exact version (1.2.3), a partial one (1.2, which matches any patch), a comparator (>=1.2.3 <2), a caret or tilde range (^1.2.3, ~1.2.3), a hyphen range (1.2.3 - 2.3.4) or several of those joined with ||. Set it to false to accept whatever the scope offers.

A share scope can hold several versions of the same module at once — each build provides the version it has, and a consumer gets the highest one that satisfies its own requiredVersion. That is what lets two builds that need different major versions of a library work on one page, at the cost of shipping both. singleton: true gives up that guarantee to keep one instance, and warns when the chosen version does not satisfy a consumer.

shareKey

string

The requested shared module is looked up under this key from the shared scope. It defaults to the key you used in shared, i.e. the request itself, so shared: ['lodash'] is stored and looked up as lodash.

Set it when the name a build imports and the name it should share under differ. Two builds only share a module when both the key and the scope match, so the key is what lets an application that imports lodash-es reuse what another build provided as lodash:

new ModuleFederationPlugin({
  shared: {
    "lodash-es": {
      shareKey: "lodash",
    },
  },
});
shareScope

string

The name of the shared scope, defaulting to the plugin's own shareScope option and, when that is unset, to 'default'. A scope is one namespace of shared modules: builds put the modules they provide into it and look the ones they need up from it, so builds using different scope names never share with each other even when their keys match.

Change it to keep groups of builds apart — for example when a page hosts two independent sets of remotes that should each get their own copy of a library, or when a widget must not adopt the host's version of one. Both the providing and the consuming side have to name the same scope.

singleton

boolean

This hint only allows a single version of the shared module in the shared scope (disabled by default). Some libraries use a global internal state (e.g. react, react-dom). Thus, it is critical to have only one instance of the library running at a time.

In cases where there are multiple versions of the same dependency in the shared scope, the highest semantic version is used.

strictVersion

boolean

This hint allows webpack to reject the shared module if version is not valid (defaults to true when local fallback module is available and shared module is not a singleton, otherwise false, it has no effect if there is no required version specified). Throws a runtime error if the required version is not found.

version

false | string

The version of the provided module. It allows webpack to replace lower matching versions, but not higher.

By default, webpack uses the version from the package.json file of the dependency.

Additional examples

export default {
  plugins: [
    new ModuleFederationPlugin({
      // adds vue as shared module
      // version is inferred from package.json
      // it will always use the shared version, but print a warning when the shared vue is < 2.6.5 or >= 3
      shared: {
        vue: {
          requiredVersion: "^2.6.5",
          singleton: true,
        },
      },
    }),
  ],
};
export default {
  plugins: [
    new ModuleFederationPlugin({
      // adds vue as shared module
      // there is no local version provided
      // it will emit a warning if the shared vue is < 2.6.5 or >= 3
      shared: {
        vue: {
          import: false,
          requiredVersion: "^2.6.5",
        },
      },
    }),
  ],
};
export default {
  plugins: [
    new ModuleFederationPlugin({
      // adds vue as shared module
      // there is no local version provided
      // it will throw an error when the shared vue is < 2.6.5 or >= 3
      shared: {
        vue: {
          import: false,
          requiredVersion: "^2.6.5",
          strictVersion: true,
        },
      },
    }),
  ],
};
export default {
  plugins: [
    new ModuleFederationPlugin({
      shared: {
        "my-vue": {
          // can be referenced by import "my-vue"
          import: "vue", // the "vue" package will be used as a provided and fallback module
          shareKey: "shared-vue", // under this name the shared module will be placed in the share scope
          shareScope: "default", // share scope with this name will be used
          singleton: true, // only a single version of the shared module is allowed
          strictVersion: true, // don't use shared version when version isn't valid. Singleton or modules without fallback will throw, otherwise fallback is used
          version: "1.2.3", // the version of the shared module
          requiredVersion: "^1.0.0", // the required version of the shared module
        },
      },
    }),
  ],
};

NoEmitOnErrorsPlugin

The NoEmitOnErrorsPlugin allows you to avoid emitting assets when there are any errors. It is enabled by default in production mode, where optimization.emitOnErrors defaults to false; use that option to enable or disable it.

webpack.config.js

export default {
  plugins: [new webpack.NoEmitOnErrorsPlugin()],
};

NormalModuleReplacementPlugin

The NormalModuleReplacementPlugin allows you to replace resources that match resourceRegExp with newResource. If newResource is relative, it is resolved relative to the previous resource. If newResource is a function, it is expected to overwrite the request attribute of the supplied resource.

This can be useful for allowing different behaviour between builds.

new webpack.NormalModuleReplacementPlugin(resourceRegExp, newResource);

Note that the resourceRegExp is tested twice: against the request as written in your code before resolving (e.g. './sum' for import sum from './sum') and against the resolved absolute resource path after resolving (e.g. '/project/src/sum.js').

Also please note that when using Windows, you have to accommodate the different folder separator symbol. E.g. /src\/environments\/environment\.ts/ won't work on Windows, you have to use /src[\\/]environments[\\/]environment\.ts/, instead.

Basic Example

Replace a specific module when building for a development environment.

Say you have a configuration file some/path/config.development.module.js and a special version for production in some/path/config.production.module.js

Add the following plugin when building for production:

new webpack.NormalModuleReplacementPlugin(
  /some\/path\/config\.development\.js/,
  "./config.production.js",
);

Advanced Example

Conditional build depending on an specified environment.

Say you want a configuration with specific values for different build targets.

export default (env) => {
  const appTarget = env.APP_TARGET || "VERSION_A";
  return {
    plugins: [
      new webpack.NormalModuleReplacementPlugin(/-APP_TARGET$/, (resource) => {
        resource.request = resource.request.replace(
          /-APP_TARGET/,
          `-${appTarget}`,
        );

        if (resource.createData) {
          resource.createData.request = resource.request;
        }
      }),
    ],
  };
};

Create the two configuration files:

app/config-VERSION_A.js

export default {
  title: "I am version A",
};

app/config-VERSION_B.js

export default {
  title: "I am version B",
};

Then import that configuration using the keyword you're looking for in the regexp:

import config from "app/config-APP_TARGET";

console.log(config.title);

And now you get the right configuration imported depending on which target you're building for:

npx webpack --env APP_TARGET=VERSION_A
=> 'I am version A'

npx webpack --env APP_TARGET=VERSION_B
=> 'I am version B'

PrefetchPlugin

Prefetch normal module requests, causing them to be resolved and built before the first import or require of that module occurs. Using this plugin can boost performance. Try to profile the build first to determine clever prefetching points.

import webpack from "webpack";

new webpack.PrefetchPlugin([context], request);

Options

  • context: An absolute path to a directory
  • request: A request string for a normal module

What it does

webpack discovers modules by walking the dependency graph: a module deep in an import chain is only resolved once everything above it has been parsed. A long chain therefore builds partly in sequence, even though webpack processes modules in parallel wherever it can.

This plugin starts one such chain at the very beginning of the compilation instead, when webpack would otherwise be waiting for the first modules to be parsed. By the time the real import is found, the module is already built and the result is reused.

import path from "node:path";
import webpack from "webpack";

export default {
  plugins: [
    // start building the heaviest dependency right away
    new webpack.PrefetchPlugin(path.resolve("./src"), "./deeply/nested/heavy"),
  ],
};

Prefetching a module that the build never imports only wastes the work: the module ends up in no chunk and nothing is emitted for it. That makes profiling the build first worthwhile — see Build performance — because the win depends entirely on picking a request that is both expensive and discovered late.

ProfilingPlugin

Generate Chrome profile file which includes timings of plugins execution. Outputs events.json file by default. It is possible to provide custom file path using outputPath option.

Note : ProfilingPlugin accepts only absolute paths.

Options

  • outputPath: An absolute path to a custom output file (json)

Usage: default

new webpack.debug.ProfilingPlugin();

Usage: custom outputPath

import path from "node:path";
import { fileURLToPath } from "node:url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

new webpack.debug.ProfilingPlugin({
  outputPath: path.join(__dirname, "profiling/profileEvents.json"),
});

In order to view the profile file:

  1. Run webpack with ProfilingPlugin.
  2. Go to Chrome, open DevTools, and go to the Performance tab (formerly Timeline).
  3. Drag and drop generated file (events.json by default) into the profiler.

It will then display timeline stats and calls per plugin!

ProgressPlugin

The ProgressPlugin provides a way to customize how progress is reported during a compilation.

Usage

Create an instance of ProgressPlugin and provide one of the allowed params, besides, there's a static method createDefaultHandler which can be used to customize the default handler.

Providing function

Provide a handler function which will be called when hooks report progress. handler function arguments:

  • percentage: a number between 0 and 1 indicating the completion percentage of the compilation
  • message: a short description of the currently-executing hook
  • ...args: zero or more additional strings describing the current progress
const handler = (percentage, message, ...args) => {
  // e.g. Output each progress message directly to the console:
  console.info(percentage, message, ...args);
};

new webpack.ProgressPlugin(handler);

Providing object

When providing an object to the ProgressPlugin, following properties are supported:

  • activeModules (boolean = false): Shows active modules count and one active module in progress message.
  • entries (boolean = true): Shows entries count in progress message.
  • handler (See Providing function)
  • modules (boolean = true): Shows modules count in progress message.
  • modulesCount (number = 5000): A minimum modules count to start with. Takes effect when modules property is enabled.
  • profile (boolean = false): Tells ProgressPlugin to collect profile data for progress steps.
  • dependencies (boolean = true): Shows the count of dependencies in progress message.
  • dependenciesCount (number = 10000): A minimum dependencies count to start with. Takes effect when dependencies property is enabled.
  • percentBy ('entries' | 'dependencies' | 'modules' | null = null): Tells ProgressPlugin how to calculate progress percentage.
  • progressBar (boolean | 'auto' | object = false) 5.107.0+: Renders a progress bar in the terminal instead of the plain percentage message. Only takes effect with the default handler, i.e. when no custom handler is provided. Pass true to enable it with defaults, 'auto' 5.109.0+ to enable it only in interactive terminals (TTY), or an object with the following properties:
    • name (string = 'Build'): Name shown before the progress bar.
    • color (string = 'green'): Color used for the filled portion of the bar. Falls back to 'green' when the given color is not supported.
    • width (number = 25) 5.109.0+: Width of the progress bar in characters.
  • estimatedTime (boolean = false) 5.109.0+: Shows the estimated time remaining based on build progress.
  • phaseTimings (boolean = false) 5.109.0+: Shows a timing breakdown for each build phase when the build completes.
new webpack.ProgressPlugin({
  activeModules: false,
  entries: true,
  handler(percentage, message, ...args) {
    // custom logic
  },
  modules: true,
  modulesCount: 5000,
  profile: false,
  dependencies: true,
  dependenciesCount: 10000,
  percentBy: null,
});

Rendering a progress bar:

new webpack.ProgressPlugin({
  progressBar: {
    name: "Build",
    color: "green",
  },
});

webpack.ProgressPlugin.createDefaultHandler

If the default handler of ProgressPlugin does not meet your requirements, you can customize it using the static ProgressPlugin.createDefaultHandler method.

static createDefaultHandler: (
  profile: undefined | null | boolean,
  logger: WebpackLogger,
  options?: {
    progressBar?: { name: string; color: string; width: number } | false;
    estimatedTime?: boolean;
    phaseTimings?: boolean;
  },
) => (percentage: number, msg: string, ...args: string[]) => void;

Percentage calculation

By default, the progress percentage is the median of three ratios: built modules to total modules, finished entries to total entries and processed dependencies to total dependencies. Setting percentBy uses one of these ratios instead.

The total modules count is unknown in advance and changes during the build. This may cause inaccurate progress percentage.

To solve this problem ProgressPlugin caches the last known total modules count and reuses this value on the next build. The first build will warm the cache but the following builds will use and update this value.

We recommend using percentBy: 'entries' setting for projects with multiple configured entry points. Percentage calculation will become more accurate because the amount of entry points is known in advance.

Supported Hooks

The following hooks report progress information to ProgressPlugin.

Compiler

  • beforeRun*
  • run*
  • watchRun*
  • normalModuleFactory*
  • contextModuleFactory*
  • beforeCompile*
  • compile*
  • thisCompilation*
  • compilation*
  • make
  • finishMake*
  • emit*
  • afterEmit*
  • done*
  • watchClose*

Compiler cache (compiler.cache.hooks)

  • endIdle*
  • storeBuildDependencies*
  • shutdown*
  • beginIdle*

Compilation

  • buildModule (only when activeModules is enabled)
  • addEntry
  • failedEntry
  • succeedEntry
  • finishModules*
  • seal*
  • optimizeDependencies*
  • afterOptimizeDependencies*
  • beforeChunks*
  • afterChunks*
  • optimize*
  • optimizeModules*
  • afterOptimizeModules*
  • optimizeChunks*
  • afterOptimizeChunks*
  • optimizeTree*
  • afterOptimizeTree*
  • optimizeChunkModules*
  • afterOptimizeChunkModules*
  • reviveModules*
  • beforeModuleIds*
  • moduleIds*
  • optimizeModuleIds*
  • afterOptimizeModuleIds*
  • reviveChunks*
  • beforeChunkIds*
  • chunkIds*
  • optimizeChunkIds*
  • afterOptimizeChunkIds*
  • recordModules*
  • recordChunks*
  • beforeModuleHash*
  • beforeCodeGeneration*
  • beforeRuntimeRequirements*
  • beforeHash*
  • afterHash*
  • recordHash*
  • beforeModuleAssets*
  • beforeChunkAssets*
  • processAssets*
  • afterProcessAssets*
  • record*
  • afterSeal*

Source

ProvidePlugin

Automatically load modules instead of having to import or require them everywhere.

new webpack.ProvidePlugin({
  identifier: "module1",
  // ...
});

or

new webpack.ProvidePlugin({
  identifier: ["module1", "property1"],
  // ...
});

By default, module resolution path is current folder (./**) and node_modules.

It is also possible to specify full path:

import path from "node:path";
import { fileURLToPath } from "node:url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

new webpack.ProvidePlugin({
  identifier: path.resolve(path.join(__dirname, "src/module1")),
  // ...
});

Whenever the identifier is encountered as free variable in a module, the module is loaded automatically and the identifier is filled with the exports of the loaded module (or property in order to support named exports).

For importing the default export of an ES2015 module, you have to specify the default property of module.

Usage: jQuery

To automatically load jquery we can point both variables it exposes to the corresponding node module:

new webpack.ProvidePlugin({
  $: "jquery",
  jQuery: "jquery",
});

Then in any of our source code:

// in a module
$("#item"); // <= works
jQuery("#item"); // <= also works
// $ is automatically set to the exports of module "jquery"

Usage: jQuery with Angular 1

Angular looks for window.jQuery in order to determine whether jQuery is present, see the source code.

new webpack.ProvidePlugin({
  "window.jQuery": "jquery",
});

Usage: Lodash Map

new webpack.ProvidePlugin({
  _map: ["lodash", "map"],
});

Usage: Vue.js

new webpack.ProvidePlugin({
  Vue: ["vue/dist/vue.esm.js", "default"],
});

SourceMapDevToolPlugin

This plugin enables more fine grained control of source map generation. It is also enabled automatically by certain settings of the devtool configuration option.

new webpack.SourceMapDevToolPlugin(options);

Options

The following options are supported:

type Matcher =
  | string
  | RegExp
  | ((name: string) => boolean)
  | (string | RegExp | ((name: string) => boolean))[];

A string matches by prefix, a RegExp by test, and a function is called with the name and returns whether it matches.

  • test (Matcher): Include source maps for modules based on their extension (defaults to .js, .cjs, .mjs, and .css).

  • include (Matcher): Include source maps for module paths that match the given value.

  • exclude (Matcher): Exclude modules that match the given value from source map generation.

  • filename (string): Defines the output filename of the SourceMap (will be inlined if no value is provided).

  • append (string (pathData: PathData, assetInfo?: AssetInfo) => string false): Appends the given value to the original asset. Usually the #sourceMappingURL comment. [url] is replaced with a URL to the source map file. Since webpack v4.36.0, path parameters are supported: [chunk], [filename] and [contenthash]. Setting append to false disables the appending.

    Starting from version 5.84.0, webpack allows the append option to be a function that accepts path data and an asset info object as arguments, and returns a string.

    (pathData: PathData, assetInfo?: AssetInfo) => string;
  • moduleFilenameTemplate (string): See output.devtoolModuleFilenameTemplate.

  • fallbackModuleFilenameTemplate (string): See link above.

  • ignoreList (Matcher): Decide whether to ignore source files that match the specified value in source maps.

  • module = true (boolean): Indicates whether loaders should generate source maps.

  • columns = true (boolean): Indicates whether column mappings should be used.

  • namespace (string): Namespace prefix to allow multiple webpack roots in the devtools. See output.devtoolNamespace.

  • noSources = false (boolean): Prevents the source file content from being included in the source map.

  • publicPath (string): Emits absolute URLs with public path prefix, e.g. https://example.com/project/.

  • fileContext (string): Makes the [file] argument relative to this directory.

  • sourceRoot (string): Provide a custom value for the sourceRoot property in the SourceMap.

  • debugIds (boolean): If true, unique ids will be emitted in source and sourcemaps which streamlines identifying sourcemaps across different builds. See the TC39 sourcemap debug ID proposal for more details.

The fileContext option is useful when you want to store source maps in an upper level directory to avoid ../../ appearing in the absolute [url].

Examples

The following examples demonstrate some common use cases for this plugin.

Basic Use Case

You can use the following code to replace the configuration option devtool: inline-source-map with an equivalent custom plugin configuration:

export default {
  // ...
  devtool: false,
  plugins: [new webpack.SourceMapDevToolPlugin({})],
};

Exclude Vendor Maps

The following code would exclude source maps for any modules in the vendor.js bundle:

new webpack.SourceMapDevToolPlugin({
  filename: "[file].map[query]",
  exclude: ["vendor.js"],
});

Host Source Maps Externally

Set a URL for source maps. Useful for hosting them on a host that requires authorization.

new webpack.SourceMapDevToolPlugin({
  append: "\n//# sourceMappingURL=https://example.com/sourcemap/[url]",
  filename: "[file].map[query]",
});

And for cases when source maps are stored in the upper level directory:

project
|- dist
  |- public
    |- bundle-[hash].js
  |- sourcemaps
    |- bundle-[hash].js.map

With the following config:

new webpack.SourceMapDevToolPlugin({
  filename: "sourcemaps/[file].map",
  publicPath: "https://example.com/project/",
  fileContext: "public",
});

Will produce the following URL:

https://example.com/project/sourcemaps/bundle-[hash].js.map

SplitChunksPlugin

Originally, chunks (and modules imported inside them) were connected by a parent-child relationship in the internal webpack graph. The CommonsChunkPlugin was used to avoid duplicated dependencies across them, but further optimizations were not possible.

Since webpack v4, the CommonsChunkPlugin was removed in favor of optimization.splitChunks.

Defaults

Out of the box SplitChunksPlugin should work well for most users.

By default it only affects on-demand chunks, because changing initial chunks would affect the script tags the HTML file should include to run the project.

Webpack will automatically split chunks based on these conditions:

  • New chunk can be shared OR modules are from the node_modules folder
  • New chunk would be bigger than 20kb (before min+gz; 10kb outside production mode)
  • Maximum number of parallel requests when loading chunks on demand would be lower or equal to 30 (unlimited outside production mode)
  • Maximum number of parallel requests at initial page load would be lower or equal to 30 (unlimited outside production mode)

When trying to fulfill the last two conditions, bigger chunks are preferred.

Configuration

Webpack provides a set of options for developers that want more control over this functionality.

optimization.splitChunks

This configuration object represents the default behavior of the SplitChunksPlugin in production mode; the comments give the values used in the other modes.

webpack.config.js

export default {
  // ...
  optimization: {
    splitChunks: {
      chunks: "async",
      minSize: 20000, // 10000 in other modes
      minRemainingSize: 20000, // equals minSize; 0 in development mode
      minChunks: 1,
      maxAsyncRequests: 30, // Infinity in other modes
      maxInitialRequests: 30, // Infinity in other modes
      enforceSizeThreshold: 50000, // 30000 in other modes
      cacheGroups: {
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          reuseExistingChunk: true,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

splitChunks.automaticNameDelimiter

string = '-'

By default webpack will generate names using origin and name of the chunk (e.g. vendors-main.js). This option lets you specify the delimiter to use for the generated names.

splitChunks.chunks

'initial' | 'async' | 'all' = 'async' RegExp (chunk: Chunk) => boolean | undefined

This indicates which chunks will be selected for optimization. When a string is provided, valid values are all, async, and initial:

  • 'initial' selects chunks that a browser loads up front, before any code of the application runs — the chunks that belong to an entry point, including its runtime and anything it statically imports.
  • 'async' selects chunks that are loaded on demand, i.e. chunks created by import() and other asynchronous dependencies.
  • 'all' selects both.

A chunk counts as initial when at least one of the chunk groups it belongs to is loaded with the page, so a chunk that is part of an entry point and is additionally pulled in by an import() somewhere else is treated as initial rather than async. Providing all can be particularly powerful, because it means that chunks can be shared even between async and non-async chunks.

Note that it is applied to the fallback cache group as well (splitChunks.fallbackCacheGroup.chunks).

webpack.config.js

export default {
  // ...
  optimization: {
    splitChunks: {
      // include all types of chunks
      chunks: "all",
    },
  },
};

Alternatively, you may provide a function for more control. The return value will indicate whether to include each chunk.

export default {
  // ...
  optimization: {
    splitChunks: {
      chunks(chunk) {
        // exclude `my-excluded-chunk`
        return chunk.name !== "my-excluded-chunk";
      },
    },
  },
};

If you are using webpack version 5.86.0 or later, you can also pass a regular expression:

export default {
  // ...
  optimization: {
    splitChunks: {
      chunks: /foo/,
    },
  },
};

splitChunks.maxAsyncRequests

number

Maximum number of parallel requests when on-demand loading.

The default value of splitChunks.maxAsyncRequests depends on the mode:

ModeDefault
"production"30
"development"Infinity
"none"Infinity

splitChunks.maxInitialRequests

number

Maximum number of parallel requests at an entry point.

The default value of splitChunks.maxInitialRequests depends on the mode:

ModeDefault
"production"30
"development"Infinity
"none"Infinity

splitChunks.defaultSizeTypes

string[] = ['javascript', 'css', 'unknown']

Sets the size types which are used when a number is used for sizes. When the built-in CSS support (experiments.css) is disabled, the default is ['javascript', 'unknown'].

splitChunks.minChunks

number = 1

The minimum times must a module be shared among chunks before splitting.

splitChunks.hidePathInfo

boolean

Prevents exposing path info when creating names for parts splitted by maxSize.

splitChunks.minSize

number Record<string, number>

Minimum size, in bytes, for a chunk to be generated.

The default value of splitChunks.minSize depends on the mode:

ModeDefault
"production"20000
"development"10000
"none"10000

splitChunks.minSizeReduction

number Record<string, number>

Minimum size reduction to the main chunk (bundle), in bytes, needed for a chunk to be generated. Meaning if splitting into a chunk does not reduce the size of the main chunk (bundle) by the given amount of bytes, it won't be split, even if it meets the splitChunks.minSize value.

splitChunks.enforceSizeThreshold

splitChunks.cacheGroups.{cacheGroup}.enforceSizeThreshold

number

Size threshold at which splitting is enforced and other restrictions (minRemainingSize, maxAsyncRequests, maxInitialRequests) are ignored.

The default value of splitChunks.enforceSizeThreshold depends on the mode:

ModeDefault
"production"50000
"development"30000
"none"30000

splitChunks.minRemainingSize

splitChunks.cacheGroups.{cacheGroup}.minRemainingSize

number

splitChunks.minRemainingSize option was introduced in webpack 5 to avoid zero sized modules by ensuring that the minimum size of the chunk which remains after splitting is above a limit.

The default value of splitChunks.minRemainingSize depends on the mode:

ModeDefault
"production"the value of splitChunks.minSize
"development"0
"none"the value of splitChunks.minSize

It doesn't need to be specified manually except for the rare cases where deep control is required.

splitChunks.maxSize

number = 0

Using maxSize (either globally optimization.splitChunks.maxSize per cache group optimization.splitChunks.cacheGroups[x].maxSize or for the fallback cache group optimization.splitChunks.fallbackCacheGroup.maxSize) tells webpack to try to split chunks bigger than maxSize bytes into smaller parts. Parts will be at least minSize (next to maxSize) in size. The algorithm is deterministic and changes to the modules will only have local effects. So that it is usable when using long term caching and doesn't require records. maxSize is only a hint and could be violated when modules are bigger than maxSize or splitting would violate minSize.

When the chunk has a name already, each part will get a new name derived from that name. Depending on the value of optimization.splitChunks.hidePathInfo it will add a key derived from the first module name or a hash of it.

maxSize option is intended to be used with HTTP/2 and long term caching. It increases the request count for better caching. It could also be used to decrease the file size for faster rebuilding.

splitChunks.maxAsyncSize

number

Like maxSize, maxAsyncSize can be applied globally (splitChunks.maxAsyncSize), to cacheGroups (splitChunks.cacheGroups.{cacheGroup}.maxAsyncSize), or to the fallback cache group (splitChunks.fallbackCacheGroup.maxAsyncSize).

The difference between maxAsyncSize and maxSize is that maxAsyncSize will only affect on-demand loading chunks.

splitChunks.maxInitialSize

number

Like maxSize, maxInitialSize can be applied globally (splitChunks.maxInitialSize), to cacheGroups (splitChunks.cacheGroups.{cacheGroup}.maxInitialSize), or to the fallback cache group (splitChunks.fallbackCacheGroup.maxInitialSize).

The difference between maxInitialSize and maxSize is that maxInitialSize will only affect initial load chunks.

splitChunks.name

false = false string (module: Module, chunks: Chunk[], cacheGroupKey: string) => string | undefined

Also available for each cacheGroup: splitChunks.cacheGroups.{cacheGroup}.name.

The name of the split chunk. Providing false will keep the same name of the chunks so it doesn't change names unnecessarily. It is the recommended value for production builds.

Providing a string or a function allows you to use a custom name. Specifying either a string or a function that always returns the same string will merge all common modules and vendors into a single chunk. This might lead to bigger initial downloads and slow down page loads.

If you choose to specify a function, you may find the chunk.name property (where chunk is an element of the chunks array) particularly useful in choosing a name for your chunk.

If splitChunks.name matches the name of an existing chunk (e.g. an entry point), the selected modules are moved into that chunk only when it is a parent of all selected chunks (e.g. via dependOn); otherwise webpack emits an error.

main.js

import _ from "lodash";

console.log(_.join(["Hello", "webpack"], " "));

webpack.config.js

export default {
  // ...
  optimization: {
    splitChunks: {
      cacheGroups: {
        commons: {
          test: /[\\/]node_modules[\\/]/,
          // cacheGroupKey here is `commons` as the key of the cacheGroup
          name(module, chunks, cacheGroupKey) {
            const moduleFileName = module
              .identifier()
              .split("/")
              .reduceRight((item) => item);
            const allChunksNames = chunks.map((item) => item.name).join("~");
            return `${cacheGroupKey}-${allChunksNames}-${moduleFileName}`;
          },
          chunks: "all",
        },
      },
    },
  },
};

Running webpack with following splitChunks configuration would also output a chunk of the group common with next name: commons-main-lodash.js.e7519d2bb8777058fa27.js (hash given as an example of real world output).

splitChunks.usedExports

splitChunks.cacheGroups{cacheGroup}.usedExports

boolean

Figure out which exports are used by modules to mangle export names, omit unused exports and generate more efficient code.

Defaults to true when optimization.usedExports is true (the case in production mode) and to false otherwise.

splitChunks.fallbackCacheGroup

object

Options for the modules that no cache group selected, which is the group webpack uses when it has to split a chunk further to honour maxSize. It accepts chunks, minSize, maxSize, maxAsyncSize, maxInitialSize and automaticNameDelimiter, each defaulting to the value set at splitChunks level.

webpack.config.js

export default {
  // ...
  optimization: {
    splitChunks: {
      fallbackCacheGroup: {
        maxSize: 100000,
        automaticNameDelimiter: "~",
      },
    },
  },
};

splitChunks.cacheGroups

Cache groups can inherit and/or override any options from splitChunks.*; but test, priority and reuseExistingChunk can only be configured on cache group level. To disable any of the default cache groups, set them to false.

webpack.config.js

export default {
  // ...
  optimization: {
    splitChunks: {
      cacheGroups: {
        default: false,
      },
    },
  },
};

splitChunks.cacheGroups.{cacheGroup}.priority

number = -20

A module can belong to multiple cache groups. The optimization will prefer the cache group with a higher priority. The default groups have a negative priority to allow custom groups to take higher priority (default value is 0 for custom groups).

splitChunks.cacheGroups.{cacheGroup}.reuseExistingChunk

boolean = false

If the current chunk contains modules already split out from the main bundle, it will be reused instead of a new one being generated. This can affect the resulting file name of the chunk. The built-in default and defaultVendors cache groups set it to true.

webpack.config.js

export default {
  // ...
  optimization: {
    splitChunks: {
      cacheGroups: {
        defaultVendors: {
          reuseExistingChunk: true,
        },
      },
    },
  },
};

splitChunks.cacheGroups.{cacheGroup}.type

function RegExp string

Allows to assign modules to a cache group by module type.

webpack.config.js

export default {
  // ...
  optimization: {
    splitChunks: {
      cacheGroups: {
        json: {
          type: "json",
        },
      },
    },
  },
};

splitChunks.cacheGroups.{cacheGroup}.layer

RegExp string function

Assign modules to a cache group by module layer.

splitChunks.cacheGroups.test

splitChunks.cacheGroups.{cacheGroup}.test

RegExp string (module: Module, context: { chunkGraph: ChunkGraph, moduleGraph: ModuleGraph }) => boolean

Controls which modules are selected by this cache group. Omitting it selects all modules. It is matched against the module's resource path (module.nameForCondition()): a string matches by prefix, a RegExp by test.

Providing a function to{cacheGroup}.test:

webpack.config.js

import path from "node:path";

export default {
  // ...
  optimization: {
    splitChunks: {
      cacheGroups: {
        svgGroup: {
          test(module) {
            // `module.resource` contains the absolute path of the file on disk.
            // Note the usage of `path.sep` instead of / or \, for cross-platform compatibility.

            return (
              module.resource &&
              module.resource.endsWith(".svg") &&
              module.resource.includes(`${path.sep}cacheable_svgs${path.sep}`)
            );
          },
        },
        byModuleTypeGroup: {
          test(module) {
            return module.type === "javascript/auto";
          },
        },
      },
    },
  },
};

In order to see what information is available in module and chunks objects, you can put debugger; statement in the callback. Then run your webpack build in debug mode to inspect the parameters in Chromium DevTools.

Providing a RegExp to {cacheGroup}.test:

webpack.config.js

export default {
  // ...
  optimization: {
    splitChunks: {
      cacheGroups: {
        defaultVendors: {
          // Note the usage of `[\\/]` as a path separator for cross-platform compatibility.
          test: /[\\/]node_modules[\\/]|vendor[\\/]analytics_provider|vendor[\\/]other_lib/,
        },
      },
    },
  },
};

splitChunks.cacheGroups.{cacheGroup}.filename

string (pathData: PathData, assetInfo?: AssetInfo) => string

Overrides the filename template for the chunks created by this cache group (initial and on-demand). It can also be set globally as splitChunks.filename. All placeholders available in output.filename are also available here.

webpack.config.js

export default {
  // ...
  optimization: {
    splitChunks: {
      cacheGroups: {
        defaultVendors: {
          filename: "[name].bundle.js",
        },
      },
    },
  },
};

And as a function:

webpack.config.js

export default {
  // ...
  optimization: {
    splitChunks: {
      cacheGroups: {
        defaultVendors: {
          filename: (pathData) =>
            // Use pathData object for generating filename string based on your requirements
            `${pathData.chunk.name}-bundle.js`,
        },
      },
    },
  },
};

It is possible to create a folder structure by providing path prefixing the filename: 'js/vendor/bundle.js'.

webpack.config.js

export default {
  // ...
  optimization: {
    splitChunks: {
      cacheGroups: {
        defaultVendors: {
          filename: "js/[name]/bundle.js",
        },
      },
    },
  },
};

splitChunks.cacheGroups.{cacheGroup}.enforce

boolean = false

Tells webpack to ignore splitChunks.minSize, splitChunks.minChunks, splitChunks.maxAsyncRequests and splitChunks.maxInitialRequests options and always create chunks for this cache group.

webpack.config.js

export default {
  // ...
  optimization: {
    splitChunks: {
      cacheGroups: {
        defaultVendors: {
          enforce: true,
        },
      },
    },
  },
};

splitChunks.cacheGroups.{cacheGroup}.idHint

string

Sets the hint for chunk id. It will be added to chunk's filename.

webpack.config.js

export default {
  // ...
  optimization: {
    splitChunks: {
      cacheGroups: {
        defaultVendors: {
          idHint: "vendors",
        },
      },
    },
  },
};

Examples

Defaults: Example 1

// index.js

import("./a"); // dynamic import
// a.js
import "react";

// ...

Result: A separate chunk would be created containing react. At the import call this chunk is loaded in parallel to the original chunk containing ./a.

Why:

  • Condition 1: The chunk contains modules from node_modules
  • Condition 2: react is bigger than 20kb
  • Condition 3: Number of parallel requests at the import call is 2
  • Condition 4: Doesn't affect request at initial page load

What's the reasoning behind this? react probably won't change as often as your application code. By moving it into a separate chunk this chunk can be cached separately from your app code (assuming you are using chunkhash, records, Cache-Control or other long term cache approach).

Defaults: Example 2

// entry.js

// dynamic imports
import("./a");
import("./b");
// a.js
import "./helpers"; // helpers is 40kb in size

// ...
// b.js
import "./helpers";
import "./more-helpers"; // more-helpers is also 40kb in size

// ...

Result: A separate chunk would be created containing ./helpers and all dependencies of it. At the import calls this chunk is loaded in parallel to the original chunks.

Why:

  • Condition 1: The chunk is shared between both import calls
  • Condition 2: helpers is bigger than 20kb
  • Condition 3: Number of parallel requests at the import calls is 2
  • Condition 4: Doesn't affect request at initial page load

Putting the content of helpers into each chunk will result into its code being downloaded twice. By using a separate chunk this will only happen once. We pay the cost of an additional request, which could be considered a tradeoff. That's why there is a minimum size of 20kb.

Split Chunks: Example 1

Create a commons chunk, which includes all code shared between entry points.

webpack.config.js

export default {
  // ...
  optimization: {
    splitChunks: {
      cacheGroups: {
        commons: {
          name: "commons",
          chunks: "initial",
          minChunks: 2,
        },
      },
    },
  },
};

Split Chunks: Example 2

Create a vendors chunk, which includes all code from node_modules in the whole application.

webpack.config.js

export default {
  // ...
  optimization: {
    splitChunks: {
      cacheGroups: {
        commons: {
          test: /[\\/]node_modules[\\/]/,
          name: "vendors",
          chunks: "all",
        },
      },
    },
  },
};

Split Chunks: Example 3

Create a custom vendor chunk, which contains certain node_modules packages matched by RegExp.

webpack.config.js

export default {
  // ...
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
          name: "vendor",
          chunks: "all",
        },
      },
    },
  },
};

SvgChunkWebpackPlugin

GitHub Workflow Status (branch) Coverage Status Npm downloads Ko-fi

The svg-chunk-webpack-plugin creates optimized SVG sprites, according to Webpack's entrypoints. Each sprite contains only the SVG dependencies listed on its entrypoints to improved code splitting, even on SVG files.

The plugin includes the popular SVGO package to generates clean and optimized SVG sprites.

Code splitting is the key to deliver files without any content that is unused by the pages. It already exists for CSS, Javascript and now for SVG files with this plugin.

When to use this plugin

On multiple page application, each pages must includes only its necessary dependencies. In other words, it must include only the SVG files imported by its entrypoint and all its dependencies.

With reusable components, SVGs are often duplicated on all the project. Now, you can create a global SVG library and every Javascript files can easily import any SVG from this library. Entrypoint dependencies are automatically updated, thanks to the Webpack compilation.

When you work with SVGs exported by design softwares, like Sketch or Illustrator, their source code is never optimized and often contains comments, CSS classes which can create conflicts between them. The plugin automatically cleans all SVGs before creating the sprite.

Zero config

The plugin works without configuration with already the optimized settings. For advanced usage, see the section using configuration.

Installation

The plugin is available as a package with the name of svg-chunk-webpack-plugin on npm and Github.

npm install svg-chunk-webpack-plugin --save-dev
yarn add svg-chunk-webpack-plugin --dev

[!WARNING] Plugin svg-chunk-webpack-plugin@5 is ESM only.\n

[!NOTE] Minimum supported Node.js version is 16.20.0 and Webpack >=5.10.3.

[!NOTE] The plugin is compatible with html-webpack-plugin (optional dependency). Use the injectSpritesInTemplates option to automatically inject sprites into your HTML templates.

Example

The project includes a minimalist example in the ./example directory. Run the npm run build:example command to execute the Webpack example and see the plugin's implementation in action.

Basic usage

The plugin will generate one SVG sprite for each entrypoints. Sprites are built in the output path directory with all the other assets. Each sprite filename is composed with its entrypoint name (in the example below, that would be home.svg).

First, let's add the loader and the plugin to the Webpack configuration.

[!WARNING] The loader and the plugin need to works together.

webpack.config.js

import SvgChunkWebpackPlugin from 'svg-chunk-webpack-plugin';

export default {
  module: {
    rules: [
      {
        test: /\.svg$/,
        use: [
          {
            loader: SvgChunkWebpackPlugin.loader
          }
        ]
      }
    ]
  },
  plugins: [new SvgChunkWebpackPlugin()]
};

[!NOTE] For more flexibility and better performance, inline SVG files are better. Fewer HTTP requests, CSS properties to change the style, no flickering during the page load.

Then, include the sprite in the wanted pages (we use Twig in the following example).

home.html.twig

{{ include 'home.svg' }}

Finally, use the SVG with the <use> tag, like the following example. Replace <svg_name> by the SVG name (without the extension).

home.html.twig

<svg>
  <use href="#<svg_name>"></use>
</svg>

Using a configuration

The loader and the plugin accepts configuration to override the default behavior.

Loader

The loader configuration allow to personalize the SVGO configuration. SVGO optimization is executed during the loader process to optimize build performance.

configFile

Type:

type configFile = string | boolean;

Default: path.resolve(opts.root, 'svgo.config.js')

Tells the loader whether to load the custom SVGO configuration. Custom configuration can be disabled with configFile: false.

webpack.config.js

export default {
  module: {
    rules: [
      {
        test: /\.svg$/,
        loader: SvgChunkWebpackPlugin.loader,
        options: {
          configFile: './path/svgo.config.js'
        }
      }
    ]
  }
};

SVGO custom configuration

SVGO have a default preset to optimize SVG files. See how to configure svgo for details.

svgo.config.js

export default {
  multipass: true,
  plugins: [
    {
      name: 'preset-default',
      params: {
        overrides: {
          inlineStyles: {
            onlyMatchedOnce: false
          },
          removeViewBox: false
        }
      }
    },
    {
      name: 'convertStyleToAttrs'
    }
  ]
};

Plugin

The plugin configuration allow to personalize sprite settings.

filename

Type:

type filename = string;

Default: '[name].svg'

Tells the plugin whether to personalize the default sprite filename. The placeholder [name] is automatically replaced by entrypoints names.

webpack.config.js

export default {
  plugins: [
    new SvgChunkWebpackPlugin({
      filename: '[name].svg'
    })
  ]
};

[!NOTE] The filename parameter is compatible with Webpack caching placeholders, see the section caching.

svgstoreConfig

Type:

type svgstoreConfig = object;

Default: { cleanDefs: false, cleanSymbols: false, inline: true }

SVG sprites are built using the svgstore package. Update the parameters according to your needs from the options list available on the svgstore documentation.

webpack.config.js

export default {
  plugins: [
    new SvgChunkWebpackPlugin({
      svgstoreConfig: {
        svgAttrs: {
          'aria-hidden': true,
          style: 'position: absolute; width: 0; height: 0; overflow: hidden;'
        }
      }
    })
  ]
};

[!NOTE] To avoid LinearGradient conflicts, avoid the display: none property which breaks SVG definitions.

generateSpritesManifest

Type:

type generateSpritesManifest = boolean;

Default: false

Tells the plugin whether to generate the sprites-manifest.json. The JSON file contains the list of all SVG included by entrypoints.

webpack.config.js

export default {
  plugins: [
    new SvgChunkWebpackPlugin({
      generateSpritesManifest: true
    })
  ]
};

generateSpritesPreview

Type:

type generateSpritesPreview = boolean;

Default: false

Tells the plugin whether to generate the sprites-preview.html. The HTML preview contains a display list of all SVG included by entrypoints with the SVG overviews and the names. See the sprites preview of the example.

webpack.config.js

export default {
  plugins: [
    new SvgChunkWebpackPlugin({
      generateSpritesPreview: true
    })
  ]
};

injectSpritesInTemplates

Type:

type injectSpritesInTemplates = boolean;

Default: false

Enables the plugin to inject sprites into HTML files generated by html-webpack-plugin. The sprites will be injected at the beginning of the content <body>.

[!NOTE] The sprites associated with the entrypoints listed in the chunks parameter of HtmlWebpackPlugin will be automatically included in the corresponding HTML files.

webpack.config.js

export default {
  plugins: [
    new SvgChunkWebpackPlugin({
      injectSpritesInTemplates: true
    })
  ]
};

Caching

With webpack caching, several placeholders are available depending on your needs. With SVG inlined in the page, this option is not useful.

[!NOTE] The [contenthash] placeholder is the best option because it depends on the sprite content. Cache placeholders are expensive in build performance, use it only in production mode.

[contenthash]

The [contenthash] placeholder will add a unique hash based on the content of the sprite. When the sprite's content changes, the hash will change as well.

webpack.config.js

export default {
  plugins: [
    new SvgChunkWebpackPlugin({
      filename: '[name].[contenthash].svg'
    })
  ]
};

[fullhash]

The [fullhash] placeholder will add a unique hash generated for every build. When the compilation build is updated, the hash will change as well.

webpack.config.js

export default {
  plugins: [
    new SvgChunkWebpackPlugin({
      filename: '[name].[fullhash].svg'
    })
  ]
};

License

svg-chunk-webpack-plugin is licensed under the MIT License.

Created with ♥ by @yoriiis.

VirtualUrlPlugin

Allow creating virtual modules of any type, such as .ts, .json, .css, etc. default is .js.

5.100.0+
import webpack from "webpack";

new webpack.experiments.schemes.VirtualUrlPlugin({
  myModule: 'export const msg = "from virtual module"',
});

src/app.js

import { msg } from "virtual:myModule";

console.log(msg);

Basic Example

Create a virtual module that generates build information

import webpack from "webpack";

new webpack.experiments.schemes.VirtualUrlPlugin({
  buildInfo: {
    source() {
      return `export const buildTime = ${Date.now()}`;
    },
    version: true,
  },
});

src/app.js

import { buildTime } from "virtual:buildInfo";

console.log(`App version: ${buildTime}`);

Use custom scheme

import webpack from "webpack";

new webpack.experiments.schemes.VirtualUrlPlugin(
  {
    myModule: 'export const msg = "from virtual module"',
  },
  "v",
);

src/app.js

import { msg } from "v:myModule";

console.log(msg);

Advanced Example

Create multiple virtual modules of different types

import webpack from "webpack";

new webpack.experiments.schemes.VirtualUrlPlugin({
  myCssModule: {
    type: ".css",
    source: "body{background-color: powderblue;}",
  },
  myJsonModule: {
    type: ".json",
    source: '{"name": "virtual-url-plugin"}',
  },
});

src/app.js

import json from "virtual:myJsonModule";
import "virtual:myCssModule";

Virtualize the routing file

import path from "node:path";
import { fileURLToPath } from "node:url";
import webpack from "webpack";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const watchDir = path.join(__dirname, "./src/routes");

new webpack.experiments.schemes.VirtualUrlPlugin({
  routes: {
    source(loaderContext) {
      // Use addContextDependency to monitor the addition or removal of subdirectories in watchDir to trigger the rebuilding of virtual modules.
      loaderContext.addContextDependency(watchDir);

      const files = fs.readdirSync(watchDir);
      return `
        export const routes = {
          ${files.map((key) => `${key.split(".")[0]}: () => import('./src/routes/${key}')`).join(",\n")}
        }
      `;
    },
  },
});

src/app.js

import { routes } from "virtual:routes";

Options

  • module.type (string): Content type of the virtual module.
  • module.source (string | ((loaderContext: import('webpack').LoaderContext<T>) => Promise<string> | string)): Factory function for generating the content of virtual module.

  • module.version(boolean | string | () => string): When a invalidate is triggered, the source function is called again if the value of the version is different from the previous one. If set to true it will always trigger.

  • scheme (string): URL scheme for virtual modules, default is 'virtual'. The second constructor argument is either the scheme string or an object { scheme?, context? }.

WatchIgnorePlugin

Ignore the specified files, i.e. those matching the provided paths or regular expressions, while in watch mode.

new webpack.WatchIgnorePlugin({ paths });

Options

  • paths (Array<string | RegExp>): A list of RegExps or absolute paths to directories or files that should be ignored.
Edit this page·

1 Contributor

webpack