Skip to content

Commit 685b695

Browse files
committed
feat: add filter helper to dispatch assets per-minimizer
Each built-in minimizer now exposes a `filter(name, info)` helper based on file extension. When `minify` is an array, each asset is dispatched only to the minimizers whose `filter` accepts it; chain semantics still apply when multiple minimizers claim the same asset. The `test` option defaults to `undefined` for the array form so that filters can decide. This lets a single `TerserPlugin` instance handle JS/CSS/HTML/JSON with one shared worker pool instead of forcing a separate plugin instance per type.
1 parent 1a34e62 commit 685b695

6 files changed

Lines changed: 599 additions & 13 deletions

File tree

README.md

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,34 @@ Allows you to override the default minify function.
321321
By default plugin uses [terser](https://github.com/terser/terser) package.
322322
Useful for using and testing unpublished versions or forks.
323323

324-
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.
324+
An array of functions can also be provided. Each minimizer can expose a
325+
`filter(name, info)` helper that decides whether it should run on a given
326+
asset; the plugin dispatches each asset only to the minimizers whose `filter`
327+
accepts it (or runs them all when no filter is set). All built-in minimizers
328+
ship with a `filter` that matches their natural extension, so a single plugin
329+
instance and a single worker pool can handle JS, CSS, HTML and JSON together
330+
without juggling multiple `TerserPlugin` instances:
331+
332+
```js
333+
new TerserPlugin({
334+
minify: [
335+
TerserPlugin.terserMinify,
336+
TerserPlugin.cssnanoMinify,
337+
TerserPlugin.htmlMinifierTerser,
338+
TerserPlugin.jsonMinify,
339+
],
340+
});
341+
```
342+
343+
When more than one minimizer in the array claims the same asset, the chain
344+
semantic still applies: the output of each accepting minimizer is fed as
345+
input to the next. The [`minimizerOptions`](#minimizeroptions) option may
346+
be an array (index-paired with `minify`) or a single object reused by every
347+
minimizer.
348+
349+
When `minify` is an array, the `test` option defaults to `undefined` so each
350+
minimizer's `filter` decides which assets it processes. With a single `minify`
351+
function, `test` keeps its JS-only default of `/\.[cm]?js(\?.*)?$/i`.
325352

326353
> **Warning**
327354
>
@@ -362,6 +389,12 @@ minify.getMinimizerVersion = () => {
362389
return packageJson && packageJson.version;
363390
};
364391

392+
// Restrict the minimizer to the assets it can actually handle. The plugin
393+
// skips assets for which `filter` returns `false` and (when an array of
394+
// minimizers is used) dispatches each asset only to the minimizers that
395+
// accept it. Returning `undefined` is treated as accept.
396+
minify.filter = (name) => /\.[cm]?js(\?.*)?$/i.test(name);
397+
365398
module.exports = {
366399
optimization: {
367400
minimize: true,
@@ -379,11 +412,13 @@ module.exports = {
379412

380413
#### `array`
381414

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

388423
**webpack.config.js**
389424

@@ -407,6 +442,30 @@ module.exports = {
407442
};
408443
```
409444

445+
A single plugin instance can also handle multiple asset types — the built-in
446+
minimizers each ship with a `filter` matching their natural extension, so JS,
447+
CSS, HTML and JSON can all be minified by one shared worker pool:
448+
449+
```js
450+
module.exports = {
451+
optimization: {
452+
minimize: true,
453+
minimizer: [
454+
new TerserPlugin({
455+
// `test` defaults to `undefined` here so each filter decides which
456+
// assets it handles. Override `test` to narrow further if needed.
457+
minify: [
458+
TerserPlugin.terserMinify,
459+
TerserPlugin.cssnanoMinify,
460+
TerserPlugin.htmlMinifierTerser,
461+
TerserPlugin.jsonMinify,
462+
],
463+
}),
464+
],
465+
},
466+
};
467+
```
468+
410469
### `minimizerOptions`
411470

412471
Type:

src/index.js

Lines changed: 81 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ const {
124124
* @property {() => string | undefined=} getMinimizerVersion function that returns version of minimizer
125125
* @property {() => boolean | undefined=} supportsWorkerThreads true when minimizer support worker threads, otherwise false
126126
* @property {() => boolean | undefined=} supportsWorker true when minimizer support worker, otherwise false
127+
* @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.
127128
*/
128129

129130
/**
@@ -189,13 +190,17 @@ class TerserPlugin {
189190

190191
// TODO handle json and etc in the next major release
191192
// TODO make `minimizer` option instead `minify` and `terserOptions` in the next major release, also rename `terserMinify` to `terserMinimize`
193+
// When an array of minimizers is supplied, leave `test` undefined so that
194+
// each minimizer's own `filter` decides which assets it processes;
195+
// otherwise fall back to the JS-only default.
196+
const isMinifyArray = Array.isArray(options && options.minify);
192197
const {
193198
minify = /** @type {MinimizerImplementation<T>} */ (
194199
/** @type {unknown} */ (terserMinify)
195200
),
196201
minimizerOptions,
197202
terserOptions,
198-
test = /\.[cm]?js(\?.*)?$/i,
203+
test = isMinifyArray ? undefined : /\.[cm]?js(\?.*)?$/i,
199204
extractComments = true,
200205
parallel = true,
201206
include,
@@ -377,6 +382,37 @@ class TerserPlugin {
377382
async optimize(compiler, compilation, assets, optimizeOptions) {
378383
const cache = compilation.getCache("TerserWebpackPlugin");
379384
let numberOfAssets = 0;
385+
const allImplementations = Array.isArray(
386+
this.options.minimizer.implementation,
387+
)
388+
? this.options.minimizer.implementation
389+
: null;
390+
/**
391+
* Apply each minimizer's `filter` helper. For array form, an asset is
392+
* accepted when at least one configured minimizer accepts it; for the
393+
* single form, the asset must be accepted by that minimizer.
394+
* @param {string} name asset name
395+
* @param {AssetInfo} info asset info
396+
* @returns {boolean} whether at least one minimizer accepts this asset
397+
*/
398+
const isAcceptedByAnyMinimizer = (name, info) => {
399+
if (allImplementations) {
400+
return allImplementations.some(
401+
(impl) =>
402+
typeof impl.filter !== "function" ||
403+
// eslint-disable-next-line unicorn/no-array-method-this-argument
404+
impl.filter(name, info) !== false,
405+
);
406+
}
407+
const single =
408+
/** @type {BasicMinimizerImplementation<EXPECTED_ANY> & MinimizeFunctionHelpers} */
409+
(this.options.minimizer.implementation);
410+
return (
411+
typeof single.filter !== "function" ||
412+
// eslint-disable-next-line unicorn/no-array-method-this-argument
413+
single.filter(name, info) !== false
414+
);
415+
};
380416
const assetsForMinify = await Promise.all(
381417
Object.keys(assets)
382418
.filter((name) => {
@@ -400,6 +436,10 @@ class TerserPlugin {
400436
return false;
401437
}
402438

439+
if (!isAcceptedByAnyMinimizer(name, info)) {
440+
return false;
441+
}
442+
403443
return true;
404444
})
405445
.map(async (name) => {
@@ -523,11 +563,45 @@ class TerserPlugin {
523563
input = input.toString();
524564
}
525565

526-
const clonedMinimizerOptions = Array.isArray(
527-
this.options.minimizer.options,
528-
)
529-
? this.options.minimizer.options.map((item) => ({ ...item }))
530-
: { .../** @type {T} */ (this.options.minimizer.options) };
566+
// Dispatch to only the minimizers whose `filter` accepts this asset.
567+
// For the single-implementation form `assetImplementation` matches
568+
// the original; for the array form it's the matching subset (still
569+
// an array, even when only one entry remains, so chained behavior
570+
// is preserved when multiple match).
571+
let assetImplementation = this.options.minimizer.implementation;
572+
let assetMinimizerOptions = this.options.minimizer.options;
573+
574+
if (allImplementations) {
575+
const matchedIndices = [];
576+
577+
for (let i = 0; i < allImplementations.length; i++) {
578+
const impl = allImplementations[i];
579+
580+
if (
581+
typeof impl.filter !== "function" ||
582+
// eslint-disable-next-line unicorn/no-array-method-this-argument
583+
impl.filter(name, info) !== false
584+
) {
585+
matchedIndices.push(i);
586+
}
587+
}
588+
589+
assetImplementation =
590+
/** @type {MinimizerImplementation<T>} */
591+
(matchedIndices.map((i) => allImplementations[i]));
592+
593+
if (Array.isArray(this.options.minimizer.options)) {
594+
const sourceOptions = this.options.minimizer.options;
595+
596+
assetMinimizerOptions =
597+
/** @type {MinimizerOptions<T>} */
598+
(matchedIndices.map((i) => sourceOptions[i] || {}));
599+
}
600+
}
601+
602+
const clonedMinimizerOptions = Array.isArray(assetMinimizerOptions)
603+
? assetMinimizerOptions.map((item) => ({ ...item }))
604+
: { .../** @type {T} */ (assetMinimizerOptions) };
531605

532606
/**
533607
* @type {InternalOptions<T>}
@@ -537,7 +611,7 @@ class TerserPlugin {
537611
input,
538612
inputSourceMap,
539613
minimizer: {
540-
implementation: this.options.minimizer.implementation,
614+
implementation: assetImplementation,
541615
options:
542616
/** @type {MinimizerOptions<T>} */
543617
(clonedMinimizerOptions),

0 commit comments

Comments
 (0)