Skip to content

Commit c69dae3

Browse files
feat: an empty minify list, and deleting an original alone (#743)
* feat: deleting an original takes its own file and nothing else The asset it read goes alone, so a source map or another generator's file beside it stays, and a function decides per asset which of them to delete. * feat: an empty minify list is nothing to minify For an instance whose whole job is its generate. A list with none in it needs no guard where a second kind of value would, which is why false stays out. * fix: nothing minifying salts no hash, and names no file beside itself An instance whose whole job is its generate rewrote no bytes, so the bundle kept its own name; and a file written under the original's name has replaced it, so recording related there wrote the source back over it. * feat: a generator names its file with a function too, and minify is typed as written The public type said one function where the schema has taken a list or a descriptor since 5.10, and a filename function is what webpack's own getPath takes. * docs: compressing with minifying, and without The two shapes a compression instance is written in, now that an empty list of minimizers says the second one. * fix: nothing minifying salts no embedded-source hash either The tap varies a module's hash on the minimizers, so an instance with none of them renamed files it never rewrote — the same defect the chunk hash had. * fix: delete the asset that was read, not whatever holds its name The generators of a pass run together, so another may have written over the original first — deleting then destroys that file instead.
1 parent 816e5b8 commit c69dae3

9 files changed

Lines changed: 564 additions & 70 deletions

File tree

‎.changeset/generator-compression-options.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
"minimizer-webpack-plugin": minor
33
---
44

5-
Add `threshold`, `minRatio` and `relatedName` to an `asset` generator, and stop `minify` from taking `false`.
5+
Add `threshold`, `minRatio` and `relatedName` to an `asset` generator, let `deleteOriginalAssets` be a function and take the original file alone, take a `filename` function, read `minify: []` as nothing to minify rather than `false`, and type what `minify` has long accepted.

‎README.md‎

Lines changed: 118 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -340,13 +340,36 @@ interface minimizer {
340340
}
341341

342342
type minify = minifyFn | (minifyFn | minimizer)[] | minimizer;
343+
// An empty array is nothing to minify.
343344
```
344345

345346
Default: `MinimizerPlugin.terserMinify`
346347

347348
Which minimizer runs, and the options it runs with. By default the plugin uses
348349
[terser](https://github.com/terser/terser); overriding it is also how you test
349-
an unpublished version or a fork.
350+
an unpublished version or a fork. The default stands whether or not a
351+
[`generate`](#generate) was configured too, and so does [`test`](#test)'s: the
352+
JavaScript minifier and the names it reads are what this plugin is.
353+
354+
Nothing minifying is an **empty array** rather than a missing value, for an
355+
instance whose whole job is its `generate`:
356+
357+
```js
358+
new MinimizerPlugin({
359+
test: /.*/,
360+
minify: [],
361+
generate: {
362+
implementation: MinimizerPlugin.compress,
363+
options: { algorithm: "gzip" },
364+
type: "asset",
365+
filename: "[path][base].gz",
366+
},
367+
});
368+
```
369+
370+
`false` is not accepted. A list with nothing in it needs no guard at any of the
371+
places that run the minimizers — each simply does nothing — while a second kind
372+
of value does.
350373

351374
> **Warning**
352375
>
@@ -690,9 +713,12 @@ interface generator {
690713
implementation: generateFn;
691714
options?: Record<string, any>;
692715
type?: "import" | "asset";
693-
filename?: string;
716+
filename?: string | ((pathData: any) => string);
694717
filter?: (name: string) => boolean;
695-
deleteOriginalAssets?: boolean;
718+
deleteOriginalAssets?: boolean | ((name: string) => boolean);
719+
threshold?: number;
720+
minRatio?: number;
721+
relatedName?: string | false;
696722
}
697723

698724
type generate =
@@ -793,13 +819,23 @@ new MinimizerPlugin({
793819
implementation: MinimizerPlugin.sharpGenerate,
794820
options: { encodeOptions: { webp: {} } },
795821
type: "asset",
796-
// Optional. Without it the generator's own name for the result is used,
797-
// which for `sharpGenerate` is the original with its extension replaced.
822+
// Optional, a template or a function answering with one. Without it the
823+
// generator's own name for the result is used, which for `sharpGenerate`
824+
// is the original with its extension replaced.
798825
filename: "[path][name].webp",
799826
// Optional. Narrows what this generator reads, on top of `test`.
800827
filter: (name) => !name.includes("icons/"),
801828
// Optional, `false` by default: the asset it read stays where it is.
829+
// Written as a function it is asked per asset.
802830
deleteOriginalAssets: false,
831+
// Optional. Skips an asset this small, before the generator is asked.
832+
threshold: 10240,
833+
// Optional. Drops the result unless it is this much smaller than what
834+
// it read, as `generated size / original size`.
835+
minRatio: 0.8,
836+
// Optional. The key the new file is recorded under in the original's
837+
// `related` info, which is how a server asked for one finds the other.
838+
relatedName: "webp",
803839
},
804840
},
805841
});
@@ -808,14 +844,14 @@ new MinimizerPlugin({
808844
`type` decides which of the two things a generator does, and they are not
809845
interchangeable — they read different input, at different points in the build:
810846

811-
| | `"import"` (the default) | `"asset"` |
812-
| :----------------------------- | :----------------------------------------- | :------------------------------------------------------ |
813-
| Reads | a module, **as it builds** | an asset, **once it is emitted** |
814-
| Produces | that module's own bytes, renamed with them | a **new file beside** the one it read |
815-
| Picked by | `?as=<name>` on the import | `test` / `include` / `exclude`, then `filter` |
816-
| Reaches a file nothing imports | no | yes — copied assets included |
817-
| Fields it reads | `implementation`, `options` | those plus `filename`, `filter`, `deleteOriginalAssets` |
818-
| webpack | **5.111** or newer | any supported version |
847+
| | `"import"` (the default) | `"asset"` |
848+
| :----------------------------- | :----------------------------------------- | :---------------------------------------------------------------------------------------------- |
849+
| Reads | a module, **as it builds** | an asset, **once it is emitted** |
850+
| Produces | that module's own bytes, renamed with them | a **new file beside** the one it read |
851+
| Picked by | `?as=<name>` on the import | `test` / `include` / `exclude`, then `filter` |
852+
| Reaches a file nothing imports | no | yes — copied assets included |
853+
| Fields it reads | `implementation`, `options` | those plus `filename`, `filter`, `deleteOriginalAssets`, `threshold`, `minRatio`, `relatedName` |
854+
| webpack | **5.111** or newer | any supported version |
819855

820856
**`"import"`** is the only point at which a rename can reach the bundle: the
821857
asset is named while its module is built, so every reference follows it. The
@@ -873,9 +909,35 @@ photo.jpg still there, unless `deleteOriginalAssets`
873909
photo.webp generated beside it
874910
```
875911
876-
`filename`, `filter` and `deleteOriginalAssets` describe a file being written
877-
beside another, so they belong to `"asset"` and setting one on an `"import"`
878-
generator is an error rather than a field that quietly does nothing.
912+
`filename`, `filter`, `deleteOriginalAssets`, `threshold`, `minRatio` and
913+
`relatedName` describe a file being written beside another, so they belong to
914+
`"asset"` and setting one on an `"import"` generator is an error rather than a
915+
field that quietly does nothing.
916+
917+
Three of them decide whether the new file is worth having. `threshold` skips an
918+
asset too small to bother with, before the generator is asked at all. `minRatio`
919+
drops a result that is not enough smaller than what it read, since a file that
920+
saves nothing still costs a request. `relatedName` records the new file under
921+
that key in the original's `related` info — which is how a server asked for the
922+
original finds it — and declines an asset already carrying that key.
923+
924+
A file written under the original's own name has replaced it, so there is
925+
nothing beside it to delete and nothing for `relatedName` to point at; a
926+
generator doing that re-encodes an asset in place.
927+
928+
Deleting takes the original file and nothing else. webpack deletes whatever an
929+
asset's `related` names along with it, so a source map, or the file a second
930+
generator wrote beside the same original, would go too; the original goes
931+
alone instead. Where the generated file took the original's own name there is
932+
nothing left to delete, and `relatedName` is not recorded when the asset that
933+
would carry it is being deleted.
934+
935+
A generated file inherits nothing from the one it was read from: what the
936+
original's info says about its hashes, its module and where its source came
937+
from is true of that file and not of this one. The exception is `immutable`,
938+
and only where `filename` still derives from the original's name — `[name]`,
939+
`[base]` or `[file]` — since that is what carried the hash the promise rests
940+
on.
879941
880942
**When** a generator runs is not among them, because it is not the config's to
881943
say: the implementation declares it through a `getStage` of its own, the way it
@@ -934,6 +996,46 @@ what `stage` states rather than what applying two plugins in the right order
934996
happens to give. Each algorithm is run at its own maximum by default (`zlib`'s
935997
best level, brotli's best quality); name `compressionOptions` to say otherwise.
936998
999+
#### Compressing with minifying, and without
1000+
1001+
The example above does both: `minify` defaults to terser, so the bundle is
1002+
minified and the compressed files are written from what minification produced.
1003+
That is the shape to want — one pass of filtering, one cache, and `stage`
1004+
ordering the two — and it needs nothing said about `minify` at all:
1005+
1006+
```js
1007+
new MinimizerPlugin({
1008+
test: /\.(js|css|html|svg)$/i,
1009+
generate: {
1010+
implementation: MinimizerPlugin.compress,
1011+
options: { algorithm: "gzip" },
1012+
type: "asset",
1013+
filename: "[path][base].gz",
1014+
},
1015+
});
1016+
```
1017+
1018+
Compressing **only** — an instance that must not touch what it reads — says so
1019+
with an empty list of minimizers. `test` is then yours to state too, since the
1020+
`.js` default belongs to minifying:
1021+
1022+
```js
1023+
new MinimizerPlugin({
1024+
test: /.*/,
1025+
minify: [],
1026+
generate: {
1027+
implementation: MinimizerPlugin.compress,
1028+
options: { algorithm: "gzip" },
1029+
type: "asset",
1030+
filename: "[path][base].gz",
1031+
},
1032+
});
1033+
```
1034+
1035+
Nothing is minified, no asset is marked `minimized`, and the bundle keeps the
1036+
name it would have had without this plugin — an instance that rewrites nothing
1037+
salts no hash.
1038+
9371039
It is an ordinary minimizer too, so [`minify`](#minify) takes it the way it
9381040
takes `terserMinify` or `swcMinify`. There it compresses the asset **in place**
9391041
rather than beside it — the shape for a server that says what the encoding is

‎src/index.js‎

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -211,9 +211,9 @@ const {
211211
* @property {MinimizerImplementation<EXPECTED_ANY>} implementation the generator itself
212212
* @property {MinimizerOptions<EXPECTED_ANY>=} options options for this generator, preferred over the deprecated `generatorOptions`
213213
* @property {("import" | "asset")=} type `import` re-encodes a module as it is built, so the import that asked for it is renamed with it; `asset` writes a new file beside one already emitted
214-
* @property {string=} filename name for the generated asset, as a webpack filename template. `asset` generators only
214+
* @property {(string | ((pathData: EXPECTED_ANY) => string))=} filename name for the generated asset, as a webpack filename template or a function answering with one. `asset` generators only
215215
* @property {((name: string) => boolean)=} filter decides per asset whether to generate from it, on top of `test`/`include`/`exclude`
216-
* @property {boolean=} deleteOriginalAssets removes the asset generated from. `asset` generators only
216+
* @property {(boolean | ((name: string) => boolean))=} deleteOriginalAssets removes the asset generated from, its own file alone — whatever its `related` names stays. Written as a function it is asked per asset. `asset` generators only
217217
* @property {number=} threshold generate only from assets larger than this, in bytes. `asset` generators only
218218
* @property {number=} minRatio keep the generated asset only when it is this much smaller than the one it was read from. `asset` generators only
219219
* @property {(string | false)=} relatedName the key the generated asset is recorded under in the original's `related` info. `asset` generators only
@@ -237,8 +237,24 @@ const {
237237
*/
238238

239239
/**
240+
* One minimizer, written as an object stating how to run it.
240241
* @template T
241-
* @typedef {T extends import("terser").MinifyOptions ? { minify?: MinimizerImplementation<T> | undefined, minimizerOptions?: MinimizerOptions<T> | undefined, terserOptions?: MinimizerOptions<T> | undefined } : { minify: MinimizerImplementation<T>, minimizerOptions?: MinimizerOptions<T> | undefined, terserOptions?: MinimizerOptions<T> | undefined }} DefinedDefaultMinimizerAndOptions
242+
* @typedef {object} MinimizerDescriptor
243+
* @property {MinimizerImplementation<T>} implementation the minimizer itself
244+
* @property {MinimizerOptions<T>=} options options for this minimizer, preferred over the deprecated `minimizerOptions`
245+
* @property {((name: string, info: AssetInfo) => boolean | undefined)=} filter which assets this minimizer is offered, overriding a `filter` on the function itself
246+
*/
247+
248+
/**
249+
* What `minify` may be written as: one minimizer, a list of them — empty for
250+
* nothing to minify — or a descriptor.
251+
* @template T
252+
* @typedef {MinimizerImplementation<T> | (MinimizerImplementation<T> | MinimizerDescriptor<T>)[] | MinimizerDescriptor<T>} Minify
253+
*/
254+
255+
/**
256+
* @template T
257+
* @typedef {T extends import("terser").MinifyOptions ? { minify?: Minify<T> | undefined, minimizerOptions?: MinimizerOptions<T> | undefined, terserOptions?: MinimizerOptions<T> | undefined } : { minify: Minify<T>, minimizerOptions?: MinimizerOptions<T> | undefined, terserOptions?: MinimizerOptions<T> | undefined }} DefinedDefaultMinimizerAndOptions
242258
*/
243259

244260
/**
@@ -1355,7 +1371,7 @@ class MinimizerPlugin {
13551371
* @param {string | undefined} name the preset it is written under, where it has one
13561372
* @param {EXPECTED_ANY} entry what was written there
13571373
* @param {EXPECTED_ANY} declared what `generatorOptions` says for it
1358-
* @returns {{ name: string | undefined, implementation: EXPECTED_ANY, options: EXPECTED_ANY, type: string | undefined, filename: string | undefined, filter: ((name: string) => boolean) | undefined, deleteOriginalAssets: boolean | undefined, threshold: number | undefined, minRatio: number | undefined, relatedName: string | false | undefined }} the generator
1374+
* @returns {{ name: string | undefined, implementation: EXPECTED_ANY, options: EXPECTED_ANY, type: string | undefined, filename: string | ((pathData: EXPECTED_ANY) => string) | undefined, filter: ((name: string) => boolean) | undefined, deleteOriginalAssets: boolean | ((name: string) => boolean) | undefined, threshold: number | undefined, minRatio: number | undefined, relatedName: string | false | undefined }} the generator
13591375
*/
13601376
describeGenerator(name, entry, declared) {
13611377
const descriptor = isDescriptor(entry) ? entry : undefined;
@@ -1758,21 +1774,37 @@ class MinimizerPlugin {
17581774
compilation.emitAsset(generatedName, generatedSource, generatedInfo);
17591775
}
17601776

1761-
if (generator.deleteOriginalAssets) {
1762-
// Deleting an asset takes everything its `related` names with it, so
1763-
// recording this file there first would delete the file just written.
1777+
const deletes =
1778+
typeof generator.deleteOriginalAssets === "function"
1779+
? generator.deleteOriginalAssets(name)
1780+
: generator.deleteOriginalAssets;
1781+
1782+
if (deletes) {
1783+
const still = compilation.getAsset(name);
1784+
17641785
// A generator writing under the original's own name leaves nothing to
1765-
// delete either: that file is now the generated one.
1766-
if (generatedName !== name && compilation.getAsset(name)) {
1786+
// delete: that file is now the generated one. Another generator of this
1787+
// pass may have written over it too, which is the same thing — what the
1788+
// name holds is no longer what this one read.
1789+
if (generatedName !== name && still && still.source === source) {
1790+
// Deleting an asset takes everything its `related` names with it — a
1791+
// source map, another generator's file — so it goes alone.
1792+
compilation.updateAsset(name, source, (was) => {
1793+
const { related, ...rest } = was || {};
1794+
1795+
return rest;
1796+
});
17671797
compilation.deleteAsset(name);
17681798
}
17691799

17701800
return;
17711801
}
17721802

17731803
// Recorded on the asset it was read from, which is how a server asked for
1774-
// that one finds this one.
1775-
if (generator.relatedName) {
1804+
// that one finds this one. A file written under that same name has replaced
1805+
// it, so there is nothing left to point anywhere, and writing the source
1806+
// back would undo what was just generated.
1807+
if (generator.relatedName && generatedName !== name) {
17761808
compilation.updateAsset(name, source, {
17771809
related: { [generator.relatedName]: generatedName },
17781810
});
@@ -2156,7 +2188,9 @@ class MinimizerPlugin {
21562188
const written = Array.isArray(minify) ? minify : [minify];
21572189

21582190
for (const [index, one] of written.entries()) {
2159-
const own = isDescriptor(one) ? one.options : undefined;
2191+
const own = isDescriptor(one)
2192+
? /** @type {MinimizerDescriptor<EXPECTED_ANY>} */ (one).options
2193+
: undefined;
21602194
const twice = Array.isArray(minify)
21612195
? getMinimizerOptionsAt(declared, index)
21622196
: declared;
@@ -2355,6 +2389,12 @@ class MinimizerPlugin {
23552389
});
23562390

23572391
hooks.chunkHash.tap(pluginName, (chunk, hash) => {
2392+
// Nothing minifying rewrites nothing, so no name owes it a hash of its
2393+
// own.
2394+
if (this.minimizers().length === 0) {
2395+
return;
2396+
}
2397+
23582398
const willBe = chunkAssetName(compilation, chunk);
23592399

23602400
// A chunk this instance was never pointed at cannot vary with its
@@ -2377,7 +2417,10 @@ class MinimizerPlugin {
23772417
/** @type {EmbeddedSourceHooks} */
23782418
(/** @type {unknown} */ (compilation.hooks));
23792419

2420+
// Nothing minifying rewrites no embedded source either, and salting the
2421+
// module hash would rename a file this instance never touches.
23802422
if (
2423+
this.minimizers().length > 0 &&
23812424
embeddedHooks.renderEmbeddedSource &&
23822425
embeddedHooks.embeddedSourceHash
23832426
) {

0 commit comments

Comments
 (0)