} Promise that resolves to the minified result
187 | */
188 | async function swcMinifyFragment(input, minimizerOptions = {}) {
189 | const swcMinifier = require("@swc/html");
190 |
191 | const [[, code]] = Object.entries(input);
192 | const options = /** @type {import("@swc/html").FragmentOptions} */ ({
193 | ...minimizerOptions,
194 | });
195 | const result = await swcMinifier.minifyFragment(Buffer.from(code), options);
196 |
197 | return {
198 | code: result.code,
199 | errors: result.errors
200 | ? result.errors.map((diagnostic) => {
201 | const error =
202 | /** @type {Error & { span: EXPECTED_ANY; level: EXPECTED_ANY }} */ (
203 | new Error(diagnostic.message)
204 | );
205 |
206 | error.span = diagnostic.span;
207 |
208 | error.level = diagnostic.level;
209 |
210 | return error;
211 | })
212 | : undefined,
213 | };
214 | }
215 |
216 | /**
217 | * @returns {boolean} Whether worker threads are supported
218 | */
219 | swcMinifyFragment.supportsWorkerThreads = () => false;
220 |
221 | /**
222 | * @template T
223 | * @param {(() => EXPECTED_ANY) | undefined} fn The function to memoize
224 | * @returns {() => T} The memoized function
225 | */
226 | function memoize(fn) {
227 | let cache = false;
228 | /** @type {T} */
229 | let result;
230 |
231 | return () => {
232 | if (cache) {
233 | return result;
234 | }
235 | result = /** @type {() => EXPECTED_ANY} */ (fn)();
236 | cache = true;
237 | // Allow to clean up memory for fn
238 | // and all dependent resources
239 |
240 | fn = undefined;
241 |
242 | return result;
243 | };
244 | }
245 |
246 | module.exports = {
247 | htmlMinifierTerser,
248 | memoize,
249 | minifyHtmlNode,
250 | swcMinify,
251 | swcMinifyFragment,
252 | throttleAll,
253 | };
254 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4 |
5 | ### [5.0.4](https://github.com/webpack/html-minimizer-webpack-plugin/compare/v5.0.3...v5.0.4) (2025-12-05)
6 |
7 |
8 | ### Bug Fixes
9 |
10 | * respect errors and warnings from minimizer without code ([1aa2a80](https://github.com/webpack/html-minimizer-webpack-plugin/commit/1aa2a80821aa29d8066227033585c778a9cd6f31))
11 |
12 | ### [5.0.3](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/compare/v5.0.2...v5.0.3) (2025-07-28)
13 |
14 |
15 | ### Bug Fixes
16 |
17 | * don't lose errors and warnings from minimizers ([#157](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/issues/157)) ([c409a32](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/commit/c409a32109d3b586810127311021fa6f4c2dc8a3))
18 |
19 | ### [5.0.2](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/compare/v5.0.1...v5.0.2) (2025-04-07)
20 |
21 |
22 | ### Bug Fixes
23 |
24 | * types ([#149](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/issues/149)) ([4d71e4f](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/commit/4d71e4f303076264e418a494e7903daed28954c8))
25 |
26 | ### [5.0.1](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/compare/v5.0.0...v5.0.1) (2025-03-06)
27 |
28 |
29 | ### Bug Fixes
30 |
31 | * better support worker threads ([fa90ada](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/commit/fa90adaa4a83ab532690db0e739bc350cc6b2c5d))
32 | * use `os.availableParallelism()`` for parallelism when it is available ([b88e9f4](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/commit/b88e9f45a930aae9f468eb5587bd73f7f2196892))
33 |
34 | ## [5.0.0](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/compare/v4.4.0...v5.0.0) (2024-01-17)
35 |
36 |
37 | ### ⚠ BREAKING CHANGES
38 |
39 | * minimum supported Node.js version is `18.12.0` ([#127](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/issues/127)) ([57da672](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/commit/57da672f346933982869a70ae051d2bcfac3209c))
40 |
41 | ## [4.4.0](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/compare/v4.3.0...v4.4.0) (2023-06-10)
42 |
43 |
44 | ### Features
45 |
46 | * added `@minify-html/node` support ([#117](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/issues/117)) ([966de45](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/commit/966de45db5de81fcb31da34efbe2946c5cb791b5))
47 |
48 | ## [4.3.0](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/compare/v4.2.1...v4.3.0) (2022-10-13)
49 |
50 |
51 | ### Features
52 |
53 | * added `swcMinifyFragment` to minify HTML fragments ([021c752](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/commit/021c7524072328f9442604907656ad63227ef980))
54 |
55 |
56 | ### Bug Fixes
57 |
58 | * compatibility with swc ([#87](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/issues/87)) ([afaf453](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/commit/afaf45329e4d50f015baf27a2cc9409efce3a946))
59 |
60 | ### [4.2.1](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/compare/v4.2.0...v4.2.1) (2022-10-06)
61 |
62 |
63 | ### Bug Fixes
64 |
65 | * crash ([#86](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/issues/86)) ([f11fe4e](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/commit/f11fe4e59b9a81b79bd438aeed8570fe46683958))
66 |
67 | ## [4.2.0](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/compare/v4.1.0...v4.2.0) (2022-09-29)
68 |
69 |
70 | ### Features
71 |
72 | * added `SWC` HTML minifier ([#81](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/issues/81)) ([8481f8c](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/commit/8481f8ce7d835470873cebb847cb636f9c8b52f5))
73 |
74 | ## [4.1.0](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/compare/v4.0.0...v4.1.0) (2022-08-17)
75 |
76 |
77 | ### Features
78 |
79 | * update `html-minifier-terser` to v7 ([#78](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/issues/78)) ([4d9c5bf](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/commit/4d9c5bff31ce73fd08f6981700c61ac7b1fbbfc0))
80 |
81 | ## [4.0.0](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/compare/v3.5.0...v4.0.0) (2022-05-17)
82 |
83 |
84 | ### ⚠ BREAKING CHANGES
85 |
86 | * minimum supported `Node.js` version is `14.15.0`
87 |
88 | ## [3.5.0](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/compare/v3.4.0...v3.5.0) (2021-12-16)
89 |
90 |
91 | ### Features
92 |
93 | * removed cjs wrapper and generated types in commonjs format (`export =` and `namespaces` used in types), now you can directly use exported types ([e4c64c8](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/commit/e4c64c8c9d0cee2f6545893252738626d51503f1))
94 |
95 | ## [3.4.0](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/compare/v3.3.2...v3.4.0) (2021-12-06)
96 |
97 |
98 | ### Features
99 |
100 | * added types ([#43](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/issues/43)) ([67338f0](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/commit/67338f0d92bf4adc5c49aeabb969b747bf877dd9))
101 |
102 | ### [3.3.2](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/compare/v3.3.1...v3.3.2) (2021-11-17)
103 |
104 |
105 | ### Chore
106 |
107 | * update `schema-utils` package to `4.0.0` version
108 |
109 | ### [3.3.1](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/compare/v3.3.0...v3.3.1) (2021-11-08)
110 |
111 | ### Chore
112 |
113 | * avoid usage `p-limit` package
114 |
115 | ## [3.3.0](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/compare/v3.2.0...v3.3.0) (2021-10-04)
116 |
117 |
118 | ### Features
119 |
120 | * better validation errors ([#39](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/issues/39)) ([018d432](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/commit/018d432ca37362e66c7f6ef28834600747135fb7))
121 |
122 |
123 | ### Bug Fixes
124 |
125 | * warning and error text messages ([#38](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/issues/38)) ([235429e](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/commit/235429ea476f9addbe7b5c3cbbb0a4fd3b40218f))
126 |
127 | ## [3.2.0](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/compare/v3.1.1...v3.2.0) (2021-08-31)
128 |
129 |
130 | ### Features
131 |
132 | * update `html-minifier-terser` package ([#37](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/issues/37)) ([268a5e6](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/commit/268a5e6e5a3bb25bccdd9a3bc986bcd37688dfe9))
133 |
134 | ### [3.1.1](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/compare/v3.1.0...v3.1.1) (2021-06-25)
135 |
136 | ### Chore
137 |
138 | * update `serialize-javascript`
139 |
140 | ## [3.1.0](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/compare/v3.0.0...v3.1.0) (2021-05-27)
141 |
142 |
143 | ### Features
144 |
145 | * allow to return object in `minify` function ([43ae683](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/commit/43ae6838e54f5adea23e82c66db1fd493c7efd95))
146 |
147 | ## [3.0.0](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/compare/v2.1.0...v3.0.0) (2021-05-26)
148 |
149 |
150 | ### ⚠ BREAKING CHANGES
151 |
152 | * minimum supported `Node.js` version is `12.13.0`
153 |
154 | ### Features
155 |
156 | * added support multiple `minify` functions ([0763136](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/commit/0763136d7b763a9802f1b4da156518dc05f1ec2d))
157 |
158 | ## [2.1.0](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/compare/v2.0.0...v2.1.0) (2021-01-08)
159 |
160 |
161 | ### Features
162 |
163 | * optimize HTML assets added later by plugins ([7198dac](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/commit/7198dac4f5c9a0b91e586d64b79ae16133a16447))
164 |
165 | ## [2.0.0](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/compare/v1.0.1...v2.0.0) (2020-11-09)
166 |
167 |
168 | ### ⚠ BREAKING CHANGES
169 |
170 | * minimum supported `webpack` version is `5.1.0`
171 | * removed the `cache` option (webpack automatically caches assets, please read https://webpack.js.org/configuration/other-options/#cache)
172 | * removed the `cacheKeys` option (webpack automatically caches assets, please read https://webpack.js.org/configuration/other-options/#cache)
173 |
174 | ### [1.0.1](https://github.com/webpack-contrib/html-minimizer-webpack-plugin/compare/v1.0.0...v1.0.1) (2020-10-07)
175 |
176 | ### Chore
177 |
178 | * update `schema-utils`
179 |
180 | ## 1.0.0 (2020-10-03)
181 |
182 | Initial release
183 |
184 | # Change Log
185 |
186 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
187 |
--------------------------------------------------------------------------------
/test/__snapshots__/test-option.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`when applied with "test" option matches snapshot for a single "test" value (RegExp): assets 1`] = `
4 | {
5 | "parallel/foo-0.html": "
6 |
7 |
8 |
9 |
11 |
12 | Cache
13 |
14 |
15 |
16 | My First Heading
17 | My first paragraph.
18 | An Unordered HTML List
19 |
20 |
21 | - Coffee
22 | - Tea
23 | - Milk
24 |
25 |
26 | An Ordered HTML List
27 |
28 |
29 | - Coffee
30 | - Tea
31 | - Milk
32 |
33 |
34 |
35 |
36 | ",
37 | "parallel/foo-1.html": " Cache-1 My First Heading
My first paragraph.
An Unordered HTML List
An Ordered HTML List
- Coffee
- Tea
- Milk
",
38 | "parallel/foo-2.html": " Cache-2 My First Heading
My first paragraph.
An Unordered HTML List
An Ordered HTML List
- Coffee
- Tea
- Milk
",
39 | "parallel/foo-3.html": " Cache-3 My First Heading
My first paragraph.
An Unordered HTML List
An Ordered HTML List
- Coffee
- Tea
- Milk
",
40 | "parallel/foo-4.html": "
41 |
42 |
43 |
44 |
46 |
47 | Cache-4
48 |
49 |
50 |
51 | My First Heading
52 | My first paragraph.
53 | An Unordered HTML List
54 |
55 |
56 | - Coffee
57 | - Tea
58 | - Milk
59 |
60 |
61 | An Ordered HTML List
62 |
63 |
64 | - Coffee
65 | - Tea
66 | - Milk
67 |
68 |
69 |
70 |
71 | ",
72 | }
73 | `;
74 |
75 | exports[`when applied with "test" option matches snapshot for a single "test" value (RegExp): errors 1`] = `[]`;
76 |
77 | exports[`when applied with "test" option matches snapshot for a single "test" value (RegExp): warnings 1`] = `[]`;
78 |
79 | exports[`when applied with "test" option matches snapshot for multiple "test" value (RegExp): assets 1`] = `
80 | {
81 | "parallel/foo-0.html": " Cache My First Heading
My first paragraph.
An Unordered HTML List
An Ordered HTML List
- Coffee
- Tea
- Milk
",
82 | "parallel/foo-1.html": " Cache-1 My First Heading
My first paragraph.
An Unordered HTML List
An Ordered HTML List
- Coffee
- Tea
- Milk
",
83 | "parallel/foo-2.html": " Cache-2 My First Heading
My first paragraph.
An Unordered HTML List
An Ordered HTML List
- Coffee
- Tea
- Milk
",
84 | "parallel/foo-3.html": "
85 |
86 |
87 |
88 |
90 |
91 | Cache-3
92 |
93 |
94 |
95 | My First Heading
96 | My first paragraph.
97 | An Unordered HTML List
98 |
99 |
100 | - Coffee
101 | - Tea
102 | - Milk
103 |
104 |
105 | An Ordered HTML List
106 |
107 |
108 | - Coffee
109 | - Tea
110 | - Milk
111 |
112 |
113 |
114 |
115 | ",
116 | "parallel/foo-4.html": "
117 |
118 |
119 |
120 |
122 |
123 | Cache-4
124 |
125 |
126 |
127 | My First Heading
128 | My first paragraph.
129 | An Unordered HTML List
130 |
131 |
132 | - Coffee
133 | - Tea
134 | - Milk
135 |
136 |
137 | An Ordered HTML List
138 |
139 |
140 | - Coffee
141 | - Tea
142 | - Milk
143 |
144 |
145 |
146 |
147 | ",
148 | }
149 | `;
150 |
151 | exports[`when applied with "test" option matches snapshot for multiple "test" value (RegExp): errors 1`] = `[]`;
152 |
153 | exports[`when applied with "test" option matches snapshot for multiple "test" value (RegExp): warnings 1`] = `[]`;
154 |
155 | exports[`when applied with "test" option matches snapshot with empty value: assets 1`] = `
156 | {
157 | "parallel/foo-0.html": " Cache My First Heading
My first paragraph.
An Unordered HTML List
An Ordered HTML List
- Coffee
- Tea
- Milk
",
158 | "parallel/foo-1.html": " Cache-1 My First Heading
My first paragraph.
An Unordered HTML List
An Ordered HTML List
- Coffee
- Tea
- Milk
",
159 | "parallel/foo-2.html": " Cache-2 My First Heading
My first paragraph.
An Unordered HTML List
An Ordered HTML List
- Coffee
- Tea
- Milk
",
160 | "parallel/foo-3.html": " Cache-3 My First Heading
My first paragraph.
An Unordered HTML List
An Ordered HTML List
- Coffee
- Tea
- Milk
",
161 | "parallel/foo-4.html": " Cache-4 My First Heading
My first paragraph.
An Unordered HTML List
An Ordered HTML List
- Coffee
- Tea
- Milk
",
162 | }
163 | `;
164 |
165 | exports[`when applied with "test" option matches snapshot with empty value: errors 1`] = `[]`;
166 |
167 | exports[`when applied with "test" option matches snapshot with empty value: warnings 1`] = `[]`;
168 |
--------------------------------------------------------------------------------
/test/parallel-option.test.js:
--------------------------------------------------------------------------------
1 | import os from "node:os";
2 |
3 | import { Worker } from "jest-worker";
4 |
5 | import HtmlMinimizerPlugin from "../src/index";
6 |
7 | import {
8 | compile,
9 | getCompiler,
10 | getErrors,
11 | getWarnings,
12 | readAssets,
13 | } from "./helpers";
14 |
15 | const ENABLE_WORKER_THREADS =
16 | typeof process.env.ENABLE_WORKER_THREADS !== "undefined"
17 | ? process.env.ENABLE_WORKER_THREADS === "true"
18 | : true;
19 |
20 | jest.mock("os", () => {
21 | const actualOs = jest.requireActual("os");
22 | const isAvailableParallelism =
23 | typeof actualOs.availableParallelism !== "undefined";
24 |
25 | const mocked = {
26 | availableParallelism: isAvailableParallelism ? jest.fn(() => 4) : undefined,
27 | cpus: jest.fn(() => ({ length: 4 })),
28 | };
29 |
30 | return { ...actualOs, ...mocked };
31 | });
32 |
33 | // Based on https://github.com/facebook/jest/blob/edde20f75665c2b1e3c8937f758902b5cf28a7b4/packages/jest-runner/src/__tests__/test_runner.test.js
34 | let workerTransform;
35 | let workerEnd;
36 |
37 | jest.mock("jest-worker", () => ({
38 | Worker: jest.fn().mockImplementation((workerPath) => ({
39 | transform: (workerTransform = jest.fn((data) =>
40 | require(workerPath).transform(data),
41 | )),
42 | end: (workerEnd = jest.fn()),
43 | getStderr: jest.fn(),
44 | getStdout: jest.fn(),
45 | })),
46 | }));
47 |
48 | const workerPath = require.resolve("../src/minify");
49 |
50 | const getParallelism = () => {
51 | if (typeof os.availableParallelism !== "undefined") {
52 | return os.availableParallelism();
53 | }
54 |
55 | return os.cpus().length;
56 | };
57 |
58 | describe("parallel option", () => {
59 | let compiler;
60 |
61 | beforeEach(() => {
62 | jest.clearAllMocks();
63 |
64 | const testHtmlId = "./parallel/foo-(0|1|2|3|4).html";
65 |
66 | compiler = getCompiler(testHtmlId);
67 | });
68 |
69 | it("should match snapshot when a value is not specify", async () => {
70 | new HtmlMinimizerPlugin().apply(compiler);
71 |
72 | const stats = await compile(compiler);
73 |
74 | expect(Worker).toHaveBeenCalledTimes(1);
75 | expect(Worker).toHaveBeenLastCalledWith(workerPath, {
76 | enableWorkerThreads: ENABLE_WORKER_THREADS,
77 | numWorkers: getParallelism() - 1,
78 | });
79 | expect(workerTransform).toHaveBeenCalledTimes(
80 | Object.keys(readAssets(compiler, stats, /\.html$/i)).length,
81 | );
82 | expect(workerEnd).toHaveBeenCalledTimes(1);
83 |
84 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("assets");
85 | expect(getErrors(stats)).toMatchSnapshot("errors");
86 | expect(getWarnings(stats)).toMatchSnapshot("warnings");
87 | });
88 |
89 | it('should match snapshot for the "false" value', async () => {
90 | new HtmlMinimizerPlugin({ parallel: false }).apply(compiler);
91 |
92 | const stats = await compile(compiler);
93 |
94 | expect(Worker).toHaveBeenCalledTimes(0);
95 |
96 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("assets");
97 | expect(getErrors(stats)).toMatchSnapshot("errors");
98 | expect(getWarnings(stats)).toMatchSnapshot("warnings");
99 | });
100 |
101 | it('should match snapshot for the "true" value', async () => {
102 | new HtmlMinimizerPlugin({ parallel: true }).apply(compiler);
103 |
104 | const stats = await compile(compiler);
105 |
106 | expect(Worker).toHaveBeenCalledTimes(1);
107 | expect(Worker).toHaveBeenLastCalledWith(workerPath, {
108 | enableWorkerThreads: ENABLE_WORKER_THREADS,
109 | numWorkers: getParallelism() - 1,
110 | });
111 | expect(workerTransform).toHaveBeenCalledTimes(
112 | Object.keys(readAssets(compiler, stats, /\.html$/i)).length,
113 | );
114 | expect(workerEnd).toHaveBeenCalledTimes(1);
115 |
116 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("assets");
117 | expect(getErrors(stats)).toMatchSnapshot("errors");
118 | expect(getWarnings(stats)).toMatchSnapshot("warnings");
119 | });
120 |
121 | it('should match snapshot for the "undefined" value', async () => {
122 | new HtmlMinimizerPlugin({ parallel: undefined }).apply(compiler);
123 |
124 | const stats = await compile(compiler);
125 |
126 | expect(Worker).toHaveBeenCalledTimes(1);
127 | expect(Worker).toHaveBeenLastCalledWith(workerPath, {
128 | enableWorkerThreads: ENABLE_WORKER_THREADS,
129 | numWorkers: getParallelism() - 1,
130 | });
131 | expect(workerTransform).toHaveBeenCalledTimes(
132 | Object.keys(readAssets(compiler, stats, /\.html$/i)).length,
133 | );
134 | expect(workerEnd).toHaveBeenCalledTimes(1);
135 |
136 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("assets");
137 | expect(getErrors(stats)).toMatchSnapshot("errors");
138 | expect(getWarnings(stats)).toMatchSnapshot("warnings");
139 | });
140 |
141 | it('should match snapshot for the "2" value', async () => {
142 | new HtmlMinimizerPlugin({ parallel: 2 }).apply(compiler);
143 |
144 | const stats = await compile(compiler);
145 |
146 | expect(Worker).toHaveBeenCalledTimes(1);
147 | expect(Worker).toHaveBeenLastCalledWith(workerPath, {
148 | enableWorkerThreads: ENABLE_WORKER_THREADS,
149 | numWorkers: 2,
150 | });
151 | expect(workerTransform).toHaveBeenCalledTimes(
152 | Object.keys(readAssets(compiler, stats, /\.html$/i)).length,
153 | );
154 | expect(workerEnd).toHaveBeenCalledTimes(1);
155 |
156 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("assets");
157 | expect(getErrors(stats)).toMatchSnapshot("errors");
158 | expect(getWarnings(stats)).toMatchSnapshot("warnings");
159 | });
160 |
161 | it('should match snapshot for the "true" value when only one file passed', async () => {
162 | const testHtmlId = "./simple.html";
163 |
164 | compiler = getCompiler(testHtmlId);
165 |
166 | new HtmlMinimizerPlugin({ parallel: true }).apply(compiler);
167 |
168 | const stats = await compile(compiler);
169 |
170 | expect(Worker).toHaveBeenCalledTimes(1);
171 | expect(Worker).toHaveBeenLastCalledWith(workerPath, {
172 | enableWorkerThreads: ENABLE_WORKER_THREADS,
173 | numWorkers: Math.min(1, os.cpus().length - 1),
174 | });
175 | expect(workerTransform).toHaveBeenCalledTimes(
176 | Object.keys(readAssets(compiler, stats, /\.html$/i)).length,
177 | );
178 | expect(workerEnd).toHaveBeenCalledTimes(1);
179 |
180 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("assets");
181 | expect(getErrors(stats)).toMatchSnapshot("errors");
182 | expect(getWarnings(stats)).toMatchSnapshot("warnings");
183 | });
184 |
185 | it('should match snapshot for the "true" value and the number of files is less than the number of cores', async () => {
186 | const entries = [];
187 |
188 | for (let i = 0; i < os.cpus().length / 2; i++) {
189 | entries.push(i);
190 | }
191 |
192 | const testHtmlId = `./parallel/foo-(${entries.join("|")}).html`;
193 |
194 | compiler = getCompiler(testHtmlId);
195 |
196 | new HtmlMinimizerPlugin({ parallel: true }).apply(compiler);
197 |
198 | const stats = await compile(compiler);
199 |
200 | expect(Worker).toHaveBeenCalledTimes(1);
201 | expect(Worker).toHaveBeenLastCalledWith(workerPath, {
202 | enableWorkerThreads: ENABLE_WORKER_THREADS,
203 | numWorkers: Math.min(entries.length, os.cpus().length - 1),
204 | });
205 | expect(workerTransform).toHaveBeenCalledTimes(
206 | Object.keys(readAssets(compiler, stats, /\.html$/i)).length,
207 | );
208 | expect(workerEnd).toHaveBeenCalledTimes(1);
209 |
210 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("assets");
211 | expect(getErrors(stats)).toMatchSnapshot("errors");
212 | expect(getWarnings(stats)).toMatchSnapshot("warnings");
213 | });
214 |
215 | it('should match snapshot for the "true" value and the number of files is same than the number of cores', async () => {
216 | const entries = [];
217 |
218 | for (let i = 0; i < os.cpus().length; i++) {
219 | entries.push(i);
220 | }
221 |
222 | const testHtmlId = `./parallel/foo-(${entries.join("|")}).html`;
223 |
224 | compiler = getCompiler(testHtmlId);
225 |
226 | new HtmlMinimizerPlugin({ parallel: true }).apply(compiler);
227 |
228 | const stats = await compile(compiler);
229 |
230 | expect(Worker).toHaveBeenCalledTimes(1);
231 | expect(Worker).toHaveBeenLastCalledWith(workerPath, {
232 | enableWorkerThreads: ENABLE_WORKER_THREADS,
233 | numWorkers: Math.min(Object.keys(entries).length, os.cpus().length - 1),
234 | });
235 | expect(workerTransform).toHaveBeenCalledTimes(
236 | Object.keys(readAssets(compiler, stats, /\.html$/i)).length,
237 | );
238 | expect(workerEnd).toHaveBeenCalledTimes(1);
239 |
240 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("assets");
241 | expect(getErrors(stats)).toMatchSnapshot("errors");
242 | expect(getWarnings(stats)).toMatchSnapshot("warnings");
243 | });
244 |
245 | it('should match snapshot for the "true" value and the number of files is more than the number of cores', async () => {
246 | const entries = [];
247 |
248 | for (let i = 0; i < os.cpus().length * 2; i++) {
249 | entries.push(i);
250 | }
251 |
252 | const testHtmlId = `./parallel/foo-(${entries.join("|")}).html`;
253 |
254 | compiler = getCompiler(testHtmlId);
255 |
256 | new HtmlMinimizerPlugin({ parallel: true }).apply(compiler);
257 |
258 | const stats = await compile(compiler);
259 |
260 | expect(Worker).toHaveBeenCalledTimes(1);
261 | expect(Worker).toHaveBeenLastCalledWith(workerPath, {
262 | enableWorkerThreads: ENABLE_WORKER_THREADS,
263 | numWorkers: Math.min(Object.keys(entries).length, os.cpus().length - 1),
264 | });
265 | expect(workerTransform).toHaveBeenCalledTimes(
266 | Object.keys(readAssets(compiler, stats, /\.html$/i)).length,
267 | );
268 | expect(workerEnd).toHaveBeenCalledTimes(1);
269 |
270 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("assets");
271 | expect(getErrors(stats)).toMatchSnapshot("errors");
272 | expect(getWarnings(stats)).toMatchSnapshot("warnings");
273 | });
274 | });
275 |
--------------------------------------------------------------------------------
/test/HtmlMinimizerPlugin.test.js:
--------------------------------------------------------------------------------
1 | import path from "node:path";
2 |
3 | import HtmlMinimizerPlugin from "../src/index";
4 |
5 | import {
6 | EmitNewAsset,
7 | ModifyExistingAsset,
8 | compile,
9 | getCompiler,
10 | getErrors,
11 | getWarnings,
12 | readAssets,
13 | } from "./helpers";
14 |
15 | describe("HtmlMinimizerPlugin", () => {
16 | it("should work (without options)", async () => {
17 | const testHtmlId = "./simple.html";
18 | const compiler = getCompiler(testHtmlId);
19 |
20 | new HtmlMinimizerPlugin().apply(compiler);
21 |
22 | const stats = await compile(compiler);
23 |
24 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("assets");
25 | expect(getErrors(stats)).toMatchSnapshot("errors");
26 | expect(getWarnings(stats)).toMatchSnapshot("warnings");
27 | });
28 |
29 | it("should work with an empty file", async () => {
30 | const testHtmlId = "./empty.html";
31 | const compiler = getCompiler(testHtmlId);
32 |
33 | new HtmlMinimizerPlugin().apply(compiler);
34 |
35 | const stats = await compile(compiler);
36 |
37 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("assets");
38 | expect(getErrors(stats)).toMatchSnapshot("errors");
39 | expect(getWarnings(stats)).toMatchSnapshot("warnings");
40 | });
41 |
42 | it("should work without files", async () => {
43 | const testHtmlId = "./simple.html";
44 | const compiler = getCompiler(testHtmlId);
45 |
46 | new HtmlMinimizerPlugin({
47 | include: "nothing",
48 | }).apply(compiler);
49 |
50 | const stats = await compile(compiler);
51 |
52 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("assets");
53 | expect(getErrors(stats)).toMatchSnapshot("errors");
54 | expect(getWarnings(stats)).toMatchSnapshot("warnings");
55 | });
56 |
57 | it("should write stdout and stderr of workers to stdout and stderr of main process in parallel mode", async () => {
58 | const { write: stdoutWrite } = process.stdout;
59 | const { write: stderrWrite } = process.stderr;
60 |
61 | let stdoutOutput = "";
62 | let stderrOutput = "";
63 |
64 | process.stdout.write = (str) => {
65 | stdoutOutput += str;
66 | };
67 |
68 | process.stderr.write = (str) => {
69 | stderrOutput += str;
70 | };
71 |
72 | const testHtmlId = "./parallel/foo-[1-3].html";
73 | const compiler = getCompiler(testHtmlId);
74 |
75 | new HtmlMinimizerPlugin({
76 | parallel: true,
77 | minify: () => {
78 | process.stdout.write("stdout\n");
79 |
80 | process.stderr.write("stderr\n");
81 |
82 | return ' foo
';
83 | },
84 | }).apply(compiler);
85 |
86 | const stats = await compile(compiler);
87 |
88 | expect(stdoutOutput).toMatchSnapshot("process stdout output");
89 | expect(stderrOutput).toMatchSnapshot("process stderr output");
90 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("assets");
91 | expect(getErrors(stats)).toMatchSnapshot("errors");
92 | expect(getWarnings(stats)).toMatchSnapshot("warnings");
93 |
94 | process.stdout.write = stdoutWrite;
95 | process.stderr.write = stderrWrite;
96 | });
97 |
98 | it("should work with child compilation", async () => {
99 | const testHtmlId = "./simple.html";
100 | const compiler = getCompiler(testHtmlId, {
101 | module: {
102 | rules: [
103 | {
104 | test: /entry.js$/i,
105 | use: [
106 | {
107 | loader: path.resolve(
108 | __dirname,
109 | "./helpers/emitAssetInChildCompilationLoader",
110 | ),
111 | },
112 | ],
113 | },
114 | ],
115 | },
116 | });
117 |
118 | new HtmlMinimizerPlugin().apply(compiler);
119 |
120 | const stats = await compile(compiler);
121 |
122 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("assets");
123 | expect(getErrors(stats)).toMatchSnapshot("errors");
124 | expect(getWarnings(stats)).toMatchSnapshot("warnings");
125 | });
126 |
127 | it("should emit error", async () => {
128 | const testHtmlId = "./simple.html";
129 | const compiler = getCompiler(testHtmlId);
130 |
131 | new HtmlMinimizerPlugin({
132 | minify: () => {
133 | throw new Error("Error message");
134 | },
135 | }).apply(compiler);
136 |
137 | const stats = await compile(compiler);
138 |
139 | expect(getErrors(stats)).toMatchSnapshot("errors");
140 | expect(getWarnings(stats)).toMatchSnapshot("warnings");
141 | });
142 |
143 | it("should emit error when broken html syntax", async () => {
144 | const testHtmlId = "./broken-html-syntax.html";
145 | const compiler = getCompiler(testHtmlId);
146 |
147 | new HtmlMinimizerPlugin().apply(compiler);
148 |
149 | const stats = await compile(compiler);
150 |
151 | expect(getErrors(stats)).toMatchSnapshot("errors");
152 | expect(getWarnings(stats)).toMatchSnapshot("warnings");
153 | });
154 |
155 | it('should work and use cache by default in "development" mode', async () => {
156 | const testHtmlId = false;
157 | const compiler = getCompiler(testHtmlId, {
158 | mode: "development",
159 | entry: {
160 | foo: path.resolve(__dirname, "./fixtures/cache.js"),
161 | },
162 | });
163 |
164 | new HtmlMinimizerPlugin().apply(compiler);
165 |
166 | const stats = await compile(compiler);
167 |
168 | expect(stats.compilation.emittedAssets.size).toBe(6);
169 |
170 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("result");
171 | expect(getErrors(stats)).toMatchSnapshot("errors");
172 | expect(getWarnings(stats)).toMatchSnapshot("warnings");
173 |
174 | const newStats = await compile(compiler);
175 |
176 | expect(newStats.compilation.emittedAssets.size).toBe(0);
177 |
178 | expect(readAssets(compiler, newStats, /\.html$/i)).toMatchSnapshot(
179 | "assets",
180 | );
181 | expect(getWarnings(newStats)).toMatchSnapshot("errors");
182 | expect(getErrors(newStats)).toMatchSnapshot("warnings");
183 | });
184 |
185 | it("should work and use memory cache", async () => {
186 | const testHtmlId = false;
187 | const compiler = getCompiler(testHtmlId, {
188 | cache: { type: "memory" },
189 | entry: {
190 | foo: path.resolve(__dirname, "./fixtures/cache.js"),
191 | },
192 | });
193 |
194 | new HtmlMinimizerPlugin().apply(compiler);
195 |
196 | const stats = await compile(compiler);
197 |
198 | expect(stats.compilation.emittedAssets.size).toBe(6);
199 |
200 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("result");
201 | expect(getErrors(stats)).toMatchSnapshot("errors");
202 | expect(getWarnings(stats)).toMatchSnapshot("warnings");
203 |
204 | const newStats = await compile(compiler);
205 |
206 | expect(newStats.compilation.emittedAssets.size).toBe(0);
207 |
208 | expect(readAssets(compiler, newStats, /\.html$/i)).toMatchSnapshot(
209 | "assets",
210 | );
211 | expect(getWarnings(newStats)).toMatchSnapshot("errors");
212 | expect(getErrors(newStats)).toMatchSnapshot("warnings");
213 | });
214 |
215 | it('should work and use memory cache when the "cache" option is "true"', async () => {
216 | const testHtmlId = false;
217 | const compiler = getCompiler(testHtmlId, {
218 | cache: true,
219 | entry: {
220 | foo: path.resolve(__dirname, "./fixtures/cache.js"),
221 | },
222 | });
223 |
224 | new HtmlMinimizerPlugin().apply(compiler);
225 |
226 | const stats = await compile(compiler);
227 |
228 | expect(stats.compilation.emittedAssets.size).toBe(6);
229 |
230 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("result");
231 | expect(getErrors(stats)).toMatchSnapshot("errors");
232 | expect(getWarnings(stats)).toMatchSnapshot("warnings");
233 |
234 | const newStats = await compile(compiler);
235 |
236 | expect(newStats.compilation.emittedAssets.size).toBe(0);
237 |
238 | expect(readAssets(compiler, newStats, /\.html$/i)).toMatchSnapshot(
239 | "assets",
240 | );
241 | expect(getWarnings(newStats)).toMatchSnapshot("errors");
242 | expect(getErrors(newStats)).toMatchSnapshot("warnings");
243 | });
244 |
245 | it('should work and use memory cache when the "cache" option is "true" and the asset has been changed', async () => {
246 | const testHtmlId = false;
247 | const compiler = getCompiler(testHtmlId, {
248 | cache: true,
249 | entry: {
250 | foo: path.resolve(__dirname, "./fixtures/cache.js"),
251 | },
252 | });
253 |
254 | new HtmlMinimizerPlugin().apply(compiler);
255 |
256 | const stats = await compile(compiler);
257 |
258 | expect(stats.compilation.emittedAssets.size).toBe(6);
259 |
260 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("result");
261 | expect(getErrors(stats)).toMatchSnapshot("errors");
262 | expect(getWarnings(stats)).toMatchSnapshot("warnings");
263 |
264 | new ModifyExistingAsset({ name: "cache.html" }).apply(compiler);
265 | new ModifyExistingAsset({ name: "cache-1.html" }).apply(compiler);
266 |
267 | const newStats = await compile(compiler);
268 |
269 | expect(newStats.compilation.emittedAssets.size).toBe(2);
270 |
271 | expect(readAssets(compiler, newStats, /\.html$/i)).toMatchSnapshot(
272 | "assets",
273 | );
274 | expect(getWarnings(newStats)).toMatchSnapshot("errors");
275 | expect(getErrors(newStats)).toMatchSnapshot("warnings");
276 | });
277 |
278 | it('should work and do not use memory cache when the "cache" option is "false"', async () => {
279 | const testHtmlId = false;
280 | const compiler = getCompiler(testHtmlId, {
281 | cache: false,
282 | entry: {
283 | foo: path.resolve(__dirname, "./fixtures/cache.js"),
284 | },
285 | });
286 |
287 | new HtmlMinimizerPlugin().apply(compiler);
288 |
289 | const stats = await compile(compiler);
290 |
291 | expect(stats.compilation.emittedAssets.size).toBe(6);
292 |
293 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("result");
294 | expect(getErrors(stats)).toMatchSnapshot("errors");
295 | expect(getWarnings(stats)).toMatchSnapshot("warnings");
296 |
297 | const newStats = await compile(compiler);
298 |
299 | expect(newStats.compilation.emittedAssets.size).toBe(6);
300 |
301 | expect(readAssets(compiler, newStats, /\.html$/i)).toMatchSnapshot(
302 | "assets",
303 | );
304 | expect(getWarnings(newStats)).toMatchSnapshot("errors");
305 | expect(getErrors(newStats)).toMatchSnapshot("warnings");
306 | });
307 |
308 | it("should run plugin against assets added later by plugins", async () => {
309 | const testHtmlId = "./simple.html";
310 | const compiler = getCompiler(testHtmlId);
311 |
312 | new HtmlMinimizerPlugin().apply(compiler);
313 | new EmitNewAsset({ name: "newFile.html" }).apply(compiler);
314 |
315 | const stats = await compile(compiler);
316 |
317 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("assets");
318 | expect(getErrors(stats)).toMatchSnapshot("errors");
319 | expect(getWarnings(stats)).toMatchSnapshot("warnings");
320 | });
321 | });
322 |
--------------------------------------------------------------------------------
/test/__snapshots__/minify-option.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
2 |
3 | exports[`"minify" option should emit error: errors 1`] = `
4 | [
5 | "Error: simple.html from Html Minimizer plugin
6 | simple.html from Html Minimizer plugin",
7 | "Error: simple.html from Html Minimizer plugin
8 | simple.html from Html Minimizer plugin",
9 | "Error: simple.html from Html Minimizer plugin
10 | simple.html from Html Minimizer plugin",
11 | ]
12 | `;
13 |
14 | exports[`"minify" option should emit error: warnings 1`] = `[]`;
15 |
16 | exports[`"minify" option should emit errors and warnings without code: errors 1`] = `
17 | [
18 | "Error: simple.html from Html Minimizer plugin
19 | simple.html from Html Minimizer plugin",
20 | "Error: simple.html from Html Minimizer plugin
21 | simple.html from Html Minimizer plugin",
22 | "Error: simple.html from Html Minimizer plugin
23 | simple.html from Html Minimizer plugin",
24 | ]
25 | `;
26 |
27 | exports[`"minify" option should emit errors and warnings without code: warnings 1`] = `
28 | [
29 | "Warning: object error",
30 | "Warning: string error",
31 | "Warning: test error",
32 | ]
33 | `;
34 |
35 | exports[`"minify" option should minimize code and emit warning: assets 1`] = `
36 | {
37 | "simple.html": " Document My First Heading
My first paragraph.
An Unordered HTML List
An Ordered HTML List
- Coffee
- Tea
- Milk
",
38 | }
39 | `;
40 |
41 | exports[`"minify" option should minimize code and emit warning: errors 1`] = `[]`;
42 |
43 | exports[`"minify" option should minimize code and emit warning: warnings 1`] = `
44 | [
45 | "Warning: object warning",
46 | "Warning: string warning",
47 | "Warning: test warning",
48 | ]
49 | `;
50 |
51 | exports[`"minify" option should work if minify function return object: assets 1`] = `
52 | {
53 | "simple.html": "simple.html - Document My First Heading
My first paragraph.
An Unordered HTML List
An Ordered HTML List
- Coffee
- Tea
- Milk
:Second function: text from option
",
54 | }
55 | `;
56 |
57 | exports[`"minify" option should work if minify function return object: errors 1`] = `[]`;
58 |
59 | exports[`"minify" option should work if minify function return object: warnings 1`] = `[]`;
60 |
61 | exports[`"minify" option should work if minify is array && minimizerOptions is array: assets 1`] = `
62 | {
63 | "simple.html": "simple.html - Document My First Heading
My first paragraph.
An Unordered HTML List
An Ordered HTML List
- Coffee
- Tea
- Milk
:Second function: text from option
",
64 | }
65 | `;
66 |
67 | exports[`"minify" option should work if minify is array && minimizerOptions is array: errors 1`] = `[]`;
68 |
69 | exports[`"minify" option should work if minify is array && minimizerOptions is array: warnings 1`] = `[]`;
70 |
71 | exports[`"minify" option should work if minify is array && minimizerOptions is object: assets 1`] = `
72 | {
73 | "simple.html": "simple.html - Document My First Heading
My first paragraph.
An Unordered HTML List
An Ordered HTML List
- Coffee
- Tea
- Milk
Second function: true
Third function: true
",
74 | }
75 | `;
76 |
77 | exports[`"minify" option should work if minify is array && minimizerOptions is object: errors 1`] = `[]`;
78 |
79 | exports[`"minify" option should work if minify is array && minimizerOptions is object: warnings 1`] = `[]`;
80 |
81 | exports[`"minify" option should work minify function: assets 1`] = `
82 | {
83 | "simple.html": "DocumentMy First Heading
My first paragraph.
An Unordered HTML List
An Ordered HTML List
- Coffee
- Tea
- Milk
",
84 | }
85 | `;
86 |
87 | exports[`"minify" option should work minify function: errors 1`] = `[]`;
88 |
89 | exports[`"minify" option should work minify function: warnings 1`] = `[]`;
90 |
91 | exports[`"minify" option should work with 'swcMinify' and options: assets 1`] = `
92 | {
93 | "simple.html": "DocumentMy First Heading
My first paragraph.
An Unordered HTML List
An Ordered HTML List
- Coffee
- Tea
- Milk
",
94 | }
95 | `;
96 |
97 | exports[`"minify" option should work with 'swcMinify' and options: errors 1`] = `[]`;
98 |
99 | exports[`"minify" option should work with 'swcMinify' and options: warnings 1`] = `[]`;
100 |
101 | exports[`"minify" option should work with 'swcMinify' and throw errors: assets 1`] = `
102 | {
103 | "broken-html-syntax.html": "Text < img src="image.png" >
104 | Text <
105 | Text >
106 |
107 | boohay
108 | <<<<>foo
109 | >><",
110 | }
111 | `;
112 |
113 | exports[`"minify" option should work with 'swcMinify' and throw errors: errors 1`] = `
114 | [
115 | "Error: broken-html-syntax.html from Html Minimizer plugin
116 | broken-html-syntax.html from Html Minimizer plugin",
117 | "Error: broken-html-syntax.html from Html Minimizer plugin
118 | broken-html-syntax.html from Html Minimizer plugin",
119 | "Error: broken-html-syntax.html from Html Minimizer plugin
120 | broken-html-syntax.html from Html Minimizer plugin",
121 | "Error: broken-html-syntax.html from Html Minimizer plugin
122 | broken-html-syntax.html from Html Minimizer plugin",
123 | "Error: broken-html-syntax.html from Html Minimizer plugin
124 | broken-html-syntax.html from Html Minimizer plugin",
125 | "Error: broken-html-syntax.html from Html Minimizer plugin
126 | broken-html-syntax.html from Html Minimizer plugin",
127 | "Error: broken-html-syntax.html from Html Minimizer plugin
128 | broken-html-syntax.html from Html Minimizer plugin",
129 | "Error: broken-html-syntax.html from Html Minimizer plugin
130 | broken-html-syntax.html from Html Minimizer plugin",
131 | "Error: broken-html-syntax.html from Html Minimizer plugin
132 | broken-html-syntax.html from Html Minimizer plugin",
133 | "Error: broken-html-syntax.html from Html Minimizer plugin
134 | broken-html-syntax.html from Html Minimizer plugin",
135 | "Error: broken-html-syntax.html from Html Minimizer plugin
136 | broken-html-syntax.html from Html Minimizer plugin",
137 | "Error: broken-html-syntax.html from Html Minimizer plugin
138 | broken-html-syntax.html from Html Minimizer plugin",
139 | "Error: broken-html-syntax.html from Html Minimizer plugin
140 | broken-html-syntax.html from Html Minimizer plugin",
141 | "Error: broken-html-syntax.html from Html Minimizer plugin
142 | broken-html-syntax.html from Html Minimizer plugin",
143 | "Error: broken-html-syntax.html from Html Minimizer plugin
144 | broken-html-syntax.html from Html Minimizer plugin",
145 | "Error: broken-html-syntax.html from Html Minimizer plugin
146 | broken-html-syntax.html from Html Minimizer plugin",
147 | ]
148 | `;
149 |
150 | exports[`"minify" option should work with 'swcMinify' and throw errors: warnings 1`] = `[]`;
151 |
152 | exports[`"minify" option should work with 'swcMinify': assets 1`] = `
153 | {
154 | "simple.html": "DocumentMy First Heading
155 | My first paragraph.
156 | An Unordered HTML List
157 |
158 |
159 | - Coffee
160 | - Tea
161 | - Milk
162 |
163 |
164 | An Ordered HTML List
165 |
166 |
167 | - Coffee
168 | - Tea
169 | - Milk
170 |
",
171 | }
172 | `;
173 |
174 | exports[`"minify" option should work with 'swcMinify': errors 1`] = `[]`;
175 |
176 | exports[`"minify" option should work with 'swcMinify': warnings 1`] = `[]`;
177 |
178 | exports[`"minify" option should work with 'swcMinifyFragment' and options: assets 1`] = `
179 | {
180 | "template.html": " test ",
181 | }
182 | `;
183 |
184 | exports[`"minify" option should work with 'swcMinifyFragment' and options: errors 1`] = `[]`;
185 |
186 | exports[`"minify" option should work with 'swcMinifyFragment' and options: warnings 1`] = `[]`;
187 |
188 | exports[`"minify" option should work with 'swcMinifyFragment' and throw errors: assets 1`] = `
189 | {
190 | "broken-html-syntax.html": "Text < img src="image.png" >
191 | Text <
192 | Text >
193 |
194 | boohay
195 | <<<<>foo
196 | >><",
197 | }
198 | `;
199 |
200 | exports[`"minify" option should work with 'swcMinifyFragment' and throw errors: errors 1`] = `
201 | [
202 | "Error: broken-html-syntax.html from Html Minimizer plugin
203 | broken-html-syntax.html from Html Minimizer plugin",
204 | "Error: broken-html-syntax.html from Html Minimizer plugin
205 | broken-html-syntax.html from Html Minimizer plugin",
206 | "Error: broken-html-syntax.html from Html Minimizer plugin
207 | broken-html-syntax.html from Html Minimizer plugin",
208 | "Error: broken-html-syntax.html from Html Minimizer plugin
209 | broken-html-syntax.html from Html Minimizer plugin",
210 | "Error: broken-html-syntax.html from Html Minimizer plugin
211 | broken-html-syntax.html from Html Minimizer plugin",
212 | "Error: broken-html-syntax.html from Html Minimizer plugin
213 | broken-html-syntax.html from Html Minimizer plugin",
214 | "Error: broken-html-syntax.html from Html Minimizer plugin
215 | broken-html-syntax.html from Html Minimizer plugin",
216 | "Error: broken-html-syntax.html from Html Minimizer plugin
217 | broken-html-syntax.html from Html Minimizer plugin",
218 | "Error: broken-html-syntax.html from Html Minimizer plugin
219 | broken-html-syntax.html from Html Minimizer plugin",
220 | "Error: broken-html-syntax.html from Html Minimizer plugin
221 | broken-html-syntax.html from Html Minimizer plugin",
222 | "Error: broken-html-syntax.html from Html Minimizer plugin
223 | broken-html-syntax.html from Html Minimizer plugin",
224 | "Error: broken-html-syntax.html from Html Minimizer plugin
225 | broken-html-syntax.html from Html Minimizer plugin",
226 | "Error: broken-html-syntax.html from Html Minimizer plugin
227 | broken-html-syntax.html from Html Minimizer plugin",
228 | "Error: broken-html-syntax.html from Html Minimizer plugin
229 | broken-html-syntax.html from Html Minimizer plugin",
230 | ]
231 | `;
232 |
233 | exports[`"minify" option should work with 'swcMinifyFragment' and throw errors: warnings 1`] = `[]`;
234 |
235 | exports[`"minify" option should work with 'swcMinifyFragment': assets 1`] = `
236 | {
237 | "template.html": "
238 |
239 | - test
240 | - test
241 | - test
242 |
243 |
244 | test
245 | ",
246 | }
247 | `;
248 |
249 | exports[`"minify" option should work with 'swcMinifyFragment': errors 1`] = `[]`;
250 |
251 | exports[`"minify" option should work with 'swcMinifyFragment': warnings 1`] = `[]`;
252 |
--------------------------------------------------------------------------------
/test/minify-option.test.js:
--------------------------------------------------------------------------------
1 | import HtmlMinimizerPlugin from "../src/index";
2 |
3 | import {
4 | compile,
5 | getCompiler,
6 | getErrors,
7 | getWarnings,
8 | readAssets,
9 | } from "./helpers";
10 |
11 | describe('"minify" option', () => {
12 | it("should work minify function", async () => {
13 | const testHtmlId = "./simple.html";
14 | const compiler = getCompiler(testHtmlId);
15 |
16 | new HtmlMinimizerPlugin({
17 | minimizerOptions: {
18 | collapseWhitespace: true,
19 | },
20 | minify: (data, minimizerOptions) => {
21 | const htmlMinifier = require("html-minifier-terser");
22 |
23 | const [[, input]] = Object.entries(data);
24 |
25 | return htmlMinifier.minify(input, minimizerOptions);
26 | },
27 | }).apply(compiler);
28 |
29 | const stats = await compile(compiler);
30 |
31 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("assets");
32 | expect(getErrors(stats)).toMatchSnapshot("errors");
33 | expect(getWarnings(stats)).toMatchSnapshot("warnings");
34 | });
35 |
36 | it("should work if minify is array && minimizerOptions is object", async () => {
37 | const testHtmlId = "./simple.html";
38 | const compiler = getCompiler(testHtmlId);
39 |
40 | new HtmlMinimizerPlugin({
41 | minimizerOptions: {
42 | collapseWhitespace: true,
43 | },
44 | minify: [
45 | HtmlMinimizerPlugin.htmlMinifierTerser,
46 | async (data, minimizerOptions) => {
47 | const [[, input]] = Object.entries(data);
48 |
49 | return `${input}Second function: ${minimizerOptions.collapseWhitespace}
`;
50 | },
51 | async (data, minimizerOptions) => {
52 | const [[name, input]] = Object.entries(data);
53 | return `${name} - ${input}Third function: ${minimizerOptions.collapseWhitespace}
`;
54 | },
55 | ],
56 | }).apply(compiler);
57 |
58 | const stats = await compile(compiler);
59 |
60 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("assets");
61 | expect(getErrors(stats)).toMatchSnapshot("errors");
62 | expect(getWarnings(stats)).toMatchSnapshot("warnings");
63 | });
64 |
65 | it("should work if minify is array && minimizerOptions is array", async () => {
66 | const testHtmlId = "./simple.html";
67 | const compiler = getCompiler(testHtmlId);
68 |
69 | new HtmlMinimizerPlugin({
70 | minimizerOptions: [
71 | {
72 | collapseWhitespace: true,
73 | },
74 | {
75 | test: "text from option",
76 | },
77 | ],
78 | minify: [
79 | HtmlMinimizerPlugin.htmlMinifierTerser,
80 | async (data, minimizerOptions) => {
81 | const [[name, input]] = Object.entries(data);
82 | return `${name} - ${input}:Second function: ${minimizerOptions.test}
`;
83 | },
84 | ],
85 | }).apply(compiler);
86 |
87 | const stats = await compile(compiler);
88 |
89 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("assets");
90 | expect(getErrors(stats)).toMatchSnapshot("errors");
91 | expect(getWarnings(stats)).toMatchSnapshot("warnings");
92 | });
93 |
94 | it("should work if minify function return object", async () => {
95 | const testHtmlId = "./simple.html";
96 | const compiler = getCompiler(testHtmlId);
97 |
98 | new HtmlMinimizerPlugin({
99 | minimizerOptions: [
100 | {
101 | collapseWhitespace: true,
102 | },
103 | {
104 | test: "text from option",
105 | },
106 | ],
107 | minify: [
108 | HtmlMinimizerPlugin.htmlMinifierTerser,
109 | async (data, minimizerOptions) => {
110 | const [[name, input]] = Object.entries(data);
111 | return {
112 | code: `${name} - ${input}:Second function: ${minimizerOptions.test}
`,
113 | };
114 | },
115 | ],
116 | }).apply(compiler);
117 |
118 | const stats = await compile(compiler);
119 |
120 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("assets");
121 | expect(getErrors(stats)).toMatchSnapshot("errors");
122 | expect(getWarnings(stats)).toMatchSnapshot("warnings");
123 | });
124 |
125 | it("should minimize code and emit warning", async () => {
126 | const testHtmlId = "./simple.html";
127 | const compiler = getCompiler(testHtmlId);
128 |
129 | new HtmlMinimizerPlugin({
130 | minimizerOptions: [
131 | {
132 | collapseWhitespace: true,
133 | },
134 | ],
135 | minify: [
136 | HtmlMinimizerPlugin.htmlMinifierTerser,
137 | async (data) => {
138 | const [[, input]] = Object.entries(data);
139 | return {
140 | code: input,
141 | warnings: [
142 | "string warning",
143 | new Error("test warning"),
144 | {
145 | message: "object warning",
146 | },
147 | ],
148 | };
149 | },
150 | ],
151 | }).apply(compiler);
152 |
153 | const stats = await compile(compiler);
154 |
155 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("assets");
156 | expect(getErrors(stats)).toMatchSnapshot("errors");
157 | expect(getWarnings(stats)).toMatchSnapshot("warnings");
158 | });
159 |
160 | it("should emit error", async () => {
161 | const testHtmlId = "./simple.html";
162 | const compiler = getCompiler(testHtmlId);
163 |
164 | new HtmlMinimizerPlugin({
165 | minimizerOptions: [
166 | {
167 | collapseWhitespace: true,
168 | },
169 | ],
170 | minify: [
171 | HtmlMinimizerPlugin.htmlMinifierTerser,
172 | async (data) => {
173 | const [[, input]] = Object.entries(data);
174 | return {
175 | code: input,
176 | errors: [
177 | "string error",
178 | new Error("test error"),
179 | {
180 | message: "object error",
181 | },
182 | ],
183 | };
184 | },
185 | ],
186 | }).apply(compiler);
187 |
188 | const stats = await compile(compiler);
189 |
190 | expect(getErrors(stats)).toMatchSnapshot("errors");
191 | expect(getWarnings(stats)).toMatchSnapshot("warnings");
192 | });
193 |
194 | it("should emit errors and warnings without code", async () => {
195 | const testHtmlId = "./simple.html";
196 | const compiler = getCompiler(testHtmlId);
197 |
198 | new HtmlMinimizerPlugin({
199 | minify: [
200 | async (_data) => ({
201 | warnings: [
202 | "string error",
203 | new Error("test error"),
204 | {
205 | message: "object error",
206 | },
207 | ],
208 | errors: [
209 | "string error",
210 | new Error("test error"),
211 | {
212 | message: "object error",
213 | },
214 | ],
215 | }),
216 | ],
217 | }).apply(compiler);
218 |
219 | const stats = await compile(compiler);
220 |
221 | expect(getErrors(stats)).toMatchSnapshot("errors");
222 | expect(getWarnings(stats)).toMatchSnapshot("warnings");
223 | });
224 |
225 | it("should work with 'swcMinify'", async () => {
226 | const testHtmlId = "./simple.html";
227 | const compiler = getCompiler(testHtmlId);
228 |
229 | new HtmlMinimizerPlugin({
230 | minify: HtmlMinimizerPlugin.swcMinify,
231 | }).apply(compiler);
232 |
233 | const stats = await compile(compiler);
234 |
235 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("assets");
236 | expect(getErrors(stats)).toMatchSnapshot("errors");
237 | expect(getWarnings(stats)).toMatchSnapshot("warnings");
238 | });
239 |
240 | it("should work with 'swcMinify' and throw errors", async () => {
241 | const testHtmlId = "./broken-html-syntax.html";
242 | const compiler = getCompiler(testHtmlId);
243 |
244 | new HtmlMinimizerPlugin({
245 | minify: HtmlMinimizerPlugin.swcMinify,
246 | }).apply(compiler);
247 |
248 | const stats = await compile(compiler);
249 |
250 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("assets");
251 | expect(getErrors(stats)).toMatchSnapshot("errors");
252 | expect(getWarnings(stats)).toMatchSnapshot("warnings");
253 | });
254 |
255 | it("should work with 'swcMinify' and options", async () => {
256 | const testHtmlId = "./simple.html";
257 | const compiler = getCompiler(testHtmlId);
258 |
259 | new HtmlMinimizerPlugin({
260 | minimizerOptions: {
261 | collapseWhitespaces: "advanced-conservative",
262 | },
263 | minify: HtmlMinimizerPlugin.swcMinify,
264 | }).apply(compiler);
265 |
266 | const stats = await compile(compiler);
267 |
268 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("assets");
269 | expect(getErrors(stats)).toMatchSnapshot("errors");
270 | expect(getWarnings(stats)).toMatchSnapshot("warnings");
271 | });
272 |
273 | it("should work with 'swcMinifyFragment'", async () => {
274 | const testHtmlId = "./template.html";
275 | const compiler = getCompiler(testHtmlId);
276 |
277 | new HtmlMinimizerPlugin({
278 | minify: HtmlMinimizerPlugin.swcMinifyFragment,
279 | }).apply(compiler);
280 |
281 | const stats = await compile(compiler);
282 |
283 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("assets");
284 | expect(getErrors(stats)).toMatchSnapshot("errors");
285 | expect(getWarnings(stats)).toMatchSnapshot("warnings");
286 | });
287 |
288 | it("should work with 'swcMinifyFragment' and throw errors", async () => {
289 | const testHtmlId = "./broken-html-syntax.html";
290 | const compiler = getCompiler(testHtmlId);
291 |
292 | new HtmlMinimizerPlugin({
293 | minify: HtmlMinimizerPlugin.swcMinifyFragment,
294 | }).apply(compiler);
295 |
296 | const stats = await compile(compiler);
297 |
298 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("assets");
299 | expect(getErrors(stats)).toMatchSnapshot("errors");
300 | expect(getWarnings(stats)).toMatchSnapshot("warnings");
301 | });
302 |
303 | it("should work with 'swcMinifyFragment' and options", async () => {
304 | const testHtmlId = "./template.html";
305 | const compiler = getCompiler(testHtmlId);
306 |
307 | new HtmlMinimizerPlugin({
308 | minimizerOptions: {
309 | collapseWhitespaces: "advanced-conservative",
310 | },
311 | minify: HtmlMinimizerPlugin.swcMinifyFragment,
312 | }).apply(compiler);
313 |
314 | const stats = await compile(compiler);
315 |
316 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("assets");
317 | expect(getErrors(stats)).toMatchSnapshot("errors");
318 | expect(getWarnings(stats)).toMatchSnapshot("warnings");
319 | });
320 |
321 | const isMacOs = process.platform === "darwin";
322 |
323 | (isMacOs ? it.skip : it)("should work with '@minify-html/node'", async () => {
324 | const testHtmlId = "./simple.html";
325 | const compiler = getCompiler(testHtmlId);
326 |
327 | new HtmlMinimizerPlugin({
328 | minify: HtmlMinimizerPlugin.minifyHtmlNode,
329 | }).apply(compiler);
330 |
331 | await compile(compiler);
332 |
333 | // expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("assets");
334 | // expect(getErrors(stats)).toMatchSnapshot("errors");
335 | // expect(getWarnings(stats)).toMatchSnapshot("warnings");
336 | });
337 |
338 | (isMacOs ? it.skip : it)(
339 | "should work with '@minify-html/node' and broken syntax",
340 | async () => {
341 | const testHtmlId = "./broken-html-syntax.html";
342 | const compiler = getCompiler(testHtmlId);
343 |
344 | new HtmlMinimizerPlugin({
345 | minify: HtmlMinimizerPlugin.minifyHtmlNode,
346 | }).apply(compiler);
347 |
348 | await compile(compiler);
349 |
350 | // expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("assets");
351 | // expect(getErrors(stats)).toMatchSnapshot("errors");
352 | // expect(getWarnings(stats)).toMatchSnapshot("warnings");
353 | },
354 | );
355 |
356 | (isMacOs ? it.skip : it)(
357 | "should work with '@minify-html/node' and options",
358 | async () => {
359 | const testHtmlId = "./simple.html";
360 | const compiler = getCompiler(testHtmlId);
361 |
362 | new HtmlMinimizerPlugin({
363 | minimizerOptions: {
364 | do_not_minify_doctype: true,
365 | },
366 | minify: HtmlMinimizerPlugin.minifyHtmlNode,
367 | }).apply(compiler);
368 |
369 | await compile(compiler);
370 |
371 | // expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("assets");
372 | // expect(getErrors(stats)).toMatchSnapshot("errors");
373 | // expect(getWarnings(stats)).toMatchSnapshot("warnings");
374 | },
375 | );
376 | });
377 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | const os = require("node:os");
2 |
3 | const { validate } = require("schema-utils");
4 |
5 | const { minify: internalMinify } = require("./minify");
6 | const schema = require("./options.json");
7 | const {
8 | htmlMinifierTerser,
9 | memoize,
10 | minifyHtmlNode,
11 | swcMinify,
12 | swcMinifyFragment,
13 | throttleAll,
14 | } = require("./utils");
15 |
16 | /** @typedef {import("schema-utils/declarations/validate").Schema} Schema */
17 | /** @typedef {import("webpack").Compiler} Compiler */
18 | /** @typedef {import("webpack").Compilation} Compilation */
19 | /** @typedef {import("webpack").WebpackError} WebpackError */
20 | /** @typedef {import("webpack").Asset} Asset */
21 | /** @typedef {import("jest-worker").Worker} JestWorker */
22 |
23 | /** @typedef {RegExp | string} Rule */
24 | /** @typedef {Rule[] | Rule} Rules */
25 | // eslint-disable-next-line jsdoc/no-restricted-syntax
26 | /** @typedef {any} EXPECTED_ANY */
27 |
28 | /** @typedef {Error & { plugin?: string, text?: string, source?: string } | string} Warning */
29 |
30 | /**
31 | * @typedef {object} WarningObject
32 | * @property {string} message The warning message
33 | * @property {string=} plugin The plugin name
34 | * @property {string=} text The text content
35 | * @property {number=} line The line number
36 | * @property {number=} column The column number
37 | */
38 |
39 | /**
40 | * @typedef {object} ErrorObject
41 | * @property {string} message The error message
42 | * @property {number=} line The line number
43 | * @property {number=} column The column number
44 | * @property {string=} stack The error stack trace
45 | */
46 |
47 | /**
48 | * @typedef {object} MinimizedResultObj
49 | * @property {string} code The minimized code
50 | * @property {Array=} errors Array of errors
51 | * @property {Array=} warnings Array of warnings
52 | */
53 |
54 | /**
55 | * @typedef {MinimizedResultObj | string} MinimizedResult
56 | */
57 |
58 | /**
59 | * @typedef {{ [file: string]: string }} Input
60 | */
61 |
62 | /**
63 | * @typedef {{ [key: string]: EXPECTED_ANY }} CustomOptions
64 | */
65 |
66 | /**
67 | * @template T
68 | * @typedef {T extends infer U ? U : CustomOptions} InferDefaultType
69 | */
70 |
71 | /**
72 | * @template T
73 | * @typedef {T extends any[] ? { [P in keyof T]?: InferDefaultType } : InferDefaultType} MinimizerOptions
74 | */
75 |
76 | /**
77 | * @template T
78 | * @callback BasicMinimizerImplementation
79 | * @param {Input} input
80 | * @param {InferDefaultType} minifyOptions
81 | * @returns {Promise | MinimizedResult}
82 | */
83 |
84 | /**
85 | * @typedef {object} MinimizeFunctionHelpers
86 | * @property {() => boolean | undefined=} supportsWorkerThreads - Function to check if worker threads are supported
87 | */
88 |
89 | /**
90 | * @template T
91 | * @typedef {T extends any[] ? { [P in keyof T]: BasicMinimizerImplementation & MinimizeFunctionHelpers; } : BasicMinimizerImplementation & MinimizeFunctionHelpers} MinimizerImplementation
92 | */
93 |
94 | /**
95 | * @template T
96 | * @typedef {object} InternalOptions
97 | * @property {string} name The name of the minimizer
98 | * @property {string} input The input content
99 | * @property {{ implementation: MinimizerImplementation, options: MinimizerOptions }} minimizer The minimizer configuration
100 | */
101 |
102 | /**
103 | * @typedef InternalResult
104 | * @property {Array<{ code: string }>} outputs Array of output objects
105 | * @property {Array} warnings Array of warnings
106 | * @property {Array} errors Array of errors
107 | */
108 |
109 | /**
110 | * @template T
111 | * @typedef {JestWorker & { transform: (options: string) => Promise, minify: (options: InternalOptions) => Promise }} MinimizerWorker
112 | */
113 |
114 | /**
115 | * @typedef {undefined | boolean | number} Parallel
116 | */
117 |
118 | /**
119 | * @typedef {object} BasePluginOptions
120 | * @property {Rule=} test Test rule for files to process
121 | * @property {Rule=} include Include rule for files to process
122 | * @property {Rule=} exclude Exclude rule for files to process
123 | * @property {Parallel=} parallel Parallel processing configuration
124 | */
125 |
126 | /**
127 | * @template T
128 | * @typedef {BasePluginOptions & { minimizer: { implementation: MinimizerImplementation, options: MinimizerOptions } }} InternalPluginOptions
129 | */
130 |
131 | /**
132 | * @template T
133 | * @typedef {T extends import("html-minifier-terser").Options ? { minify?: MinimizerImplementation | undefined, minimizerOptions?: MinimizerOptions | undefined } : { minify: MinimizerImplementation, minimizerOptions?: MinimizerOptions | undefined }} DefinedDefaultMinimizerAndOptions
134 | */
135 |
136 | const getSerializeJavascript = memoize(() => require("serialize-javascript"));
137 |
138 | /**
139 | * @template [T=import("html-minifier-terser").Options]
140 | * @typedef {BasePluginOptions & DefinedDefaultMinimizerAndOptions} PluginOptions
141 | */
142 |
143 | /**
144 | * @template {PluginOptions} [T=PluginOptions]
145 | */
146 | class HtmlMinimizerPlugin {
147 | /**
148 | * @param {T=} options Plugin options
149 | */
150 | constructor(options) {
151 | validate(/** @type {Schema} */ (schema), options || {}, {
152 | name: "Html Minimizer Plugin",
153 | baseDataPath: "options",
154 | });
155 |
156 | const {
157 | minify = htmlMinifierTerser,
158 | minimizerOptions = {},
159 | parallel = true,
160 | test = /\.html(\?.*)?$/i,
161 | include,
162 | exclude,
163 | } = /** @type {T} */ (options || {});
164 |
165 | /**
166 | * @private
167 | * @type {InternalPluginOptions}
168 | */
169 | this.options = {
170 | test,
171 | parallel,
172 | include,
173 | exclude,
174 | minimizer: {
175 | implementation: /** @type {MinimizerImplementation} */ (minify),
176 | options: /** @type {MinimizerOptions} */ (minimizerOptions),
177 | },
178 | };
179 | }
180 |
181 | /**
182 | * @private
183 | * @param {EXPECTED_ANY} warning The warning to build
184 | * @param {string} file The file path
185 | * @returns {Error & { hideStack?: boolean, file?: string }} The built warning
186 | */
187 | static buildWarning(warning, file) {
188 | /**
189 | * @type {Error & { hideStack?: true, file?: string }}
190 | */
191 | const builtWarning = new Error(
192 | warning instanceof Error
193 | ? warning.message
194 | : typeof warning.message !== "undefined"
195 | ? warning.message
196 | : warning.toString(),
197 | );
198 |
199 | builtWarning.name = "Warning";
200 | builtWarning.hideStack = true;
201 | builtWarning.file = file;
202 |
203 | return builtWarning;
204 | }
205 |
206 | /**
207 | * @private
208 | * @param {EXPECTED_ANY} error The error to build
209 | * @param {string} file The file path
210 | * @returns {Error} The built error
211 | */
212 | static buildError(error, file) {
213 | /**
214 | * @type {Error & { file?: string }}
215 | */
216 | let builtError;
217 |
218 | if (typeof error === "string") {
219 | builtError = new Error(`${file} from Html Minimizer plugin\n${error}`);
220 | builtError.file = file;
221 |
222 | return builtError;
223 | }
224 |
225 | if (error.stack) {
226 | builtError = new Error(
227 | `${file} from Html Minimizer plugin\n${
228 | typeof error.message !== "undefined" ? error.message : ""
229 | }\n${error.stack}`,
230 | );
231 | builtError.file = file;
232 |
233 | return builtError;
234 | }
235 |
236 | builtError = new Error(
237 | `${file} from Html Minimizer plugin\n${error.message}`,
238 | );
239 | builtError.file = file;
240 |
241 | return builtError;
242 | }
243 |
244 | /**
245 | * @private
246 | * @param {Parallel} parallel Parallel configuration
247 | * @returns {number} The number of available cores
248 | */
249 | static getAvailableNumberOfCores(parallel) {
250 | // In some cases cpus() returns undefined
251 | // https://github.com/nodejs/node/issues/19022
252 | /* eslint-disable n/no-unsupported-features/node-builtins */
253 | const cpus =
254 | typeof os.availableParallelism === "function"
255 | ? { length: /** @type {number} */ (os.availableParallelism()) }
256 | : os.cpus() || { length: 1 };
257 | /* eslint-enable n/no-unsupported-features/node-builtins */
258 |
259 | return parallel === true || typeof parallel === "undefined"
260 | ? cpus.length - 1
261 | : Math.min(parallel || 0, cpus.length - 1);
262 | }
263 |
264 | /**
265 | * @private
266 | * @template T
267 | * @param {BasicMinimizerImplementation & MinimizeFunctionHelpers} implementation The minimizer implementation
268 | * @returns {boolean} Whether worker threads are supported
269 | */
270 | static isSupportsWorkerThreads(implementation) {
271 | return typeof implementation.supportsWorkerThreads !== "undefined"
272 | ? implementation.supportsWorkerThreads() !== false
273 | : true;
274 | }
275 |
276 | /**
277 | * @private
278 | * @param {Compiler} compiler The webpack compiler
279 | * @param {Compilation} compilation The webpack compilation
280 | * @param {Record} assets The assets to optimize
281 | * @param {{availableNumberOfCores: number}} optimizeOptions Optimization options
282 | * @returns {Promise} Promise that resolves when optimization is complete
283 | */
284 | async optimize(compiler, compilation, assets, optimizeOptions) {
285 | const cache = compilation.getCache("HtmlMinimizerWebpackPlugin");
286 | let numberOfAssets = 0;
287 | const assetsForMinify = await Promise.all(
288 | Object.keys(assets)
289 | .filter((name) => {
290 | const { info } = /** @type {Asset} */ (compilation.getAsset(name));
291 |
292 | // Skip double minimize assets from child compilation
293 | if (info.minimized) {
294 | return false;
295 | }
296 |
297 | if (
298 | !compiler.webpack.ModuleFilenameHelpers.matchObject.bind(
299 | undefined,
300 | this.options,
301 | )(name)
302 | ) {
303 | return false;
304 | }
305 |
306 | return true;
307 | })
308 | .map(async (name) => {
309 | const { info, source } = /** @type {Asset} */ (
310 | compilation.getAsset(name)
311 | );
312 |
313 | const eTag = cache.getLazyHashedEtag(source);
314 | const cacheItem = cache.getItemCache(name, eTag);
315 | const output = await cacheItem.getPromise();
316 |
317 | if (!output) {
318 | numberOfAssets += 1;
319 | }
320 |
321 | return { name, info, inputSource: source, output, cacheItem };
322 | }),
323 | );
324 |
325 | if (assetsForMinify.length === 0) {
326 | return;
327 | }
328 |
329 | /** @type {undefined | (() => MinimizerWorker)} */
330 | let getWorker;
331 | /** @type {undefined | MinimizerWorker} */
332 | let initializedWorker;
333 | /** @type {undefined | number} */
334 | let numberOfWorkers;
335 |
336 | if (optimizeOptions.availableNumberOfCores > 0) {
337 | // Do not create unnecessary workers when the number of files is less than the available cores, it saves memory
338 | numberOfWorkers = Math.min(
339 | numberOfAssets,
340 | optimizeOptions.availableNumberOfCores,
341 | );
342 |
343 | getWorker = () => {
344 | if (initializedWorker) {
345 | return initializedWorker;
346 | }
347 |
348 | const { Worker } = require("jest-worker");
349 |
350 | initializedWorker =
351 | /** @type {MinimizerWorker} */
352 | (
353 | new Worker(require.resolve("./minify"), {
354 | numWorkers: numberOfWorkers,
355 | enableWorkerThreads: Array.isArray(
356 | this.options.minimizer.implementation,
357 | )
358 | ? this.options.minimizer.implementation.every((item) =>
359 | HtmlMinimizerPlugin.isSupportsWorkerThreads(item),
360 | )
361 | : HtmlMinimizerPlugin.isSupportsWorkerThreads(
362 | this.options.minimizer.implementation,
363 | ),
364 | })
365 | );
366 |
367 | // https://github.com/facebook/jest/issues/8872#issuecomment-524822081
368 | const workerStdout = initializedWorker.getStdout();
369 |
370 | if (workerStdout) {
371 | workerStdout.on("data", (chunk) => process.stdout.write(chunk));
372 | }
373 |
374 | const workerStderr = initializedWorker.getStderr();
375 |
376 | if (workerStderr) {
377 | workerStderr.on("data", (chunk) => process.stderr.write(chunk));
378 | }
379 |
380 | return initializedWorker;
381 | };
382 | }
383 |
384 | const { RawSource } = compiler.webpack.sources;
385 | const scheduledTasks = [];
386 |
387 | for (const asset of assetsForMinify) {
388 | scheduledTasks.push(async () => {
389 | const { name, inputSource, cacheItem } = asset;
390 | let { output } = asset;
391 | let input;
392 |
393 | const sourceFromInputSource = inputSource.source();
394 |
395 | if (!output) {
396 | input = sourceFromInputSource;
397 |
398 | if (Buffer.isBuffer(input)) {
399 | input = input.toString();
400 | }
401 |
402 | /**
403 | * @type {InternalOptions}
404 | */
405 | const options = {
406 | name,
407 | input,
408 | minimizer: {
409 | implementation: this.options.minimizer.implementation,
410 | options: this.options.minimizer.options,
411 | },
412 | };
413 |
414 | let result;
415 |
416 | try {
417 | result = await (getWorker
418 | ? getWorker().transform(getSerializeJavascript()(options))
419 | : internalMinify(options));
420 | } catch (error) {
421 | compilation.errors.push(
422 | /** @type {WebpackError} */
423 | (HtmlMinimizerPlugin.buildError(error, name)),
424 | );
425 |
426 | return;
427 | }
428 |
429 | output = { warnings: [], errors: [] };
430 |
431 | if (result.outputs.length > 0) {
432 | output.source = new RawSource(
433 | result.outputs[result.outputs.length - 1].code,
434 | );
435 | }
436 |
437 | for (const error of result.errors) {
438 | output.errors.push(HtmlMinimizerPlugin.buildError(error, name));
439 | }
440 |
441 | for (const warning of result.warnings) {
442 | output.warnings.push(
443 | HtmlMinimizerPlugin.buildWarning(warning, name),
444 | );
445 | }
446 |
447 | await cacheItem.storePromise({
448 | source: output.source,
449 | errors: output.errors,
450 | warnings: output.warnings,
451 | });
452 | }
453 |
454 | if (output.warnings && output.warnings.length > 0) {
455 | for (const warning of output.warnings) {
456 | compilation.warnings.push(
457 | /** @type {WebpackError} */
458 | (HtmlMinimizerPlugin.buildWarning(warning, name)),
459 | );
460 | }
461 | }
462 |
463 | if (output.errors && output.errors.length > 0) {
464 | for (const error of output.errors) {
465 | compilation.errors.push(
466 | /** @type {WebpackError} */
467 | (HtmlMinimizerPlugin.buildError(error, name)),
468 | );
469 | }
470 | }
471 |
472 | if (!output.source) {
473 | return;
474 | }
475 |
476 | const newInfo = { minimized: true };
477 |
478 | compilation.updateAsset(name, output.source, newInfo);
479 | });
480 | }
481 |
482 | const limit =
483 | getWorker && numberOfAssets > 0
484 | ? /** @type {number} */ (numberOfWorkers)
485 | : scheduledTasks.length;
486 |
487 | await throttleAll(limit, scheduledTasks);
488 |
489 | if (initializedWorker) {
490 | await initializedWorker.end();
491 | }
492 | }
493 |
494 | /**
495 | * @param {Compiler} compiler The webpack compiler
496 | * @returns {void}
497 | */
498 | apply(compiler) {
499 | const pluginName = this.constructor.name;
500 | const availableNumberOfCores =
501 | HtmlMinimizerPlugin.getAvailableNumberOfCores(this.options.parallel);
502 |
503 | compiler.hooks.compilation.tap(pluginName, (compilation) => {
504 | compilation.hooks.processAssets.tapPromise(
505 | {
506 | name: pluginName,
507 | stage:
508 | compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE,
509 | additionalAssets: true,
510 | },
511 | (assets) =>
512 | this.optimize(compiler, compilation, assets, {
513 | availableNumberOfCores,
514 | }),
515 | );
516 |
517 | compilation.hooks.statsPrinter.tap(pluginName, (stats) => {
518 | stats.hooks.print
519 | .for("asset.info.minimized")
520 | .tap(
521 | "html-minimizer-webpack-plugin",
522 | (minimized, { green, formatFlag }) =>
523 | minimized
524 | ? /** @type {(text: string) => string} */ (green)(
525 | /** @type {(flag: string) => string} */ (formatFlag)(
526 | "minimized",
527 | ),
528 | )
529 | : "",
530 | );
531 | });
532 | });
533 | }
534 | }
535 |
536 | HtmlMinimizerPlugin.htmlMinifierTerser = htmlMinifierTerser;
537 | HtmlMinimizerPlugin.swcMinify = swcMinify;
538 | HtmlMinimizerPlugin.swcMinifyFragment = swcMinifyFragment;
539 | HtmlMinimizerPlugin.minifyHtmlNode = minifyHtmlNode;
540 |
541 | module.exports = HtmlMinimizerPlugin;
542 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
6 |
7 | [![npm][npm]][npm-url]
8 | [![node][node]][node-url]
9 | [![tests][tests]][tests-url]
10 | [![cover][cover]][cover-url]
11 | [![discussion][discussion]][discussion-url]
12 | [![size][size]][size-url]
13 |
14 | # html-minimizer-webpack-plugin
15 |
16 | This plugin can use 3 tools to optimize and minify your HTML:
17 |
18 | - [`swc`](https://github.com/swc-project/swc) - very fast Rust-based platform for the Web.
19 | - [`html-minifier-terser`](https://github.com/terser/html-minifier-terser) (by default) - JavaScript-based HTML minifier.
20 | - [`@minify-html/node`](https://github.com/wilsonzlin/minify-html) - A Rust HTML minifier meticulously optimised for speed and effectiveness, with bindings for other languages.
21 |
22 | This plugin integrates seamlessly into your Webpack build pipeline to reduce HTML size and improve loading performance.
23 |
24 | ## Getting Started
25 |
26 | To begin, you'll need to install `html-minimizer-webpack-plugin`:
27 |
28 | ```console
29 | npm install html-minimizer-webpack-plugin --save-dev
30 | ```
31 |
32 | or
33 |
34 | ```console
35 | yarn add -D html-minimizer-webpack-plugin
36 | ```
37 |
38 | or
39 |
40 | ```console
41 | pnpm add -D html-minimizer-webpack-plugin
42 | ```
43 |
44 | **Additional step**: If you want to use `@swc/html` you need to install it:
45 |
46 | ```console
47 | npm install @swc/html --save-dev
48 | ```
49 |
50 | or
51 |
52 | ```console
53 | yarn add -D @swc/html
54 | ```
55 |
56 | or
57 |
58 | ```console
59 | pnpm add -D @swc/html
60 | ```
61 |
62 | **Additional step**: If you want to use `@minify-html/node` you need to install it:
63 |
64 | ```console
65 | npm install @minify-html/node --save-dev
66 | ```
67 |
68 | or
69 |
70 | ```console
71 | yarn add -D @minify-html/node
72 | ```
73 |
74 | or
75 |
76 | ```console
77 | pnpm add -D @minify-html/node
78 | ```
79 |
80 | Then add the plugin to your `webpack` configuration. For example:
81 |
82 | **webpack.config.js**
83 |
84 | ```js
85 | const CopyPlugin = require("copy-webpack-plugin");
86 | const HtmlMinimizerPlugin = require("html-minimizer-webpack-plugin");
87 |
88 | module.exports = {
89 | module: {
90 | rules: [
91 | {
92 | test: /\.html$/i,
93 | type: "asset/resource",
94 | },
95 | ],
96 | },
97 | plugins: [
98 | new CopyPlugin({
99 | patterns: [
100 | {
101 | context: path.resolve(__dirname, "dist"),
102 | from: "./src/*.html",
103 | },
104 | ],
105 | }),
106 | ],
107 | optimization: {
108 | minimize: true,
109 | minimizer: [
110 | // For webpack@5 you can use the `...` syntax to extend existing minimizers (i.e. `terser-webpack-plugin`), uncomment the next line
111 | // `...`
112 |
113 | // For `html-minifier-terser`:
114 | //
115 | new HtmlMinimizerPlugin(),
116 |
117 | // For `@swc/html`:
118 | //
119 | // HTML documents - HTML documents with `Doctype` and `/``/`` tags
120 | //
121 | // Options - https://github.com/swc-project/bindings/blob/main/packages/html/index.ts#L5
122 | //
123 | // new HtmlMinimizerPlugin({
124 | // minify: HtmlMinimizerPlugin.swcMinify,
125 | // minimizerOptions: {}
126 | // })
127 | //
128 | //
129 | // HTML fragments - HTML fragments, i.e. HTML files without doctype or used in `` tags or HTML parts which injects into another HTML parts
130 | //
131 | // Options - https://github.com/swc-project/bindings/blob/main/packages/html/index.ts#L38
132 | //
133 | // new HtmlMinimizerPlugin({
134 | // minify: HtmlMinimizerPlugin.swcMinifyFragment,
135 | // minimizerOptions: {}
136 | // })
137 | ],
138 | },
139 | };
140 | ```
141 |
142 | > [!NOTE]
143 | >
144 | > HTML will only be minimized in production mode by default. To enable minification in development, explicitly set `optimization.minimize: true`.
145 |
146 | Finally, run `webpack` using the method you normally use (e.g., via CLI or an npm script).
147 |
148 | > [!NOTE]
149 | >
150 | > Removing and collapsing spaces in the tools differ (by default).
151 | >
152 | > - `@swc/html` - Remove and collapse whitespaces only in safe places (for example - around `html` and `body` elements, inside the `head` element and between metadata elements - ``/`script`/`link`/etc.)
153 | > - `html-minifier-terser` - Always collapse multiple whitespaces to 1 space (never remove it entirely), but you can change it using [`options`](https://github.com/terser/html-minifier-terser#options-quick-reference)
154 | > - `@minify-html/node` - Please read documentation https://github.com/wilsonzlin/minify-html#whitespace for detailed whitespace behavior.
155 |
156 | ## Options
157 |
158 | - **[`test`](#test)**
159 | - **[`include`](#include)**
160 | - **[`exclude`](#exclude)**
161 | - **[`parallel`](#parallel)**
162 | - **[`minify`](#minify)**
163 | - **[`minimizerOptions`](#minimizerOptions)**
164 |
165 | ### `test`
166 |
167 | Type:
168 |
169 | ```ts
170 | type test = string | RegExp | (string | RegExp)[];
171 | ```
172 |
173 | Default: `/\.html(\?.*)?$/i`
174 |
175 | Test to match files against.
176 |
177 | ```js
178 | module.exports = {
179 | optimization: {
180 | minimize: true,
181 | minimizer: [
182 | new HtmlMinimizerPlugin({
183 | test: /\.foo\.html/i,
184 | }),
185 | ],
186 | },
187 | };
188 | ```
189 |
190 | ### `include`
191 |
192 | Type:
193 |
194 | ```ts
195 | type include = string | RegExp | (string | RegExp)[];
196 | ```
197 |
198 | Default: `undefined`
199 |
200 | Files to include for minification.
201 |
202 | **webpack.config.js**
203 |
204 | ```js
205 | module.exports = {
206 | optimization: {
207 | minimize: true,
208 | minimizer: [
209 | new HtmlMinimizerPlugin({
210 | include: /\/includes/,
211 | }),
212 | ],
213 | },
214 | };
215 | ```
216 |
217 | ### `exclude`
218 |
219 | Type:
220 |
221 | ```ts
222 | type exclude = string | RegExp | (string | RegExp)[];
223 | ```
224 |
225 | Default: `undefined`
226 |
227 | Files to exclude from minification.
228 |
229 | **webpack.config.js**
230 |
231 | ```js
232 | module.exports = {
233 | optimization: {
234 | minimize: true,
235 | minimizer: [
236 | new HtmlMinimizerPlugin({
237 | exclude: /\/excludes/,
238 | }),
239 | ],
240 | },
241 | };
242 | ```
243 |
244 | ### `parallel`
245 |
246 | Type:
247 |
248 | ```ts
249 | type parallel = undefined | boolean | number;
250 | ```
251 |
252 | Default: `true`
253 |
254 | Enables multi-process parallelization to improve build performance.
255 |
256 | - If `true`, uses `os.cpus().length - 1` or `os.availableParallelism() - 1` (if available).
257 |
258 | - If `number`, sets the number of concurrent workers.
259 |
260 | > [!NOTE]
261 | >
262 | > Parallelization can speed up your build significantly and is therefore **highly recommended**.
263 |
264 | #### `boolean`
265 |
266 | Enable or disable multi-process parallel running.
267 |
268 | **webpack.config.js**
269 |
270 | ```js
271 | module.exports = {
272 | optimization: {
273 | minimize: true,
274 | minimizer: [
275 | new HtmlMinimizerPlugin({
276 | parallel: true,
277 | }),
278 | ],
279 | },
280 | };
281 | ```
282 |
283 | #### `number`
284 |
285 | Enable multi-process parallel running and set number of concurrent runs.
286 |
287 | **webpack.config.js**
288 |
289 | ```js
290 | module.exports = {
291 | optimization: {
292 | minimize: true,
293 | minimizer: [
294 | new HtmlMinimizerPlugin({
295 | parallel: 4,
296 | }),
297 | ],
298 | },
299 | };
300 | ```
301 |
302 | ### `minify`
303 |
304 | Type:
305 |
306 | ```ts
307 | type minify =
308 | | ((
309 | data: Record,
310 | minimizerOptions: Record,
311 | ) => {
312 | code: string;
313 | errors?: unknown[] | undefined;
314 | warnings?: unknown[] | undefined;
315 | })
316 | | ((
317 | data: Record,
318 | minimizerOptions: Record,
319 | ) => {
320 | code: string;
321 | errors?: unknown[] | undefined;
322 | warnings?: unknown[] | undefined;
323 | })[];
324 | ```
325 |
326 | Default: `HtmlMinimizerPlugin.htmlMinifierTerser`
327 |
328 | Allows you to override default minify function.
329 | By default, plugin uses [html-minifier-terser](https://github.com/terser/html-minifier-terser) package.
330 |
331 | We currently support:
332 |
333 | - `HtmlMinimizerPlugin.swcMinify` (used to compress HTML documents, i.e. with HTML doctype and ``/``/`` tags)
334 | - `HtmlMinimizerPlugin.swcMinifyFragment` (used to compress HTML fragments, i.e. when you have part of HTML which will be inserted into another HTML parts)
335 | - `HtmlMinimizerPlugin.htmlMinifierTerser`
336 | - `HtmlMinimizerPlugin.minifyHtmlNode`
337 |
338 | > [!NOTE]
339 | >
340 | > The difference between `swcMinify` and `swcMinifyFragment` is the error reporting.
341 | > You will get errors (invalid or broken syntax) if you have them in your HTML document or fragment. Which allows you to see all the errors and problems at the build stage.
342 |
343 | Useful for using and testing unpublished versions or forks.
344 |
345 | > [!WARNING]
346 | >
347 | > **Always use `require` inside `minify` function when `parallel` option enabled**.
348 |
349 | #### `function`
350 |
351 | You can define a custom minify function, giving full control over how the HTML is processed.
352 |
353 | **webpack.config.js**
354 |
355 | ```js
356 | module.exports = {
357 | optimization: {
358 | minimize: true,
359 | minimizer: [
360 | new HtmlMinimizerPlugin({
361 | minimizerOptions: {
362 | collapseWhitespace: true,
363 | },
364 | minify: (data, minimizerOptions) => {
365 | const htmlMinifier = require("html-minifier-terser");
366 |
367 | const [[filename, input]] = Object.entries(data);
368 |
369 | return {
370 | code: htmlMinifier.minify(input, minimizerOptions),
371 | warnings: [],
372 | errors: [],
373 | };
374 | },
375 | }),
376 | ],
377 | },
378 | };
379 | ```
380 |
381 | #### `array`
382 |
383 | If an array of functions is passed to the `minify` option, the `minimizerOptions` can be either as:
384 |
385 | - An array; If `minimizerOptions` is array, the function index in the `minify` array corresponds to the options object with the same index in the `minimizerOptions` array.
386 |
387 | - A single object; If you use `minimizerOptions` like object, all `minify` function accept it.
388 |
389 | **webpack.config.js**
390 |
391 | ```js
392 | module.exports = {
393 | optimization: {
394 | minimize: true,
395 | minimizer: [
396 | new HtmlMinimizerPlugin({
397 | minimizerOptions: [
398 | // Options for the first function (HtmlMinimizerPlugin.htmlMinifierTerser)
399 | {
400 | collapseWhitespace: true,
401 | },
402 | // Options for the second function
403 | {},
404 | ],
405 | minify: [
406 | HtmlMinimizerPlugin.htmlMinifierTerser,
407 | (data, minimizerOptions) => {
408 | const [[filename, input]] = Object.entries(data);
409 | // To do something
410 | return {
411 | code: "optimised code",
412 | warnings: [],
413 | errors: [],
414 | };
415 | },
416 | ],
417 | }),
418 | ],
419 | },
420 | };
421 | ```
422 |
423 | ### `minimizerOptions`
424 |
425 | Type:
426 |
427 | ```ts
428 | type minimizerOptions = Record | Record[];
429 | ```
430 |
431 | Default:
432 |
433 |
434 |
435 | ```js
436 | {
437 | caseSensitive: true,
438 | collapseWhitespace: true,
439 | conservativeCollapse: true,
440 | keepClosingSlash: true,
441 | minifyCSS: true,
442 | minifyJS: true,
443 | removeComments: true,
444 | removeScriptTypeAttributes: true,
445 | removeStyleLinkTypeAttributes: true,
446 | }
447 | ```
448 |
449 | `Html-minifier-terser` optimizations [options](https://github.com/terser/html-minifier-terser#options-quick-reference).
450 |
451 | #### `object`
452 |
453 | Applies the same options to the default or custom `minify` function.
454 |
455 | ```js
456 | module.exports = {
457 | optimization: {
458 | minimize: true,
459 | minimizer: [
460 | new HtmlMinimizerPlugin({
461 | minimizerOptions: {
462 | collapseWhitespace: false,
463 | },
464 | }),
465 | ],
466 | },
467 | };
468 | ```
469 |
470 | #### `array`
471 |
472 | The function index in the `minify` array corresponds to the options object with the same index in the `minimizerOptions` array.
473 | If you use `minimizerOptions` like object, all `minify` function accept it.
474 |
475 | **webpack.config.js**
476 |
477 | ```js
478 | module.exports = {
479 | optimization: {
480 | minimize: true,
481 | minimizer: [
482 | new HtmlMinimizerPlugin({
483 | minimizerOptions: [
484 | // Options for the first function (HtmlMinimizerPlugin.htmlMinifierTerser)
485 | {
486 | collapseWhitespace: true,
487 | },
488 | // Options for the second function
489 | {},
490 | ],
491 | minify: [
492 | HtmlMinimizerPlugin.htmlMinifierTerser,
493 | (data, minimizerOptions) => {
494 | const [[filename, input]] = Object.entries(data);
495 | // To do something
496 | return {
497 | code: "optimised code",
498 | warnings: [],
499 | errors: [],
500 | };
501 | },
502 | ],
503 | }),
504 | ],
505 | },
506 | };
507 | ```
508 |
509 | ## Examples
510 |
511 | ### `swc/html`
512 |
513 | Available [`options`](https://github.com/swc-project/bindings/blob/main/packages/html/index.ts#L5).
514 |
515 | HTML Documents:
516 |
517 | ```js
518 | const CopyPlugin = require("copy-webpack-plugin");
519 | const HtmlMinimizerPlugin = require("html-minimizer-webpack-plugin");
520 |
521 | module.exports = {
522 | module: {
523 | rules: [
524 | {
525 | test: /\.html$/i,
526 | type: "asset/resource",
527 | },
528 | ],
529 | },
530 | plugins: [
531 | new CopyPlugin({
532 | patterns: [
533 | {
534 | context: path.resolve(__dirname, "dist"),
535 | from: "./src/*.html",
536 | },
537 | ],
538 | }),
539 | ],
540 | optimization: {
541 | minimize: true,
542 | minimizer: [
543 | new HtmlMinimizerPlugin({
544 | minify: HtmlMinimizerPlugin.swcMinify,
545 | minimizerOptions: {
546 | // Options - https://github.com/swc-project/bindings/blob/main/packages/html/index.ts#L5
547 | },
548 | }),
549 | ],
550 | },
551 | };
552 | ```
553 |
554 | HTML Fragments:
555 |
556 | Use this for partial HTML files (e.g. inside `` tags or HTML strings).
557 |
558 | ```js
559 | const path = require("node:path");
560 | const CopyPlugin = require("copy-webpack-plugin");
561 | const HtmlMinimizerPlugin = require("html-minimizer-webpack-plugin");
562 |
563 | module.exports = {
564 | module: {
565 | rules: [
566 | {
567 | test: /\.html$/i,
568 | type: "asset/resource",
569 | },
570 | ],
571 | },
572 | plugins: [
573 | new CopyPlugin({
574 | patterns: [
575 | {
576 | context: path.resolve(__dirname, "dist"),
577 | from: "./src/*.html",
578 | },
579 | ],
580 | }),
581 | ],
582 | optimization: {
583 | minimize: true,
584 | minimizer: [
585 | new HtmlMinimizerPlugin({
586 | test: /\.template\.html$/i,
587 | minify: HtmlMinimizerPlugin.swcMinifyFragment,
588 | minimizerOptions: {
589 | // Options - https://github.com/swc-project/bindings/blob/main/packages/html/index.ts#L38
590 | },
591 | }),
592 | ],
593 | },
594 | };
595 | ```
596 |
597 | ### `@minify-html/node`
598 |
599 | Available [`options`](https://github.com/wilsonzlin/minify-html#minification).
600 |
601 | HTML Documents:
602 |
603 |
604 |
605 | ```js
606 | const CopyPlugin = require("copy-webpack-plugin");
607 | const HtmlMinimizerPlugin = require("html-minimizer-webpack-plugin");
608 |
609 | module.exports = {
610 | module: {
611 | rules: [
612 | {
613 | test: /\.html$/i,
614 | type: "asset/resource",
615 | },
616 | ],
617 | },
618 | plugins: [
619 | new CopyPlugin({
620 | patterns: [
621 | {
622 | context: path.resolve(__dirname, "dist"),
623 | from: "./src/*.html",
624 | },
625 | ],
626 | }),
627 | ],
628 | optimization: {
629 | minimize: true,
630 | minimizer: [
631 | new HtmlMinimizerPlugin({
632 | minify: HtmlMinimizerPlugin.minifyHtmlNode,
633 | minimizerOptions: {
634 | // Options - https://github.com/wilsonzlin/minify-html#minification
635 | },
636 | }),
637 | ],
638 | },
639 | };
640 | ```
641 |
642 | You can use multiple `HtmlMinimizerPlugin` plugins to compress different files with the different `minify` function.
643 |
644 | ## Contributing
645 |
646 | We welcome all contributions!
647 | If you're new here, please take a moment to review our contributing guidelines before submitting issues or pull requests.
648 |
649 | [CONTRIBUTING](https://github.com/webpack/html-minimizer-webpack-plugin?tab=contributing-ov-file#contributing)
650 |
651 | ## License
652 |
653 | [MIT](./LICENSE)
654 |
655 | [npm]: https://img.shields.io/npm/v/html-minimizer-webpack-plugin.svg
656 | [npm-url]: https://npmjs.com/package/html-minimizer-webpack-plugin
657 | [node]: https://img.shields.io/node/v/html-minimizer-webpack-plugin.svg
658 | [node-url]: https://nodejs.org
659 | [tests]: https://github.com/webpack/html-minimizer-webpack-plugin/workflows/html-minimizer-webpack-plugin/badge.svg
660 | [tests-url]: https://github.com/webpack/html-minimizer-webpack-plugin/actions
661 | [cover]: https://codecov.io/gh/webpack/html-minimizer-webpack-plugin/branch/main/graph/badge.svg
662 | [cover-url]: https://codecov.io/gh/webpack/html-minimizer-webpack-plugin
663 | [discussion]: https://img.shields.io/github/discussions/webpack/webpack
664 | [discussion-url]: https://github.com/webpack/webpack/discussions
665 | [size]: https://packagephobia.now.sh/badge?p=html-minimizer-webpack-plugin
666 | [size-url]: https://packagephobia.now.sh/result?p=html-minimizer-webpack-plugin
667 |
--------------------------------------------------------------------------------