Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 68 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,36 @@ Allows you to override the default minify function.
By default plugin uses [terser](https://github.com/terser/terser) package.
Useful for using and testing unpublished versions or forks.

An array of functions can also be provided to chain multiple minimizers — the output of each minimizer is fed as input to the next. When an array is used, the [`minimizerOptions`](#minimizeroptions) option may also be an array (index-paired with `minify`) or a single object that is reused for every minimizer.
An array of functions can also be provided. Each minimizer can expose a
`filter(name, info)` helper that decides whether it should run on a given
asset; the plugin dispatches each asset only to the minimizers whose `filter`
accepts it (or runs them all when no filter is set). All built-in minimizers
ship with a `filter` that matches their natural extension, so a single plugin
instance and a single worker pool can handle JS, CSS, HTML and JSON together
without juggling multiple `TerserPlugin` instances — just widen `test` to
let those asset types reach the dispatcher:

```js
new TerserPlugin({
test: /\.(?:[cm]?js|css|html?|json)(\?.*)?$/i,
minify: [
TerserPlugin.terserMinify,
TerserPlugin.cssnanoMinify,
TerserPlugin.htmlMinifierTerser,
TerserPlugin.jsonMinify,
],
});
```

When more than one minimizer in the array claims the same asset, the chain
semantic still applies: the output of each accepting minimizer is fed as
input to the next. The [`minimizerOptions`](#minimizeroptions) option may
be an array (index-paired with `minify`) or a single object reused by every
minimizer.

The `test` option always defaults to `/\.[cm]?js(\?.*)?$/i`. When you mix
asset types in a single plugin instance, widen `test` so non-JS assets reach
the dispatcher (for example `test: /\.(?:[cm]?js|css|html?|json)(\?.*)?$/i`).

> **Warning**
>
Expand Down Expand Up @@ -362,6 +391,12 @@ minify.getMinimizerVersion = () => {
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);

module.exports = {
optimization: {
minimize: true,
Expand All @@ -379,11 +414,13 @@ module.exports = {

#### `array`

If an array of functions is passed to the `minify` option, the output of each
minimizer is fed as input to the next one. The `minimizerOptions` option can be
either an array of option objects (index-paired with `minify`) or a single
object that will be shared by all minimizers. Warnings, errors and extracted
comments from all minimizers are merged together.
If an array of functions is passed to the `minify` option, each asset is
dispatched to the minimizers whose `filter` accepts it. When more than one
minimizer accepts the same asset the output of each is fed as input to the
next one (the chain semantic). The `minimizerOptions` option can be either an
array of option objects (index-paired with `minify`) or a single object that
will be shared by all minimizers. Warnings, errors and extracted comments
from all running minimizers are merged together.

**webpack.config.js**

Expand All @@ -407,6 +444,31 @@ module.exports = {
};
```

A single plugin instance can also handle multiple asset types — the built-in
minimizers each ship with a `filter` matching their natural extension, so JS,
CSS, HTML and JSON can all be minified by one shared worker pool:

```js
module.exports = {
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
// `test` still defaults to JS only, so widen it to catch every
// asset type you want the dispatcher to consider.
test: /\.(?:[cm]?js|css|html?|json)(\?.*)?$/i,
minify: [
TerserPlugin.terserMinify,
TerserPlugin.cssnanoMinify,
TerserPlugin.htmlMinifierTerser,
TerserPlugin.jsonMinify,
],
}),
],
},
};
```

### `minimizerOptions`

Type:
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

91 changes: 76 additions & 15 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ const {
* @property {() => string | undefined=} getMinimizerVersion function that returns version of minimizer
* @property {() => boolean | undefined=} supportsWorkerThreads true when minimizer support worker threads, otherwise false
* @property {() => boolean | undefined=} supportsWorker true when minimizer support worker, otherwise false
* @property {(name: string, info?: AssetInfo) => boolean | undefined=} filter return true when the minimizer supports the asset, otherwise false. When an array of minimizers is configured, each asset is dispatched only to the minimizers whose `filter` accepts it. Assets rejected by every minimizer in the array are skipped entirely.
*/

/**
Expand Down Expand Up @@ -377,6 +378,43 @@ class TerserPlugin {
async optimize(compiler, compilation, assets, optimizeOptions) {
const cache = compilation.getCache("TerserWebpackPlugin");
let numberOfAssets = 0;

// Normalize the implementation list to an array so dispatch and the
// worker-pool capability checks below can iterate uniformly. The
// original shape on `this.options.minimizer.implementation` is preserved
// for chunk hashing.
const implementations = Array.isArray(this.options.minimizer.implementation)
? this.options.minimizer.implementation
: [this.options.minimizer.implementation];

/**
* Collect the indices of minimizers whose `filter` accepts `name`.
* Filters returning `undefined` are treated as accept (matches the
* convention used by `supportsWorkerThreads`).
* @param {string} name asset name
* @param {AssetInfo} info asset info
* @returns {number[]} indices into `implementations` that accept the asset
*/
const matchingMinimizers = (name, info) => {
const matched = [];

for (let i = 0; i < implementations.length; i++) {
const impl = implementations[i];

if (
typeof impl.filter !== "function" ||
// eslint-disable-next-line unicorn/no-array-method-this-argument
impl.filter(name, info) !== false
) {
matched.push(i);
}
}

return matched;
};
/** @type {Map<string, number[]>} */
const matchedByName = new Map();

const assetsForMinify = await Promise.all(
Object.keys(assets)
.filter((name) => {
Expand All @@ -400,6 +438,16 @@ class TerserPlugin {
return false;
}

// Compute the matching minimizers once and carry the result to the
// per-asset task via `matchedByName` so the regexes don't run again.
const matched = matchingMinimizers(name, info);

if (matched.length === 0) {
return false;
}

matchedByName.set(name, matched);

return true;
})
.map(async (name) => {
Expand All @@ -415,7 +463,14 @@ class TerserPlugin {
numberOfAssets += 1;
}

return { name, info, inputSource: source, output, cacheItem };
return {
name,
info,
inputSource: source,
output,
cacheItem,
matched: /** @type {number[]} */ (matchedByName.get(name)),
};
}),
);

Expand All @@ -430,10 +485,6 @@ class TerserPlugin {
/** @type {undefined | number} */
let numberOfWorkers;

const implementations = Array.isArray(this.options.minimizer.implementation)
? this.options.minimizer.implementation
: [this.options.minimizer.implementation];

const needCreateWorker =
optimizeOptions.availableNumberOfCores > 0 &&
implementations.every(
Expand Down Expand Up @@ -496,7 +547,7 @@ class TerserPlugin {

for (const asset of assetsForMinify) {
scheduledTasks.push(async () => {
const { name, inputSource, info, cacheItem } = asset;
const { name, inputSource, info, cacheItem, matched } = asset;
let { output } = asset;

if (!output) {
Expand All @@ -523,11 +574,23 @@ class TerserPlugin {
input = input.toString();
}

const clonedMinimizerOptions = Array.isArray(
this.options.minimizer.options,
)
? this.options.minimizer.options.map((item) => ({ ...item }))
: { .../** @type {T} */ (this.options.minimizer.options) };
// Dispatch to only the minimizers whose `filter` accepted this
// asset (computed once when collecting `assetsForMinify`).
// `minify.js` already normalizes a single implementation into a
// one-element array, so we always hand it the matching subset.
// Options are sliced as references — `minify.js` overlays
// `module`/`ecma` without mutating the caller's object.
const assetImplementation =
/** @type {MinimizerImplementation<T>} */
(matched.map((i) => implementations[i]));
const sourceOptions = this.options.minimizer.options;
const assetMinimizerOptions =
/** @type {MinimizerOptions<T>} */
(
Array.isArray(sourceOptions)
? matched.map((i) => sourceOptions[i] || {})
: sourceOptions
);

/**
* @type {InternalOptions<T>}
Expand All @@ -537,10 +600,8 @@ class TerserPlugin {
input,
inputSourceMap,
minimizer: {
implementation: this.options.minimizer.implementation,
options:
/** @type {MinimizerOptions<T>} */
(clonedMinimizerOptions),
implementation: assetImplementation,
options: assetMinimizerOptions,
},
extractComments: this.options.extractComments,
};
Expand Down
21 changes: 11 additions & 10 deletions src/minify.js
Original file line number Diff line number Diff line change
Expand Up @@ -323,8 +323,8 @@ async function minify(options) {
const currentImplementation =
/** @type {import("./index.js").BasicMinimizerImplementation<T> & import("./index.js").MinimizeFunctionHelpers} */
(implementations[i]);
const currentOptions =
/** @type {import("./index.js").MinimizerOptions<T>} */
const baseOptions =
/** @type {import("./index.js").MinimizerOptions<T> & { module?: boolean, ecma?: number | string }} */
(
Array.isArray(minimizerOptions)
? minimizerOptions[i] || {}
Expand All @@ -333,14 +333,15 @@ async function minify(options) {
const currentInput = typeof lastCode === "string" ? lastCode : input;
const currentMap = typeof lastCode === "string" ? lastMap : inputSourceMap;

/** @type {MinimizerOptions<T & { module?: boolean }>} */
(currentOptions).module =
/** @type {MinimizerOptions<T & { module?: boolean }>} */
(currentOptions).module || module;
/** @type {MinimizerOptions<T & { ecma?: number | string }>} */
(currentOptions).ecma =
/** @type {MinimizerOptions<T & { ecma?: number | string }>} */
(currentOptions).ecma || ecma;
// Overlay `module` and `ecma` without mutating the caller's options so
// a single options object can be reused safely across assets.
const currentOptions =
/** @type {import("./index.js").MinimizerOptions<T>} */
({
...baseOptions,
module: baseOptions.module || module,
ecma: baseOptions.ecma || ecma,
});

const result = await currentImplementation(
{ [name]: currentInput },
Expand Down
Loading
Loading