├── test ├── fixtures │ ├── entry.js │ ├── empty.html │ ├── template.html │ ├── cache.js │ ├── broken-html-syntax.html │ ├── include-exclude │ │ ├── exclude-1.html │ │ ├── exclude-2.html │ │ ├── include-1.html │ │ ├── include-2.html │ │ ├── include-3.html │ │ └── include-4.html │ ├── cache │ │ ├── cache.html │ │ ├── cache-1.html │ │ ├── cache-2.html │ │ ├── cache-3.html │ │ └── cache-4.html │ ├── simple.html │ └── parallel │ │ ├── foo-0.html │ │ ├── foo-1.html │ │ ├── foo-2.html │ │ ├── foo-3.html │ │ ├── foo-4.html │ │ ├── foo-5.html │ │ ├── foo-6.html │ │ ├── foo-7.html │ │ ├── foo-8.html │ │ └── foo-9.html ├── helpers │ ├── getErrors.js │ ├── getWarnings.js │ ├── normalizedSourceMap.js │ ├── compile.js │ ├── index.js │ ├── readAsset.js │ ├── ModifyExistingAsset.js │ ├── readAssets.js │ ├── normalizeErrors.js │ ├── emitAssetInChildCompilationLoader.js │ ├── EmitNewAsset.js │ └── getCompiler.js ├── htmlMinimizerOptions-option.test.js ├── __snapshots__ │ ├── htmlMinimizerOptions-option.test.js.snap │ ├── worker.test.js.snap │ ├── exclude-option.test.js.snap │ ├── include-option.test.js.snap │ ├── validate-options.test.js.snap │ ├── test-option.test.js.snap │ └── minify-option.test.js.snap ├── test-option.test.js ├── exclude-option.test.js ├── include-option.test.js ├── worker.test.js ├── validate-options.test.js ├── parallel-option.test.js ├── HtmlMinimizerPlugin.test.js └── minify-option.test.js ├── .husky ├── pre-commit └── commit-msg ├── .prettierignore ├── .gitattributes ├── jest.config.js ├── lint-staged.config.js ├── commitlint.config.js ├── eslint.config.mjs ├── .editorconfig ├── .gitignore ├── babel.config.js ├── .github └── workflows │ ├── dependency-review.yml │ └── nodejs.yml ├── tsconfig.json ├── .cspell.json ├── types ├── minify.d.ts ├── utils.d.ts └── index.d.ts ├── LICENSE ├── src ├── minify.js ├── options.json ├── utils.js └── index.js ├── package.json ├── CHANGELOG.md └── README.md /test/fixtures/entry.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/empty.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | lint-staged 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | commitlint --edit $1 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /dist 3 | /node_modules 4 | /test/fixtures 5 | CHANGELOG.md -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | bin/* eol=lf 3 | yarn.lock -diff 4 | package-lock.json -diff -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: "node", 3 | testTimeout: 20000, 4 | }; 5 | -------------------------------------------------------------------------------- /test/helpers/getErrors.js: -------------------------------------------------------------------------------- 1 | import normalizeErrors from "./normalizeErrors"; 2 | 3 | export default (stats) => normalizeErrors(stats.compilation.errors).sort(); 4 | -------------------------------------------------------------------------------- /test/helpers/getWarnings.js: -------------------------------------------------------------------------------- 1 | import normalizeErrors from "./normalizeErrors"; 2 | 3 | export default (stats) => normalizeErrors(stats.compilation.warnings).sort(); 4 | -------------------------------------------------------------------------------- /test/fixtures/template.html: -------------------------------------------------------------------------------- 1 |
2 | 7 |
8 | test 9 | -------------------------------------------------------------------------------- /test/fixtures/cache.js: -------------------------------------------------------------------------------- 1 | import './cache/cache.html'; 2 | import './cache/cache-1.html'; 3 | import './cache/cache-2.html'; 4 | import './cache/cache-3.html'; 5 | import './cache/cache-4.html'; 6 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "*": [ 3 | "prettier --cache --write --ignore-unknown", 4 | "cspell --cache --no-must-find-files", 5 | ], 6 | "*.js": ["eslint --cache --fix"], 7 | }; 8 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@commitlint/config-conventional"], 3 | rules: { 4 | "header-max-length": [0], 5 | "body-max-line-length": [0], 6 | "footer-max-line-length": [0], 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "eslint/config"; 2 | import configs from "eslint-config-webpack/configs.js"; 3 | 4 | export default defineConfig([ 5 | { 6 | extends: [configs["recommended-dirty"]], 7 | }, 8 | ]); 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /test/fixtures/broken-html-syntax.html: -------------------------------------------------------------------------------- 1 | Text < img src="image.png" > 2 | Text < 3 | Text > 4 | 5 | boohay 6 | <<<<>foo 7 | >>< 2 | 3 | 4 | 5 | Document 6 | 7 | 8 | 9 |

My First Heading

10 |

My first paragraph.

11 |

An Unordered HTML List

12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/fixtures/include-exclude/exclude-2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Document 6 | 7 | 8 | 9 |

My First Heading

10 |

My first paragraph.

11 |

An Unordered HTML List

12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/fixtures/include-exclude/include-1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Document 6 | 7 | 8 | 9 |

My First Heading

10 |

My first paragraph.

11 |

An Unordered HTML List

12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/fixtures/include-exclude/include-2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Document 6 | 7 | 8 | 9 |

My First Heading

10 |

My first paragraph.

11 |

An Unordered HTML List

12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/fixtures/include-exclude/include-3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Document 6 | 7 | 8 | 9 |

My First Heading

10 |

My first paragraph.

11 |

An Unordered HTML List

12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/fixtures/include-exclude/include-4.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Document 6 | 7 | 8 | 9 |

My First Heading

10 |

My first paragraph.

11 |

An Unordered HTML List

12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const MIN_BABEL_VERSION = 7; 2 | 3 | module.exports = (api) => { 4 | api.assertVersion(MIN_BABEL_VERSION); 5 | api.cache(true); 6 | 7 | return { 8 | presets: [ 9 | [ 10 | "@babel/preset-env", 11 | { 12 | targets: { 13 | node: "18.12.0", 14 | }, 15 | }, 16 | ], 17 | ], 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | name: "Dependency Review" 2 | on: [pull_request] 3 | 4 | permissions: 5 | contents: read 6 | 7 | jobs: 8 | dependency-review: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: "Checkout Repository" 12 | uses: actions/checkout@v5 13 | - name: "Dependency Review" 14 | uses: actions/dependency-review-action@v4 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | // "module": "nodenext", 5 | "moduleResolution": "node", 6 | "allowJs": true, 7 | "checkJs": true, 8 | "strict": true, 9 | "types": ["node"], 10 | "resolveJsonModule": true, 11 | "newLine": "LF", 12 | "allowSyntheticDefaultImports": true 13 | }, 14 | "include": ["./src/**/*"] 15 | } 16 | -------------------------------------------------------------------------------- /test/helpers/normalizedSourceMap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {string} map The source map string 3 | * @returns {string} The normalized source map 4 | */ 5 | export default function normalizedSourceMap(map) { 6 | return map.replace( 7 | // eslint-disable-next-line no-useless-escape 8 | /"sources":\[([\d\w\/\:\"\'].*)\]\,\"names\"/i, 9 | '"sources": [replaced for tests], "names"', 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /test/helpers/compile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {import("webpack").Compiler} compiler The webpack compiler 3 | * @returns {Promise} Promise that resolves to the compilation stats 4 | */ 5 | export default function compile(compiler) { 6 | return new Promise((resolve, reject) => { 7 | compiler.run((err, stats) => { 8 | if (err) return reject(err); 9 | return resolve(stats); 10 | }); 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /.cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2", 3 | "language": "en,en-gb", 4 | "words": [ 5 | "atata", 6 | "Whitespaces", 7 | "pathinfo", 8 | "memfs", 9 | "boohay", 10 | "cssnano", 11 | "Framgents", 12 | "Minifier", 13 | "minifier", 14 | "commitlint", 15 | "pathinfo", 16 | "memfs", 17 | "nodenext" 18 | ], 19 | 20 | "ignorePaths": [ 21 | "CHANGELOG.md", 22 | "package.json", 23 | "dist/**", 24 | "**/__snapshots__/**", 25 | "package-lock.json", 26 | "node_modules", 27 | "coverage", 28 | "*.log" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /test/helpers/index.js: -------------------------------------------------------------------------------- 1 | export { default as EmitNewAsset } from "./EmitNewAsset"; 2 | export { default as compile } from "./compile"; 3 | export { default as ModifyExistingAsset } from "./ModifyExistingAsset"; 4 | export { default as getCompiler } from "./getCompiler"; 5 | export { default as getErrors } from "./getErrors"; 6 | export { default as normalizeErrors } from "./normalizeErrors"; 7 | export { default as getWarnings } from "./getWarnings"; 8 | export { default as readAsset } from "./readAsset"; 9 | export { default as normalizedSourceMap } from "./normalizedSourceMap"; 10 | export { default as readAssets } from "./readAssets"; 11 | -------------------------------------------------------------------------------- /test/helpers/readAsset.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | 3 | export default (asset, compiler, stats) => { 4 | const usedFs = compiler.outputFileSystem; 5 | const outputPath = stats.compilation.outputOptions.path; 6 | 7 | let data = ""; 8 | let targetFile = asset; 9 | 10 | const queryStringIdx = targetFile.indexOf("?"); 11 | 12 | if (queryStringIdx >= 0) { 13 | targetFile = targetFile.slice(0, queryStringIdx); 14 | } 15 | 16 | try { 17 | data = usedFs.readFileSync(path.join(outputPath, targetFile)).toString(); 18 | } catch (error) { 19 | data = error.toString(); 20 | } 21 | 22 | return data; 23 | }; 24 | -------------------------------------------------------------------------------- /test/helpers/ModifyExistingAsset.js: -------------------------------------------------------------------------------- 1 | export default class ExistingCommentsFile { 2 | constructor(options = {}) { 3 | this.options = options; 4 | } 5 | 6 | apply(compiler) { 7 | const plugin = { name: this.constructor.name }; 8 | const { ConcatSource } = compiler.webpack.sources; 9 | 10 | compiler.hooks.thisCompilation.tap(plugin, (compilation) => { 11 | compilation.hooks.additionalAssets.tap(plugin, () => { 12 | compilation.assets[this.options.name] = new ConcatSource( 13 | "

Modified!!!

", 14 | compilation.assets[this.options.name], 15 | ); 16 | }); 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/helpers/readAssets.js: -------------------------------------------------------------------------------- 1 | import readAsset from "./readAsset"; 2 | 3 | /** 4 | * @param {import("webpack").Compiler} compiler The webpack compiler 5 | * @param {import("webpack").Stats} stats The webpack stats 6 | * @param {RegExp=} extRegexp The regex to filter assets 7 | * @returns {Record} The assets as key-value pairs 8 | */ 9 | export default function readAssets(compiler, stats, extRegexp) { 10 | const assets = {}; 11 | 12 | for (const asset of Object.keys(stats.compilation.assets)) { 13 | if (extRegexp && extRegexp.test(asset)) { 14 | assets[asset] = readAsset(asset, compiler, stats); 15 | } 16 | } 17 | 18 | return assets; 19 | } 20 | -------------------------------------------------------------------------------- /test/fixtures/cache/cache.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Cache 9 | 10 | 11 | 12 |

My First Heading

13 |

My first paragraph.

14 |

An Unordered HTML List

15 | 16 |
    17 |
  • Coffee
  • 18 |
  • Tea
  • 19 |
  • Milk
  • 20 |
21 | 22 |

An Ordered HTML List

23 | 24 |
    25 |
  1. Coffee
  2. 26 |
  3. Tea
  4. 27 |
  5. Milk
  6. 28 |
29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /test/fixtures/simple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Document 9 | 10 | 11 | 12 |

My First Heading

13 |

My first paragraph.

14 |

An Unordered HTML List

15 | 16 |
    17 |
  • Coffee
  • 18 |
  • Tea
  • 19 |
  • Milk
  • 20 |
21 | 22 |

An Ordered HTML List

23 | 24 |
    25 |
  1. Coffee
  2. 26 |
  3. Tea
  4. 27 |
  5. Milk
  6. 28 |
29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /test/fixtures/cache/cache-1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Cache-1 9 | 10 | 11 | 12 |

My First Heading

13 |

My first paragraph.

14 |

An Unordered HTML List

15 | 16 |
    17 |
  • Coffee
  • 18 |
  • Tea
  • 19 |
  • Milk
  • 20 |
21 | 22 |

An Ordered HTML List

23 | 24 |
    25 |
  1. Coffee
  2. 26 |
  3. Tea
  4. 27 |
  5. Milk
  6. 28 |
29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /test/fixtures/cache/cache-2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Cache-2 9 | 10 | 11 | 12 |

My First Heading

13 |

My first paragraph.

14 |

An Unordered HTML List

15 | 16 |
    17 |
  • Coffee
  • 18 |
  • Tea
  • 19 |
  • Milk
  • 20 |
21 | 22 |

An Ordered HTML List

23 | 24 |
    25 |
  1. Coffee
  2. 26 |
  3. Tea
  4. 27 |
  5. Milk
  6. 28 |
29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /test/fixtures/cache/cache-3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Cache-3 9 | 10 | 11 | 12 |

My First Heading

13 |

My first paragraph.

14 |

An Unordered HTML List

15 | 16 |
    17 |
  • Coffee
  • 18 |
  • Tea
  • 19 |
  • Milk
  • 20 |
21 | 22 |

An Ordered HTML List

23 | 24 |
    25 |
  1. Coffee
  2. 26 |
  3. Tea
  4. 27 |
  5. Milk
  6. 28 |
29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /test/fixtures/cache/cache-4.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Cache-4 9 | 10 | 11 | 12 |

My First Heading

13 |

My first paragraph.

14 |

An Unordered HTML List

15 | 16 |
    17 |
  • Coffee
  • 18 |
  • Tea
  • 19 |
  • Milk
  • 20 |
21 | 22 |

An Ordered HTML List

23 | 24 |
    25 |
  1. Coffee
  2. 26 |
  3. Tea
  4. 27 |
  5. Milk
  6. 28 |
29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /test/fixtures/parallel/foo-0.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Cache 9 | 10 | 11 | 12 |

My First Heading

13 |

My first paragraph.

14 |

An Unordered HTML List

15 | 16 |
    17 |
  • Coffee
  • 18 |
  • Tea
  • 19 |
  • Milk
  • 20 |
21 | 22 |

An Ordered HTML List

23 | 24 |
    25 |
  1. Coffee
  2. 26 |
  3. Tea
  4. 27 |
  5. Milk
  6. 28 |
29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /test/fixtures/parallel/foo-1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Cache-1 9 | 10 | 11 | 12 |

My First Heading

13 |

My first paragraph.

14 |

An Unordered HTML List

15 | 16 |
    17 |
  • Coffee
  • 18 |
  • Tea
  • 19 |
  • Milk
  • 20 |
21 | 22 |

An Ordered HTML List

23 | 24 |
    25 |
  1. Coffee
  2. 26 |
  3. Tea
  4. 27 |
  5. Milk
  6. 28 |
29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /test/fixtures/parallel/foo-2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Cache-2 9 | 10 | 11 | 12 |

My First Heading

13 |

My first paragraph.

14 |

An Unordered HTML List

15 | 16 |
    17 |
  • Coffee
  • 18 |
  • Tea
  • 19 |
  • Milk
  • 20 |
21 | 22 |

An Ordered HTML List

23 | 24 |
    25 |
  1. Coffee
  2. 26 |
  3. Tea
  4. 27 |
  5. Milk
  6. 28 |
29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /test/fixtures/parallel/foo-3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Cache-3 9 | 10 | 11 | 12 |

My First Heading

13 |

My first paragraph.

14 |

An Unordered HTML List

15 | 16 |
    17 |
  • Coffee
  • 18 |
  • Tea
  • 19 |
  • Milk
  • 20 |
21 | 22 |

An Ordered HTML List

23 | 24 |
    25 |
  1. Coffee
  2. 26 |
  3. Tea
  4. 27 |
  5. Milk
  6. 28 |
29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /test/fixtures/parallel/foo-4.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Cache-4 9 | 10 | 11 | 12 |

My First Heading

13 |

My first paragraph.

14 |

An Unordered HTML List

15 | 16 |
    17 |
  • Coffee
  • 18 |
  • Tea
  • 19 |
  • Milk
  • 20 |
21 | 22 |

An Ordered HTML List

23 | 24 |
    25 |
  1. Coffee
  2. 26 |
  3. Tea
  4. 27 |
  5. Milk
  6. 28 |
29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /test/fixtures/parallel/foo-5.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Cache-4 9 | 10 | 11 | 12 |

My First Heading

13 |

My first paragraph.

14 |

An Unordered HTML List

15 | 16 |
    17 |
  • Coffee
  • 18 |
  • Tea
  • 19 |
  • Milk
  • 20 |
21 | 22 |

An Ordered HTML List

23 | 24 |
    25 |
  1. Coffee
  2. 26 |
  3. Tea
  4. 27 |
  5. Milk
  6. 28 |
29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /test/fixtures/parallel/foo-6.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Cache-4 9 | 10 | 11 | 12 |

My First Heading

13 |

My first paragraph.

14 |

An Unordered HTML List

15 | 16 |
    17 |
  • Coffee
  • 18 |
  • Tea
  • 19 |
  • Milk
  • 20 |
21 | 22 |

An Ordered HTML List

23 | 24 |
    25 |
  1. Coffee
  2. 26 |
  3. Tea
  4. 27 |
  5. Milk
  6. 28 |
29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /test/fixtures/parallel/foo-7.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Cache-4 9 | 10 | 11 | 12 |

My First Heading

13 |

My first paragraph.

14 |

An Unordered HTML List

15 | 16 |
    17 |
  • Coffee
  • 18 |
  • Tea
  • 19 |
  • Milk
  • 20 |
21 | 22 |

An Ordered HTML List

23 | 24 |
    25 |
  1. Coffee
  2. 26 |
  3. Tea
  4. 27 |
  5. Milk
  6. 28 |
29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /test/fixtures/parallel/foo-8.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Cache-4 9 | 10 | 11 | 12 |

My First Heading

13 |

My first paragraph.

14 |

An Unordered HTML List

15 | 16 |
    17 |
  • Coffee
  • 18 |
  • Tea
  • 19 |
  • Milk
  • 20 |
21 | 22 |

An Ordered HTML List

23 | 24 |
    25 |
  1. Coffee
  2. 26 |
  3. Tea
  4. 27 |
  5. Milk
  6. 28 |
29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /test/fixtures/parallel/foo-9.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Cache-4 9 | 10 | 11 | 12 |

My First Heading

13 |

My first paragraph.

14 |

An Unordered HTML List

15 | 16 |
    17 |
  • Coffee
  • 18 |
  • Tea
  • 19 |
  • Milk
  • 20 |
21 | 22 |

An Ordered HTML List

23 | 24 |
    25 |
  1. Coffee
  2. 26 |
  3. Tea
  4. 27 |
  5. Milk
  6. 28 |
29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /test/helpers/normalizeErrors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {string} str The string to process 3 | * @returns {string} The processed string with CWD removed 4 | */ 5 | function removeCWD(str) { 6 | const isWin = process.platform === "win32"; 7 | let cwd = process.cwd(); 8 | 9 | if (isWin) { 10 | str = str.replaceAll("\\", "/"); 11 | 12 | cwd = cwd.replaceAll("\\", "/"); 13 | } 14 | 15 | return str.replaceAll(new RegExp(cwd, "g"), ""); 16 | } 17 | 18 | /** 19 | * @param {Array} errors The errors to normalize 20 | * @returns {Array} The normalized error messages 21 | */ 22 | export default (errors) => 23 | errors.map((error) => 24 | removeCWD(error.toString().split("\n").slice(0, 2).join("\n")), 25 | ); 26 | -------------------------------------------------------------------------------- /test/htmlMinimizerOptions-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('when applied with "minimizerOptions" option', () => { 12 | it("should rewrite default options", async () => { 13 | const testHtmlId = "./simple.html"; 14 | const compiler = getCompiler(testHtmlId); 15 | 16 | new HtmlMinimizerPlugin({ 17 | minimizerOptions: { 18 | collapseWhitespace: false, 19 | }, 20 | }).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 | -------------------------------------------------------------------------------- /types/minify.d.ts: -------------------------------------------------------------------------------- 1 | export type MinimizedResult = import("./index.js").MinimizedResult; 2 | export type InternalResult = import("./index.js").InternalResult; 3 | /** @typedef {import("./index.js").MinimizedResult} MinimizedResult */ 4 | /** @typedef {import("./index.js").InternalResult} InternalResult */ 5 | /** 6 | * @template T 7 | * @param {import("./index.js").InternalOptions} options The minification options 8 | * @returns {Promise} Promise that resolves to the minification result 9 | */ 10 | export function minify( 11 | options: import("./index.js").InternalOptions, 12 | ): Promise; 13 | /** 14 | * @template T 15 | * @param {import("./index.js").InternalOptions} options The transformation options 16 | * @returns {Promise} Promise that resolves to the transformation result 17 | */ 18 | export function transform( 19 | options: import("./index.js").InternalOptions, 20 | ): Promise; 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright JS Foundation and other contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | 'Software'), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /test/helpers/emitAssetInChildCompilationLoader.js: -------------------------------------------------------------------------------- 1 | class PreCopyPlugin { 2 | constructor(options = {}) { 3 | this.options = options.options || {}; 4 | } 5 | 6 | apply(compiler) { 7 | const plugin = { name: "PreCopyPlugin" }; 8 | const { RawSource } = compiler.webpack.sources; 9 | 10 | compiler.hooks.compilation.tap(plugin, (compilation) => { 11 | compilation.hooks.additionalAssets.tapAsync(plugin, (callback) => { 12 | compilation.emitAsset( 13 | "simple.html", 14 | new RawSource('

foo

'), 15 | ); 16 | 17 | callback(); 18 | }); 19 | }); 20 | } 21 | } 22 | 23 | /** 24 | * @returns {void} 25 | */ 26 | export default function loader() { 27 | const callback = this.async(); 28 | 29 | const childCompiler = this._compilation.createChildCompiler( 30 | "preloader", 31 | this.options, 32 | ); 33 | 34 | new PreCopyPlugin().apply(childCompiler); 35 | 36 | childCompiler.runAsChild((error) => { 37 | if (error) { 38 | return callback(error); 39 | } 40 | 41 | return callback(null, "export default 1"); 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /test/__snapshots__/htmlMinimizerOptions-option.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing 2 | 3 | exports[`when applied with "minimizerOptions" option should rewrite default options: assets 1`] = ` 4 | { 5 | "simple.html": " 6 | 7 | 8 | 9 | 10 | 11 | Document 12 | 13 | 14 | 15 |

My First Heading

16 |

My first paragraph.

17 |

An Unordered HTML List

18 | 19 |
    20 |
  • Coffee
  • 21 |
  • Tea
  • 22 |
  • Milk
  • 23 |
24 | 25 |

An Ordered HTML List

26 | 27 |
    28 |
  1. Coffee
  2. 29 |
  3. Tea
  4. 30 |
  5. Milk
  6. 31 |
32 | 33 | 34 | 35 | ", 36 | } 37 | `; 38 | 39 | exports[`when applied with "minimizerOptions" option should rewrite default options: errors 1`] = `[]`; 40 | 41 | exports[`when applied with "minimizerOptions" option should rewrite default options: warnings 1`] = `[]`; 42 | -------------------------------------------------------------------------------- /test/helpers/EmitNewAsset.js: -------------------------------------------------------------------------------- 1 | export default class EmitNewAsset { 2 | constructor(options = {}) { 3 | this.options = options; 4 | } 5 | 6 | apply(compiler) { 7 | const pluginName = this.constructor.name; 8 | 9 | const { RawSource } = compiler.webpack.sources; 10 | 11 | compiler.hooks.compilation.tap(pluginName, (compilation) => { 12 | compilation.hooks.processAssets.tap( 13 | { 14 | name: pluginName, 15 | stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_REPORT, 16 | }, 17 | () => { 18 | compilation.emitAsset( 19 | this.options.name, 20 | new RawSource( 21 | ` 22 | 23 | 24 | 25 | 26 | 27 | Document 28 | 29 | 30 | 31 | 32 | 33 | `, 34 | ), 35 | ); 36 | }, 37 | ); 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/__snapshots__/worker.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`worker should emit error: error 1`] = ` 4 | [ 5 | "str.replace is not a function", 6 | ] 7 | `; 8 | 9 | exports[`worker should minify html #2: html 1`] = ` 10 | { 11 | "errors": [], 12 | "outputs": [ 13 | { 14 | "code": "

foo

", 15 | }, 16 | ], 17 | "warnings": [], 18 | } 19 | `; 20 | 21 | exports[`worker should minify html #3: html 1`] = ` 22 | { 23 | "errors": [], 24 | "outputs": [ 25 | { 26 | "code": "

from-minify-function

", 27 | }, 28 | ], 29 | "warnings": [], 30 | } 31 | `; 32 | 33 | exports[`worker should minify html #4: html 1`] = ` 34 | { 35 | "errors": [], 36 | "outputs": [ 37 | { 38 | "code": "

from-minify-function

", 39 | }, 40 | ], 41 | "warnings": [], 42 | } 43 | `; 44 | 45 | exports[`worker should minify html #5: html 1`] = ` 46 | { 47 | "errors": [], 48 | "outputs": [ 49 | { 50 | "code": "

from-minify-function

", 51 | }, 52 | ], 53 | "warnings": [], 54 | } 55 | `; 56 | 57 | exports[`worker should minify html: html 1`] = ` 58 | { 59 | "errors": [], 60 | "outputs": [ 61 | { 62 | "code": "

foo

", 63 | }, 64 | ], 65 | "warnings": [], 66 | } 67 | `; 68 | -------------------------------------------------------------------------------- /test/test-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('when applied with "test" option', () => { 12 | let compiler; 13 | 14 | beforeEach(() => { 15 | jest.clearAllMocks(); 16 | 17 | const testHtmlId = "./parallel/foo-[0-4].html"; 18 | 19 | compiler = getCompiler(testHtmlId); 20 | }); 21 | 22 | it("matches snapshot with empty value", async () => { 23 | new HtmlMinimizerPlugin().apply(compiler); 24 | 25 | const stats = await compile(compiler); 26 | 27 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("assets"); 28 | expect(getErrors(stats)).toMatchSnapshot("errors"); 29 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 30 | }); 31 | 32 | it('matches snapshot for a single "test" value (RegExp)', async () => { 33 | new HtmlMinimizerPlugin({ 34 | test: /foo-[1-3]\.html/, 35 | }).apply(compiler); 36 | 37 | const stats = await compile(compiler); 38 | 39 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("assets"); 40 | expect(getErrors(stats)).toMatchSnapshot("errors"); 41 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 42 | }); 43 | 44 | it('matches snapshot for multiple "test" value (RegExp)', async () => { 45 | new HtmlMinimizerPlugin({ 46 | test: [/foo-[0]\.html/, /foo-[1-2]\.html/], 47 | }).apply(compiler); 48 | 49 | const stats = await compile(compiler); 50 | 51 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("assets"); 52 | expect(getErrors(stats)).toMatchSnapshot("errors"); 53 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /test/exclude-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("exclude option", () => { 12 | let compiler; 13 | 14 | beforeEach(() => { 15 | const testHtmlId = "./include-exclude/*.html"; 16 | 17 | compiler = getCompiler(testHtmlId); 18 | }); 19 | 20 | it("should match snapshot for a single RegExp value exclude", async () => { 21 | new HtmlMinimizerPlugin({ 22 | exclude: /include-exclude\/exclude/i, 23 | }).apply(compiler); 24 | 25 | const stats = await compile(compiler); 26 | 27 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("assets"); 28 | expect(getErrors(stats)).toMatchSnapshot("errors"); 29 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 30 | }); 31 | 32 | it("should match snapshot for multiple RegExp values exclude", async () => { 33 | new HtmlMinimizerPlugin({ 34 | exclude: [ 35 | /include-exclude\/exclude-(1)/i, 36 | /include-exclude\/exclude-(2)/i, 37 | ], 38 | }).apply(compiler); 39 | 40 | const stats = await compile(compiler); 41 | 42 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("assets"); 43 | expect(getErrors(stats)).toMatchSnapshot("errors"); 44 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 45 | }); 46 | 47 | it("should match snapshot for multiple String values exclude", async () => { 48 | new HtmlMinimizerPlugin({ 49 | exclude: ["include-exclude/exclude-1", "include-exclude/exclude-2"], 50 | }).apply(compiler); 51 | 52 | const stats = await compile(compiler); 53 | 54 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("assets"); 55 | expect(getErrors(stats)).toMatchSnapshot("errors"); 56 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /test/include-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("include option", () => { 12 | let compiler; 13 | 14 | beforeEach(() => { 15 | const testHtmlId = "./include-exclude/*.html"; 16 | 17 | compiler = getCompiler(testHtmlId); 18 | }); 19 | 20 | it("should match snapshot for a single RegExp value include", async () => { 21 | new HtmlMinimizerPlugin({ 22 | include: /include-exclude\/include/i, 23 | }).apply(compiler); 24 | 25 | const stats = await compile(compiler); 26 | 27 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("assets"); 28 | expect(getErrors(stats)).toMatchSnapshot("errors"); 29 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 30 | }); 31 | 32 | it("should match snapshot for multiple RegExp values include", async () => { 33 | new HtmlMinimizerPlugin({ 34 | include: [ 35 | /include-exclude\/include-(1|2)/i, 36 | /include-exclude\/include-(3|4)/i, 37 | ], 38 | }).apply(compiler); 39 | 40 | const stats = await compile(compiler); 41 | 42 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("assets"); 43 | expect(getErrors(stats)).toMatchSnapshot("errors"); 44 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 45 | }); 46 | 47 | it("should match snapshot for multiple String values include", async () => { 48 | new HtmlMinimizerPlugin({ 49 | exclude: [ 50 | "include-exclude/include-1", 51 | "include-exclude/include-2", 52 | "include-exclude/include-3", 53 | "include-exclude/include-4", 54 | ], 55 | }).apply(compiler); 56 | 57 | const stats = await compile(compiler); 58 | 59 | expect(readAssets(compiler, stats, /\.html$/i)).toMatchSnapshot("assets"); 60 | expect(getErrors(stats)).toMatchSnapshot("errors"); 61 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /test/helpers/getCompiler.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | 3 | import CopyPlugin from "copy-webpack-plugin"; 4 | import { Volume, createFsFromVolume } from "memfs"; 5 | import webpack from "webpack"; 6 | 7 | /** 8 | * @param {string=} htmlFixture The HTML fixture file path 9 | * @param {import("webpack").Configuration=} config The webpack configuration 10 | * @returns {import("webpack").Compiler} The webpack compiler instance 11 | */ 12 | export default function getCompiler(htmlFixture, config = {}) { 13 | const compiler = webpack( 14 | Array.isArray(config) 15 | ? config 16 | : { 17 | mode: "production", 18 | bail: true, 19 | devtool: config.devtool || false, 20 | context: path.resolve(__dirname, "../fixtures"), 21 | entry: path.resolve(__dirname, "../fixtures/entry.js"), 22 | optimization: { 23 | minimize: false, 24 | emitOnErrors: true, 25 | }, 26 | output: { 27 | pathinfo: false, 28 | path: path.resolve(__dirname, "../dist"), 29 | filename: "[name].js", 30 | chunkFilename: "[id].[name].js", 31 | assetModuleFilename: "[name][ext]", 32 | }, 33 | plugins: [ 34 | htmlFixture 35 | ? [ 36 | new CopyPlugin({ 37 | patterns: [ 38 | { 39 | context: path.resolve(__dirname, "..", "fixtures"), 40 | from: htmlFixture, 41 | }, 42 | ], 43 | }), 44 | ] 45 | : [], 46 | ].flat(), 47 | module: { 48 | rules: [ 49 | !htmlFixture 50 | ? [ 51 | { 52 | test: /\.html$/i, 53 | type: "asset/resource", 54 | }, 55 | ] 56 | : [], 57 | ].flat(), 58 | }, 59 | ...config, 60 | }, 61 | ); 62 | 63 | if (!config.outputFileSystem) { 64 | compiler.outputFileSystem = createFsFromVolume(new Volume()); 65 | } 66 | 67 | return compiler; 68 | } 69 | -------------------------------------------------------------------------------- /src/minify.js: -------------------------------------------------------------------------------- 1 | /** @typedef {import("./index.js").MinimizedResult} MinimizedResult */ 2 | /** @typedef {import("./index.js").InternalResult} InternalResult */ 3 | 4 | /** 5 | * @template T 6 | * @param {import("./index.js").InternalOptions} options The minification options 7 | * @returns {Promise} Promise that resolves to the minification result 8 | */ 9 | async function minify(options) { 10 | const minifyFns = Array.isArray(options.minimizer.implementation) 11 | ? options.minimizer.implementation 12 | : [options.minimizer.implementation]; 13 | 14 | /** @type {InternalResult} */ 15 | const result = { outputs: [], warnings: [], errors: [] }; 16 | 17 | for (let i = 0; i <= minifyFns.length - 1; i++) { 18 | const minifyFn = minifyFns[i]; 19 | const minifyOptions = Array.isArray(options.minimizer.options) 20 | ? options.minimizer.options[i] 21 | : options.minimizer.options; 22 | const prevResult = 23 | result.outputs.length > 0 24 | ? result.outputs[result.outputs.length - 1] 25 | : { code: options.input }; 26 | const { code } = prevResult; 27 | 28 | /** @type {MinimizedResult} */ 29 | const minifyResult = await minifyFn( 30 | { [options.name]: code }, 31 | minifyOptions, 32 | ); 33 | 34 | if (typeof minifyResult === "string") { 35 | result.outputs.push({ code: minifyResult }); 36 | } else { 37 | if (typeof minifyResult.code === "string") { 38 | result.outputs.push({ code: minifyResult.code }); 39 | } 40 | 41 | if (minifyResult.errors) { 42 | result.errors = [...result.errors, ...minifyResult.errors]; 43 | } 44 | 45 | if (minifyResult.warnings) { 46 | result.warnings = [...result.warnings, ...minifyResult.warnings]; 47 | } 48 | } 49 | } 50 | 51 | if (typeof result.outputs[result.outputs.length - 1] !== "undefined") { 52 | result.outputs = [result.outputs[result.outputs.length - 1]]; 53 | } 54 | 55 | return result; 56 | } 57 | 58 | /** 59 | * @template T 60 | * @param {import("./index.js").InternalOptions} options The transformation options 61 | * @returns {Promise} Promise that resolves to the transformation result 62 | */ 63 | async function transform(options) { 64 | // 'use strict' => this === undefined (Clean Scope) 65 | // Safer for possible security issues, albeit not critical at all here 66 | // eslint-disable-next-line no-new-func 67 | const evaluatedOptions = new Function( 68 | "exports", 69 | "require", 70 | "module", 71 | "__filename", 72 | "__dirname", 73 | `'use strict'\nreturn ${options}`, 74 | )(module.exports, require, module, __filename, __dirname); 75 | 76 | return minify(evaluatedOptions); 77 | } 78 | 79 | module.exports = { minify, transform }; 80 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: html-minimizer-webpack-plugin 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - next 8 | pull_request: 9 | branches: 10 | - main 11 | - next 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | lint: 18 | name: Lint - ${{ matrix.os }} - Node v${{ matrix.node-version }} 19 | 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | 23 | strategy: 24 | matrix: 25 | os: [ubuntu-latest] 26 | node-version: [lts/*] 27 | 28 | runs-on: ${{ matrix.os }} 29 | 30 | concurrency: 31 | group: lint-${{ matrix.os }}-v${{ matrix.node-version }}-${{ github.ref }} 32 | cancel-in-progress: true 33 | 34 | steps: 35 | - uses: actions/checkout@v5 36 | with: 37 | fetch-depth: 0 38 | 39 | - name: Use Node.js ${{ matrix.node-version }} 40 | uses: actions/setup-node@v4 41 | with: 42 | node-version: ${{ matrix.node-version }} 43 | cache: "npm" 44 | 45 | - name: Install dependencies 46 | run: npm ci 47 | 48 | - name: Lint 49 | run: npm run lint 50 | 51 | - name: Build types 52 | run: npm run build:types 53 | 54 | - name: Check types 55 | run: if [ -n "$(git status types --porcelain)" ]; then echo "Missing types. Update types by running 'npm run build:types'"; exit 1; else echo "All types are valid"; fi 56 | 57 | - name: Security audit 58 | run: npm run security 59 | 60 | - name: Validate PR commits with commitlint 61 | if: github.event_name == 'pull_request' 62 | run: npx commitlint --from ${{ github.event.pull_request.head.sha }}~${{ github.event.pull_request.commits }} --to ${{ github.event.pull_request.head.sha }} --verbose 63 | 64 | test: 65 | name: Test - ${{ matrix.os }} - Node v${{ matrix.node-version }}, Webpack ${{ matrix.webpack-version }} 66 | 67 | strategy: 68 | matrix: 69 | os: [ubuntu-latest, windows-latest, macos-latest] 70 | node-version: [18.x, 20.x, 22.x, 24.x] 71 | webpack-version: [latest] 72 | 73 | runs-on: ${{ matrix.os }} 74 | 75 | concurrency: 76 | group: test-${{ matrix.os }}-v${{ matrix.node-version }}-${{ matrix.webpack-version }}-${{ github.ref }} 77 | cancel-in-progress: true 78 | 79 | steps: 80 | - name: Setup Git 81 | if: matrix.os == 'windows-latest' 82 | run: git config --global core.autocrlf input 83 | 84 | - uses: actions/checkout@v5 85 | 86 | - name: Use Node.js ${{ matrix.node-version }} 87 | uses: actions/setup-node@v4 88 | with: 89 | node-version: ${{ matrix.node-version }} 90 | cache: "npm" 91 | 92 | - name: Install dependencies 93 | run: npm i 94 | 95 | - name: Install webpack ${{ matrix.webpack-version }} 96 | if: matrix.webpack-version != 'latest' 97 | run: npm i webpack@${{ matrix.webpack-version }} 98 | 99 | - name: Run tests for webpack version ${{ matrix.webpack-version }} 100 | run: npm run test:coverage -- --ci 101 | 102 | - name: Submit coverage data to codecov 103 | uses: codecov/codecov-action@v5 104 | with: 105 | token: ${{ secrets.CODECOV_TOKEN }} 106 | -------------------------------------------------------------------------------- /src/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "definitions": { 3 | "Rule": { 4 | "description": "Filtering rule as regex or string.", 5 | "anyOf": [ 6 | { 7 | "instanceof": "RegExp", 8 | "tsType": "RegExp" 9 | }, 10 | { 11 | "type": "string", 12 | "minLength": 1 13 | } 14 | ] 15 | }, 16 | "Rules": { 17 | "description": "Filtering rules.", 18 | "anyOf": [ 19 | { 20 | "type": "array", 21 | "items": { 22 | "description": "A rule condition.", 23 | "oneOf": [ 24 | { 25 | "$ref": "#/definitions/Rule" 26 | } 27 | ] 28 | } 29 | }, 30 | { 31 | "$ref": "#/definitions/Rule" 32 | } 33 | ] 34 | }, 35 | "MinimizerOptions": { 36 | "additionalProperties": true, 37 | "type": "object" 38 | } 39 | }, 40 | "title": "HtmlMinimizerWebpackPluginOptions", 41 | "type": "object", 42 | "properties": { 43 | "test": { 44 | "description": "Include all modules that pass test assertion.", 45 | "link": "https://github.com/webpack/html-minimizer-webpack-plugin#test", 46 | "oneOf": [ 47 | { 48 | "$ref": "#/definitions/Rules" 49 | } 50 | ] 51 | }, 52 | "include": { 53 | "description": "Include all modules matching any of these conditions.", 54 | "link": "https://github.com/webpack/html-minimizer-webpack-plugin#include", 55 | "oneOf": [ 56 | { 57 | "$ref": "#/definitions/Rules" 58 | } 59 | ] 60 | }, 61 | "exclude": { 62 | "description": "Exclude all modules matching any of these conditions.", 63 | "link": "https://github.com/webpack/html-minimizer-webpack-plugin#exclude", 64 | "oneOf": [ 65 | { 66 | "$ref": "#/definitions/Rules" 67 | } 68 | ] 69 | }, 70 | "parallel": { 71 | "description": "Use multi-process parallel running to improve the build speed.", 72 | "link": "https://github.com/webpack/html-minimizer-webpack-plugin#parallel", 73 | "anyOf": [ 74 | { 75 | "type": "boolean" 76 | }, 77 | { 78 | "type": "integer" 79 | } 80 | ] 81 | }, 82 | "minify": { 83 | "description": "Allows you to override default minify function.", 84 | "link": "https://github.com/webpack/html-minimizer-webpack-plugin#minify", 85 | "anyOf": [ 86 | { 87 | "instanceof": "Function" 88 | }, 89 | { 90 | "type": "array", 91 | "minItems": 1, 92 | "items": { 93 | "instanceof": "Function" 94 | } 95 | } 96 | ] 97 | }, 98 | "minimizerOptions": { 99 | "description": "Options for `htmlMinimizerOptions`.", 100 | "link": "https://github.com/webpack/html-minimizer-webpack-plugin#minimizeroptions", 101 | "anyOf": [ 102 | { 103 | "$ref": "#/definitions/MinimizerOptions" 104 | }, 105 | { 106 | "type": "array", 107 | "minItems": 1, 108 | "items": { 109 | "$ref": "#/definitions/MinimizerOptions" 110 | } 111 | } 112 | ] 113 | } 114 | }, 115 | "additionalProperties": false 116 | } 117 | -------------------------------------------------------------------------------- /test/worker.test.js: -------------------------------------------------------------------------------- 1 | import serialize from "serialize-javascript"; 2 | 3 | import HtmlMinimizerPlugin from "../src"; 4 | import { transform } from "../src/minify"; 5 | 6 | import { normalizeErrors } from "./helpers"; 7 | 8 | describe("worker", () => { 9 | it("should minify html", async () => { 10 | const options = { 11 | name: "entry.html", 12 | input: '

foo

', 13 | minimizer: { 14 | implementation: HtmlMinimizerPlugin.htmlMinifierTerser, 15 | options: { 16 | removeComments: false, 17 | }, 18 | }, 19 | }; 20 | const result = await transform(serialize(options)); 21 | 22 | expect(result).toMatchSnapshot("html"); 23 | }); 24 | 25 | it("should minify html #2", async () => { 26 | const options = { 27 | name: "entry.html", 28 | input: '

foo

', 29 | minimizer: { 30 | implementation: HtmlMinimizerPlugin.htmlMinifierTerser, 31 | options: { 32 | removeComments: false, 33 | }, 34 | }, 35 | }; 36 | const result = await transform(serialize(options)); 37 | 38 | expect(result).toMatchSnapshot("html"); 39 | }); 40 | 41 | it("should minify html #3", async () => { 42 | const options = { 43 | name: "entry.html", 44 | input: '

foo

', 45 | minimizer: { 46 | implementation: () => ({ 47 | code: '

from-minify-function

', 48 | }), 49 | options: { removeComments: false }, 50 | }, 51 | }; 52 | const result = await transform(serialize(options)); 53 | 54 | expect(result).toMatchSnapshot("html"); 55 | }); 56 | 57 | it("should minify html #4", async () => { 58 | const options = { 59 | name: "entry.html", 60 | input: '

foo

', 61 | minimizer: { 62 | implementation: [ 63 | () => ({ 64 | code: '

from-minify-function

', 65 | }), 66 | () => ({ 67 | code: '

from-minify-function

', 68 | }), 69 | ], 70 | options: { removeComments: false }, 71 | }, 72 | }; 73 | const result = await transform(serialize(options)); 74 | 75 | expect(result).toMatchSnapshot("html"); 76 | }); 77 | 78 | it("should minify html #5", async () => { 79 | const options = { 80 | name: "entry.html", 81 | input: '

foo

', 82 | minimizer: { 83 | implementation: [ 84 | () => ({ 85 | code: '

from-minify-function

', 86 | }), 87 | (_code, opt) => ({ 88 | code: `

from-minify-function

`, 89 | }), 90 | ], 91 | options: [{ removeComments: false }, { removeComments: true }], 92 | }, 93 | }; 94 | const result = await transform(serialize(options)); 95 | 96 | expect(result).toMatchSnapshot("html"); 97 | }); 98 | 99 | it("should emit error", async () => { 100 | const options = { 101 | name: "entry.html", 102 | input: false, 103 | minimizer: { 104 | implementation: HtmlMinimizerPlugin.htmlMinifierTerser, 105 | }, 106 | }; 107 | 108 | try { 109 | await transform(serialize(options)); 110 | } catch (error) { 111 | const normalizeError = { ...error }; 112 | 113 | normalizeError.message = [error.message.split("\n")]; 114 | 115 | expect(normalizeErrors(normalizeError.message)).toMatchSnapshot("error"); 116 | } 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /types/utils.d.ts: -------------------------------------------------------------------------------- 1 | export type Task = () => Promise; 2 | export type MinimizedResult = import("./index.js").MinimizedResult; 3 | export type CustomOptions = import("./index.js").CustomOptions; 4 | export type Input = import("./index.js").Input; 5 | export type EXPECTED_ANY = any; 6 | /** 7 | * @param {Input} input The input to minify 8 | * @param {CustomOptions=} minimizerOptions The minimizer options 9 | * @returns {Promise} Promise that resolves to the minified result 10 | */ 11 | /** 12 | * @param {Input} input The input to minify 13 | * @param {CustomOptions=} minimizerOptions The minimizer options 14 | * @returns {Promise} Promise that resolves to the minified result 15 | */ 16 | export function htmlMinifierTerser( 17 | input: Input, 18 | minimizerOptions?: CustomOptions | undefined, 19 | ): Promise; 20 | export namespace htmlMinifierTerser { 21 | /** 22 | * @returns {boolean} Whether worker threads are supported 23 | */ 24 | function supportsWorkerThreads(): boolean; 25 | } 26 | /** 27 | * @template T 28 | * @param {(() => EXPECTED_ANY) | undefined} fn The function to memoize 29 | * @returns {() => T} The memoized function 30 | */ 31 | export function memoize(fn: (() => EXPECTED_ANY) | undefined): () => T; 32 | /** 33 | * @param {Input} input The input to minify 34 | * @param {CustomOptions=} minimizerOptions The minimizer options 35 | * @returns {Promise} Promise that resolves to the minified result 36 | */ 37 | /** 38 | * @param {Input} input The input to minify 39 | * @param {CustomOptions=} minimizerOptions The minimizer options 40 | * @returns {Promise} Promise that resolves to the minified result 41 | */ 42 | export function minifyHtmlNode( 43 | input: Input, 44 | minimizerOptions?: CustomOptions | undefined, 45 | ): Promise; 46 | export namespace minifyHtmlNode { 47 | /** 48 | * @returns {boolean} Whether worker threads are supported 49 | */ 50 | function supportsWorkerThreads(): boolean; 51 | } 52 | /** 53 | * @param {Input} input The input to minify 54 | * @param {CustomOptions=} minimizerOptions The minimizer options 55 | * @returns {Promise} Promise that resolves to the minified result 56 | */ 57 | /** 58 | * @param {Input} input The input to minify 59 | * @param {CustomOptions=} minimizerOptions The minimizer options 60 | * @returns {Promise} Promise that resolves to the minified result 61 | */ 62 | export function swcMinify( 63 | input: Input, 64 | minimizerOptions?: CustomOptions | undefined, 65 | ): Promise; 66 | export namespace swcMinify { 67 | /** 68 | * @returns {boolean} Whether worker threads are supported 69 | */ 70 | function supportsWorkerThreads(): boolean; 71 | } 72 | /** 73 | * @param {Input} input The input to minify 74 | * @param {CustomOptions=} minimizerOptions The minimizer options 75 | * @returns {Promise} Promise that resolves to the minified result 76 | */ 77 | /** 78 | * @param {Input} input The input to minify 79 | * @param {CustomOptions=} minimizerOptions The minimizer options 80 | * @returns {Promise} Promise that resolves to the minified result 81 | */ 82 | export function swcMinifyFragment( 83 | input: Input, 84 | minimizerOptions?: CustomOptions | undefined, 85 | ): Promise; 86 | export namespace swcMinifyFragment { 87 | /** 88 | * @returns {boolean} Whether worker threads are supported 89 | */ 90 | function supportsWorkerThreads(): boolean; 91 | } 92 | /** 93 | * @template T 94 | * @typedef {() => Promise} Task 95 | */ 96 | /** 97 | * Run tasks with limited concurrency. 98 | * @template T 99 | * @param {number} limit Limit of tasks that run at once. 100 | * @param {Task[]} tasks List of tasks to run. 101 | * @returns {Promise} A promise that fulfills to an array of the results 102 | */ 103 | export function throttleAll(limit: number, tasks: Task[]): Promise; 104 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html-minimizer-webpack-plugin", 3 | "version": "5.0.4", 4 | "description": "html minimizer plugin for Webpack", 5 | "keywords": [ 6 | "html", 7 | "webpack", 8 | "webpack-plugin", 9 | "minimize", 10 | "minimizer", 11 | "minify", 12 | "minifier", 13 | "optimize", 14 | "optimizer" 15 | ], 16 | "homepage": "https://github.com/webpack/html-minimizer-webpack-plugin", 17 | "bugs": "https://github.com/webpack/html-minimizer-webpack-plugin/issues", 18 | "repository": "webpack/html-minimizer-webpack-plugin", 19 | "funding": { 20 | "type": "opencollective", 21 | "url": "https://opencollective.com/webpack" 22 | }, 23 | "license": "MIT", 24 | "author": "webpack Contrib Team", 25 | "main": "dist/index.js", 26 | "types": "types/index.d.ts", 27 | "files": [ 28 | "dist", 29 | "types" 30 | ], 31 | "scripts": { 32 | "start": "npm run build -- -w", 33 | "clean": "del-cli dist types", 34 | "prebuild": "npm run clean", 35 | "build:types": "tsc --declaration --emitDeclarationOnly --outDir types && prettier \"types/**/*.ts\" --write", 36 | "build:code": "cross-env NODE_ENV=production babel src -d dist --copy-files", 37 | "build": "npm-run-all -p \"build:**\"", 38 | "commitlint": "commitlint --from=main", 39 | "security": "npm audit", 40 | "lint:prettier": "prettier --cache --list-different .", 41 | "lint:js": "eslint --cache .", 42 | "lint:spelling": "cspell --cache --no-must-find-files --quiet \"**/*.*\"", 43 | "lint:types": "tsc --pretty --noEmit", 44 | "lint": "npm-run-all -l -p \"lint:**\"", 45 | "fix:js": "npm run lint:js -- --fix", 46 | "fix:prettier": "npm run lint:prettier -- --write", 47 | "fix": "npm-run-all -l fix:js fix:prettier", 48 | "test:only": "cross-env NODE_ENV=test jest", 49 | "test:watch": "npm run test:only -- --watch", 50 | "test:coverage": "npm run test:only -- --collectCoverageFrom=\"src/**/*.js\" --coverage", 51 | "pretest": "npm run lint", 52 | "test": "npm run test:coverage", 53 | "prepare": "husky && npm run build", 54 | "release": "standard-version" 55 | }, 56 | "dependencies": { 57 | "@types/html-minifier-terser": "^7.0.2", 58 | "html-minifier-terser": "^7.2.0", 59 | "jest-worker": "^30.0.0", 60 | "schema-utils": "^4.2.0", 61 | "serialize-javascript": "^6.0.2" 62 | }, 63 | "devDependencies": { 64 | "@babel/cli": "^7.24.8", 65 | "@babel/core": "^7.25.2", 66 | "@babel/preset-env": "^7.25.3", 67 | "@commitlint/cli": "^19.3.0", 68 | "@commitlint/config-conventional": "^19.2.2", 69 | "@eslint/js": "^9.32.0", 70 | "@eslint/markdown": "^7.1.0", 71 | "@minify-html/node": "^0.16.4", 72 | "@stylistic/eslint-plugin": "^5.2.2", 73 | "@swc/html": "^1.13.2", 74 | "@types/node": "^20.14.9", 75 | "@types/serialize-javascript": "^5.0.4", 76 | "babel-jest": "^30.0.0", 77 | "copy-webpack-plugin": "^13.0.0", 78 | "cross-env": "^7.0.3", 79 | "cspell": "^8.13.1", 80 | "del": "^7.1.0", 81 | "del-cli": "^5.1.0", 82 | "eslint": "^9.31.0", 83 | "eslint-config-prettier": "^10.1.8", 84 | "eslint-config-webpack": "^4.5.0", 85 | "eslint-plugin-import": "^2.32.0", 86 | "eslint-plugin-jest": "^29.0.1", 87 | "eslint-plugin-jsdoc": "^52.0.0", 88 | "eslint-plugin-n": "^17.21.0", 89 | "eslint-plugin-prettier": "^5.5.3", 90 | "eslint-plugin-unicorn": "^60.0.0", 91 | "globals": "^16.3.0", 92 | "husky": "^9.1.4", 93 | "jest": "^30.0.0", 94 | "lint-staged": "^15.2.8", 95 | "memfs": "^4.11.1", 96 | "npm-run-all": "^4.1.5", 97 | "prettier": "^3.3.3", 98 | "standard-version": "^9.5.0", 99 | "typescript": "^5.5.4", 100 | "typescript-eslint": "^8.38.0", 101 | "webpack": "^5.93.0" 102 | }, 103 | "peerDependencies": { 104 | "webpack": "^5.1.0" 105 | }, 106 | "peerDependenciesMeta": { 107 | "@swc/html": { 108 | "optional": true 109 | } 110 | }, 111 | "engines": { 112 | "node": ">= 18.12.0" 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /test/validate-options.test.js: -------------------------------------------------------------------------------- 1 | import HtmlMinimizerPlugin from "../src"; 2 | 3 | describe("validation", () => { 4 | it("validation", () => { 5 | /* eslint-disable no-new */ 6 | expect(() => { 7 | new HtmlMinimizerPlugin({ test: /foo/ }); 8 | }).not.toThrow(); 9 | 10 | expect(() => { 11 | new HtmlMinimizerPlugin({ test: "foo" }); 12 | }).not.toThrow(); 13 | 14 | expect(() => { 15 | new HtmlMinimizerPlugin({ test: [/foo/] }); 16 | }).not.toThrow(); 17 | 18 | expect(() => { 19 | new HtmlMinimizerPlugin({ test: [/foo/, /bar/] }); 20 | }).not.toThrow(); 21 | 22 | expect(() => { 23 | new HtmlMinimizerPlugin({ test: ["foo", "bar"] }); 24 | }).not.toThrow(); 25 | 26 | expect(() => { 27 | new HtmlMinimizerPlugin({ test: [/foo/, "bar"] }); 28 | }).not.toThrow(); 29 | 30 | expect(() => { 31 | new HtmlMinimizerPlugin({ test: true }); 32 | }).toThrowErrorMatchingSnapshot(); 33 | 34 | expect(() => { 35 | new HtmlMinimizerPlugin({ test: [true] }); 36 | }).toThrowErrorMatchingSnapshot(); 37 | 38 | expect(() => { 39 | new HtmlMinimizerPlugin({ include: /foo/ }); 40 | }).not.toThrow(); 41 | 42 | expect(() => { 43 | new HtmlMinimizerPlugin({ include: "foo" }); 44 | }).not.toThrow(); 45 | 46 | expect(() => { 47 | new HtmlMinimizerPlugin({ include: [/foo/] }); 48 | }).not.toThrow(); 49 | 50 | expect(() => { 51 | new HtmlMinimizerPlugin({ include: [/foo/, /bar/] }); 52 | }).not.toThrow(); 53 | 54 | expect(() => { 55 | new HtmlMinimizerPlugin({ include: ["foo", "bar"] }); 56 | }).not.toThrow(); 57 | 58 | expect(() => { 59 | new HtmlMinimizerPlugin({ include: [/foo/, "bar"] }); 60 | }).not.toThrow(); 61 | 62 | expect(() => { 63 | new HtmlMinimizerPlugin({ include: true }); 64 | }).toThrowErrorMatchingSnapshot(); 65 | 66 | expect(() => { 67 | new HtmlMinimizerPlugin({ include: [true] }); 68 | }).toThrowErrorMatchingSnapshot(); 69 | 70 | expect(() => { 71 | new HtmlMinimizerPlugin({ exclude: /foo/ }); 72 | }).not.toThrow(); 73 | 74 | expect(() => { 75 | new HtmlMinimizerPlugin({ exclude: "foo" }); 76 | }).not.toThrow(); 77 | 78 | expect(() => { 79 | new HtmlMinimizerPlugin({ exclude: [/foo/] }); 80 | }).not.toThrow(); 81 | 82 | expect(() => { 83 | new HtmlMinimizerPlugin({ exclude: [/foo/, /bar/] }); 84 | }).not.toThrow(); 85 | 86 | expect(() => { 87 | new HtmlMinimizerPlugin({ exclude: ["foo", "bar"] }); 88 | }).not.toThrow(); 89 | 90 | expect(() => { 91 | new HtmlMinimizerPlugin({ exclude: [/foo/, "bar"] }); 92 | }).not.toThrow(); 93 | 94 | expect(() => { 95 | new HtmlMinimizerPlugin({ exclude: true }); 96 | }).toThrowErrorMatchingSnapshot(); 97 | 98 | expect(() => { 99 | new HtmlMinimizerPlugin({ exclude: [true] }); 100 | }).toThrowErrorMatchingSnapshot(); 101 | 102 | expect(() => { 103 | new HtmlMinimizerPlugin({ minimizerOptions: {} }); 104 | }).not.toThrow(); 105 | 106 | expect(() => { 107 | new HtmlMinimizerPlugin({ minimizerOptions: [{}, {}] }); 108 | }).not.toThrow(); 109 | 110 | expect(() => { 111 | new HtmlMinimizerPlugin({ minimizerOptions: null }); 112 | }).toThrowErrorMatchingSnapshot(); 113 | 114 | expect(() => { 115 | new HtmlMinimizerPlugin({ 116 | minimizerOptions: { collapseWhitespace: true }, 117 | }); 118 | }).not.toThrow(); 119 | 120 | expect(() => { 121 | new HtmlMinimizerPlugin({ parallel: true }); 122 | }).not.toThrow(); 123 | 124 | expect(() => { 125 | new HtmlMinimizerPlugin({ parallel: false }); 126 | }).not.toThrow(); 127 | 128 | expect(() => { 129 | new HtmlMinimizerPlugin({ parallel: 2 }); 130 | }).not.toThrow(); 131 | 132 | expect(() => { 133 | new HtmlMinimizerPlugin({ parallel: "2" }); 134 | }).toThrowErrorMatchingSnapshot(); 135 | 136 | expect(() => { 137 | new HtmlMinimizerPlugin({ parallel: {} }); 138 | }).toThrowErrorMatchingSnapshot(); 139 | 140 | expect(() => { 141 | new HtmlMinimizerPlugin({ minify() {} }); 142 | }).not.toThrow(); 143 | 144 | expect(() => { 145 | new HtmlMinimizerPlugin({ minify: [() => {}] }); 146 | }).not.toThrow(); 147 | 148 | expect(() => { 149 | new HtmlMinimizerPlugin({ minify: true }); 150 | }).toThrowErrorMatchingSnapshot(); 151 | 152 | expect(() => { 153 | new HtmlMinimizerPlugin({ unknown: true }); 154 | }).toThrowErrorMatchingSnapshot(); 155 | /* eslint-enable no-new */ 156 | }); 157 | }); 158 | -------------------------------------------------------------------------------- /test/__snapshots__/exclude-option.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`exclude option should match snapshot for a single RegExp value exclude: assets 1`] = ` 4 | { 5 | "include-exclude/exclude-1.html": " 6 | 7 | 8 | 9 | Document 10 | 11 | 12 | 13 |

My First Heading

14 |

My first paragraph.

15 |

An Unordered HTML List

16 | 17 | 18 | 19 | ", 20 | "include-exclude/exclude-2.html": " 21 | 22 | 23 | 24 | Document 25 | 26 | 27 | 28 |

My First Heading

29 |

My first paragraph.

30 |

An Unordered HTML List

31 | 32 | 33 | 34 | ", 35 | "include-exclude/include-1.html": " Document

My First Heading

My first paragraph.

An Unordered HTML List

", 36 | "include-exclude/include-2.html": " Document

My First Heading

My first paragraph.

An Unordered HTML List

", 37 | "include-exclude/include-3.html": " Document

My First Heading

My first paragraph.

An Unordered HTML List

", 38 | "include-exclude/include-4.html": " Document

My First Heading

My first paragraph.

An Unordered HTML List

", 39 | } 40 | `; 41 | 42 | exports[`exclude option should match snapshot for a single RegExp value exclude: errors 1`] = `[]`; 43 | 44 | exports[`exclude option should match snapshot for a single RegExp value exclude: warnings 1`] = `[]`; 45 | 46 | exports[`exclude option should match snapshot for multiple RegExp values exclude: assets 1`] = ` 47 | { 48 | "include-exclude/exclude-1.html": " 49 | 50 | 51 | 52 | Document 53 | 54 | 55 | 56 |

My First Heading

57 |

My first paragraph.

58 |

An Unordered HTML List

59 | 60 | 61 | 62 | ", 63 | "include-exclude/exclude-2.html": " 64 | 65 | 66 | 67 | Document 68 | 69 | 70 | 71 |

My First Heading

72 |

My first paragraph.

73 |

An Unordered HTML List

74 | 75 | 76 | 77 | ", 78 | "include-exclude/include-1.html": " Document

My First Heading

My first paragraph.

An Unordered HTML List

", 79 | "include-exclude/include-2.html": " Document

My First Heading

My first paragraph.

An Unordered HTML List

", 80 | "include-exclude/include-3.html": " Document

My First Heading

My first paragraph.

An Unordered HTML List

", 81 | "include-exclude/include-4.html": " Document

My First Heading

My first paragraph.

An Unordered HTML List

", 82 | } 83 | `; 84 | 85 | exports[`exclude option should match snapshot for multiple RegExp values exclude: errors 1`] = `[]`; 86 | 87 | exports[`exclude option should match snapshot for multiple RegExp values exclude: warnings 1`] = `[]`; 88 | 89 | exports[`exclude option should match snapshot for multiple String values exclude: assets 1`] = ` 90 | { 91 | "include-exclude/exclude-1.html": " 92 | 93 | 94 | 95 | Document 96 | 97 | 98 | 99 |

My First Heading

100 |

My first paragraph.

101 |

An Unordered HTML List

102 | 103 | 104 | 105 | ", 106 | "include-exclude/exclude-2.html": " 107 | 108 | 109 | 110 | Document 111 | 112 | 113 | 114 |

My First Heading

115 |

My first paragraph.

116 |

An Unordered HTML List

117 | 118 | 119 | 120 | ", 121 | "include-exclude/include-1.html": " Document

My First Heading

My first paragraph.

An Unordered HTML List

", 122 | "include-exclude/include-2.html": " Document

My First Heading

My first paragraph.

An Unordered HTML List

", 123 | "include-exclude/include-3.html": " Document

My First Heading

My first paragraph.

An Unordered HTML List

", 124 | "include-exclude/include-4.html": " Document

My First Heading

My first paragraph.

An Unordered HTML List

", 125 | } 126 | `; 127 | 128 | exports[`exclude option should match snapshot for multiple String values exclude: errors 1`] = `[]`; 129 | 130 | exports[`exclude option should match snapshot for multiple String values exclude: warnings 1`] = `[]`; 131 | -------------------------------------------------------------------------------- /test/__snapshots__/include-option.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`include option should match snapshot for a single RegExp value include: assets 1`] = ` 4 | { 5 | "include-exclude/exclude-1.html": " 6 | 7 | 8 | 9 | Document 10 | 11 | 12 | 13 |

My First Heading

14 |

My first paragraph.

15 |

An Unordered HTML List

16 | 17 | 18 | 19 | ", 20 | "include-exclude/exclude-2.html": " 21 | 22 | 23 | 24 | Document 25 | 26 | 27 | 28 |

My First Heading

29 |

My first paragraph.

30 |

An Unordered HTML List

31 | 32 | 33 | 34 | ", 35 | "include-exclude/include-1.html": " Document

My First Heading

My first paragraph.

An Unordered HTML List

", 36 | "include-exclude/include-2.html": " Document

My First Heading

My first paragraph.

An Unordered HTML List

", 37 | "include-exclude/include-3.html": " Document

My First Heading

My first paragraph.

An Unordered HTML List

", 38 | "include-exclude/include-4.html": " Document

My First Heading

My first paragraph.

An Unordered HTML List

", 39 | } 40 | `; 41 | 42 | exports[`include option should match snapshot for a single RegExp value include: errors 1`] = `[]`; 43 | 44 | exports[`include option should match snapshot for a single RegExp value include: warnings 1`] = `[]`; 45 | 46 | exports[`include option should match snapshot for multiple RegExp values include: assets 1`] = ` 47 | { 48 | "include-exclude/exclude-1.html": " 49 | 50 | 51 | 52 | Document 53 | 54 | 55 | 56 |

My First Heading

57 |

My first paragraph.

58 |

An Unordered HTML List

59 | 60 | 61 | 62 | ", 63 | "include-exclude/exclude-2.html": " 64 | 65 | 66 | 67 | Document 68 | 69 | 70 | 71 |

My First Heading

72 |

My first paragraph.

73 |

An Unordered HTML List

74 | 75 | 76 | 77 | ", 78 | "include-exclude/include-1.html": " Document

My First Heading

My first paragraph.

An Unordered HTML List

", 79 | "include-exclude/include-2.html": " Document

My First Heading

My first paragraph.

An Unordered HTML List

", 80 | "include-exclude/include-3.html": " Document

My First Heading

My first paragraph.

An Unordered HTML List

", 81 | "include-exclude/include-4.html": " Document

My First Heading

My first paragraph.

An Unordered HTML List

", 82 | } 83 | `; 84 | 85 | exports[`include option should match snapshot for multiple RegExp values include: errors 1`] = `[]`; 86 | 87 | exports[`include option should match snapshot for multiple RegExp values include: warnings 1`] = `[]`; 88 | 89 | exports[`include option should match snapshot for multiple String values include: assets 1`] = ` 90 | { 91 | "include-exclude/exclude-1.html": " Document

My First Heading

My first paragraph.

An Unordered HTML List

", 92 | "include-exclude/exclude-2.html": " Document

My First Heading

My first paragraph.

An Unordered HTML List

", 93 | "include-exclude/include-1.html": " 94 | 95 | 96 | 97 | Document 98 | 99 | 100 | 101 |

My First Heading

102 |

My first paragraph.

103 |

An Unordered HTML List

104 | 105 | 106 | 107 | ", 108 | "include-exclude/include-2.html": " 109 | 110 | 111 | 112 | Document 113 | 114 | 115 | 116 |

My First Heading

117 |

My first paragraph.

118 |

An Unordered HTML List

119 | 120 | 121 | 122 | ", 123 | "include-exclude/include-3.html": " 124 | 125 | 126 | 127 | Document 128 | 129 | 130 | 131 |

My First Heading

132 |

My first paragraph.

133 |

An Unordered HTML List

134 | 135 | 136 | 137 | ", 138 | "include-exclude/include-4.html": " 139 | 140 | 141 | 142 | Document 143 | 144 | 145 | 146 |

My First Heading

147 |

My first paragraph.

148 |

An Unordered HTML List

149 | 150 | 151 | 152 | ", 153 | } 154 | `; 155 | 156 | exports[`include option should match snapshot for multiple String values include: errors 1`] = `[]`; 157 | 158 | exports[`include option should match snapshot for multiple String values include: warnings 1`] = `[]`; 159 | -------------------------------------------------------------------------------- /test/__snapshots__/validate-options.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing 2 | 3 | exports[`validation validation 1`] = ` 4 | "Invalid options object. Html Minimizer Plugin has been initialized using an options object that does not match the API schema. 5 | - options.test should be one of these: 6 | [RegExp | non-empty string, ...] | RegExp | non-empty string 7 | -> Filtering rules. 8 | -> Read more at https://github.com/webpack/html-minimizer-webpack-plugin#test 9 | Details: 10 | * options.test should be an array: 11 | [RegExp | non-empty string, ...] 12 | * options.test should be one of these: 13 | RegExp | non-empty string 14 | -> Filtering rule as regex or string. 15 | Details: 16 | * options.test should be an instance of RegExp. 17 | * options.test should be a non-empty string." 18 | `; 19 | 20 | exports[`validation validation 2`] = ` 21 | "Invalid options object. Html Minimizer Plugin has been initialized using an options object that does not match the API schema. 22 | - options.test should be one of these: 23 | [RegExp | non-empty string, ...] | RegExp | non-empty string 24 | -> Filtering rules. 25 | -> Read more at https://github.com/webpack/html-minimizer-webpack-plugin#test 26 | Details: 27 | * options.test[0] should be one of these: 28 | RegExp | non-empty string 29 | -> Filtering rule as regex or string. 30 | Details: 31 | * options.test[0] should be an instance of RegExp. 32 | * options.test[0] should be a non-empty string." 33 | `; 34 | 35 | exports[`validation validation 3`] = ` 36 | "Invalid options object. Html Minimizer Plugin has been initialized using an options object that does not match the API schema. 37 | - options.include should be one of these: 38 | [RegExp | non-empty string, ...] | RegExp | non-empty string 39 | -> Filtering rules. 40 | -> Read more at https://github.com/webpack/html-minimizer-webpack-plugin#include 41 | Details: 42 | * options.include should be an array: 43 | [RegExp | non-empty string, ...] 44 | * options.include should be one of these: 45 | RegExp | non-empty string 46 | -> Filtering rule as regex or string. 47 | Details: 48 | * options.include should be an instance of RegExp. 49 | * options.include should be a non-empty string." 50 | `; 51 | 52 | exports[`validation validation 4`] = ` 53 | "Invalid options object. Html Minimizer Plugin has been initialized using an options object that does not match the API schema. 54 | - options.include should be one of these: 55 | [RegExp | non-empty string, ...] | RegExp | non-empty string 56 | -> Filtering rules. 57 | -> Read more at https://github.com/webpack/html-minimizer-webpack-plugin#include 58 | Details: 59 | * options.include[0] should be one of these: 60 | RegExp | non-empty string 61 | -> Filtering rule as regex or string. 62 | Details: 63 | * options.include[0] should be an instance of RegExp. 64 | * options.include[0] should be a non-empty string." 65 | `; 66 | 67 | exports[`validation validation 5`] = ` 68 | "Invalid options object. Html Minimizer Plugin has been initialized using an options object that does not match the API schema. 69 | - options.exclude should be one of these: 70 | [RegExp | non-empty string, ...] | RegExp | non-empty string 71 | -> Filtering rules. 72 | -> Read more at https://github.com/webpack/html-minimizer-webpack-plugin#exclude 73 | Details: 74 | * options.exclude should be an array: 75 | [RegExp | non-empty string, ...] 76 | * options.exclude should be one of these: 77 | RegExp | non-empty string 78 | -> Filtering rule as regex or string. 79 | Details: 80 | * options.exclude should be an instance of RegExp. 81 | * options.exclude should be a non-empty string." 82 | `; 83 | 84 | exports[`validation validation 6`] = ` 85 | "Invalid options object. Html Minimizer Plugin has been initialized using an options object that does not match the API schema. 86 | - options.exclude should be one of these: 87 | [RegExp | non-empty string, ...] | RegExp | non-empty string 88 | -> Filtering rules. 89 | -> Read more at https://github.com/webpack/html-minimizer-webpack-plugin#exclude 90 | Details: 91 | * options.exclude[0] should be one of these: 92 | RegExp | non-empty string 93 | -> Filtering rule as regex or string. 94 | Details: 95 | * options.exclude[0] should be an instance of RegExp. 96 | * options.exclude[0] should be a non-empty string." 97 | `; 98 | 99 | exports[`validation validation 7`] = ` 100 | "Invalid options object. Html Minimizer Plugin has been initialized using an options object that does not match the API schema. 101 | - options.minimizerOptions should be one of these: 102 | object { … } | [object { … }, ...] (should not have fewer than 1 item) 103 | -> Options for \`htmlMinimizerOptions\`. 104 | -> Read more at https://github.com/webpack/html-minimizer-webpack-plugin#minimizeroptions 105 | Details: 106 | * options.minimizerOptions should be an object: 107 | object { … } 108 | * options.minimizerOptions should be an array: 109 | [object { … }, ...] (should not have fewer than 1 item)" 110 | `; 111 | 112 | exports[`validation validation 8`] = ` 113 | "Invalid options object. Html Minimizer Plugin has been initialized using an options object that does not match the API schema. 114 | - options.parallel should be one of these: 115 | boolean | integer 116 | -> Use multi-process parallel running to improve the build speed. 117 | -> Read more at https://github.com/webpack/html-minimizer-webpack-plugin#parallel 118 | Details: 119 | * options.parallel should be a boolean. 120 | * options.parallel should be an integer." 121 | `; 122 | 123 | exports[`validation validation 9`] = ` 124 | "Invalid options object. Html Minimizer Plugin has been initialized using an options object that does not match the API schema. 125 | - options.parallel should be one of these: 126 | boolean | integer 127 | -> Use multi-process parallel running to improve the build speed. 128 | -> Read more at https://github.com/webpack/html-minimizer-webpack-plugin#parallel 129 | Details: 130 | * options.parallel should be a boolean. 131 | * options.parallel should be an integer." 132 | `; 133 | 134 | exports[`validation validation 10`] = ` 135 | "Invalid options object. Html Minimizer Plugin has been initialized using an options object that does not match the API schema. 136 | - options.minify should be one of these: 137 | function | [function, ...] (should not have fewer than 1 item) 138 | -> Allows you to override default minify function. 139 | -> Read more at https://github.com/webpack/html-minimizer-webpack-plugin#minify 140 | Details: 141 | * options.minify should be an instance of function. 142 | * options.minify should be an array: 143 | [function, ...] (should not have fewer than 1 item)" 144 | `; 145 | 146 | exports[`validation validation 11`] = ` 147 | "Invalid options object. Html Minimizer Plugin has been initialized using an options object that does not match the API schema. 148 | - options has an unknown property 'unknown'. These properties are valid: 149 | object { test?, include?, exclude?, parallel?, minify?, minimizerOptions? }" 150 | `; 151 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | export = HtmlMinimizerPlugin; 2 | /** 3 | * @template [T=import("html-minifier-terser").Options] 4 | * @typedef {BasePluginOptions & DefinedDefaultMinimizerAndOptions} PluginOptions 5 | */ 6 | /** 7 | * @template {PluginOptions} [T=PluginOptions] 8 | */ 9 | declare class HtmlMinimizerPlugin< 10 | T extends PluginOptions = PluginOptions< 11 | import("html-minifier-terser").Options 12 | >, 13 | > { 14 | /** 15 | * @private 16 | * @param {EXPECTED_ANY} warning The warning to build 17 | * @param {string} file The file path 18 | * @returns {Error & { hideStack?: boolean, file?: string }} The built warning 19 | */ 20 | private static buildWarning; 21 | /** 22 | * @private 23 | * @param {EXPECTED_ANY} error The error to build 24 | * @param {string} file The file path 25 | * @returns {Error} The built error 26 | */ 27 | private static buildError; 28 | /** 29 | * @private 30 | * @param {Parallel} parallel Parallel configuration 31 | * @returns {number} The number of available cores 32 | */ 33 | private static getAvailableNumberOfCores; 34 | /** 35 | * @private 36 | * @template T 37 | * @param {BasicMinimizerImplementation & MinimizeFunctionHelpers} implementation The minimizer implementation 38 | * @returns {boolean} Whether worker threads are supported 39 | */ 40 | private static isSupportsWorkerThreads; 41 | /** 42 | * @param {T=} options Plugin options 43 | */ 44 | constructor(options?: T | undefined); 45 | /** 46 | * @private 47 | * @type {InternalPluginOptions} 48 | */ 49 | private options; 50 | /** 51 | * @private 52 | * @param {Compiler} compiler The webpack compiler 53 | * @param {Compilation} compilation The webpack compilation 54 | * @param {Record} assets The assets to optimize 55 | * @param {{availableNumberOfCores: number}} optimizeOptions Optimization options 56 | * @returns {Promise} Promise that resolves when optimization is complete 57 | */ 58 | private optimize; 59 | /** 60 | * @param {Compiler} compiler The webpack compiler 61 | * @returns {void} 62 | */ 63 | apply(compiler: Compiler): void; 64 | } 65 | declare namespace HtmlMinimizerPlugin { 66 | export { 67 | htmlMinifierTerser, 68 | swcMinify, 69 | swcMinifyFragment, 70 | minifyHtmlNode, 71 | Schema, 72 | Compiler, 73 | Compilation, 74 | WebpackError, 75 | Asset, 76 | JestWorker, 77 | Rule, 78 | Rules, 79 | EXPECTED_ANY, 80 | Warning, 81 | WarningObject, 82 | ErrorObject, 83 | MinimizedResultObj, 84 | MinimizedResult, 85 | Input, 86 | CustomOptions, 87 | InferDefaultType, 88 | MinimizerOptions, 89 | BasicMinimizerImplementation, 90 | MinimizeFunctionHelpers, 91 | MinimizerImplementation, 92 | InternalOptions, 93 | InternalResult, 94 | MinimizerWorker, 95 | Parallel, 96 | BasePluginOptions, 97 | InternalPluginOptions, 98 | DefinedDefaultMinimizerAndOptions, 99 | PluginOptions, 100 | }; 101 | } 102 | import { htmlMinifierTerser } from "./utils"; 103 | import { swcMinify } from "./utils"; 104 | import { swcMinifyFragment } from "./utils"; 105 | import { minifyHtmlNode } from "./utils"; 106 | type Schema = import("schema-utils/declarations/validate").Schema; 107 | type Compiler = import("webpack").Compiler; 108 | type Compilation = import("webpack").Compilation; 109 | type WebpackError = import("webpack").WebpackError; 110 | type Asset = import("webpack").Asset; 111 | type JestWorker = import("jest-worker").Worker; 112 | type Rule = RegExp | string; 113 | type Rules = Rule[] | Rule; 114 | type EXPECTED_ANY = any; 115 | type Warning = 116 | | (Error & { 117 | plugin?: string; 118 | text?: string; 119 | source?: string; 120 | }) 121 | | string; 122 | type WarningObject = { 123 | /** 124 | * The warning message 125 | */ 126 | message: string; 127 | /** 128 | * The plugin name 129 | */ 130 | plugin?: string | undefined; 131 | /** 132 | * The text content 133 | */ 134 | text?: string | undefined; 135 | /** 136 | * The line number 137 | */ 138 | line?: number | undefined; 139 | /** 140 | * The column number 141 | */ 142 | column?: number | undefined; 143 | }; 144 | type ErrorObject = { 145 | /** 146 | * The error message 147 | */ 148 | message: string; 149 | /** 150 | * The line number 151 | */ 152 | line?: number | undefined; 153 | /** 154 | * The column number 155 | */ 156 | column?: number | undefined; 157 | /** 158 | * The error stack trace 159 | */ 160 | stack?: string | undefined; 161 | }; 162 | type MinimizedResultObj = { 163 | /** 164 | * The minimized code 165 | */ 166 | code: string; 167 | /** 168 | * Array of errors 169 | */ 170 | errors?: Array | undefined; 171 | /** 172 | * Array of warnings 173 | */ 174 | warnings?: Array | undefined; 175 | }; 176 | type MinimizedResult = MinimizedResultObj | string; 177 | type Input = { 178 | [file: string]: string; 179 | }; 180 | type CustomOptions = { 181 | [key: string]: EXPECTED_ANY; 182 | }; 183 | type InferDefaultType = T extends infer U ? U : CustomOptions; 184 | type MinimizerOptions = T extends any[] 185 | ? { [P in keyof T]?: InferDefaultType } 186 | : InferDefaultType; 187 | type BasicMinimizerImplementation = ( 188 | input: Input, 189 | minifyOptions: InferDefaultType, 190 | ) => Promise | MinimizedResult; 191 | type MinimizeFunctionHelpers = { 192 | /** 193 | * - Function to check if worker threads are supported 194 | */ 195 | supportsWorkerThreads?: (() => boolean | undefined) | undefined; 196 | }; 197 | type MinimizerImplementation = T extends any[] 198 | ? { 199 | [P in keyof T]: BasicMinimizerImplementation & 200 | MinimizeFunctionHelpers; 201 | } 202 | : BasicMinimizerImplementation & MinimizeFunctionHelpers; 203 | type InternalOptions = { 204 | /** 205 | * The name of the minimizer 206 | */ 207 | name: string; 208 | /** 209 | * The input content 210 | */ 211 | input: string; 212 | /** 213 | * The minimizer configuration 214 | */ 215 | minimizer: { 216 | implementation: MinimizerImplementation; 217 | options: MinimizerOptions; 218 | }; 219 | }; 220 | type InternalResult = { 221 | /** 222 | * Array of output objects 223 | */ 224 | outputs: Array<{ 225 | code: string; 226 | }>; 227 | /** 228 | * Array of warnings 229 | */ 230 | warnings: Array; 231 | /** 232 | * Array of errors 233 | */ 234 | errors: Array; 235 | }; 236 | type MinimizerWorker = JestWorker & { 237 | transform: (options: string) => Promise; 238 | minify: (options: InternalOptions) => Promise; 239 | }; 240 | type Parallel = undefined | boolean | number; 241 | type BasePluginOptions = { 242 | /** 243 | * Test rule for files to process 244 | */ 245 | test?: Rule | undefined; 246 | /** 247 | * Include rule for files to process 248 | */ 249 | include?: Rule | undefined; 250 | /** 251 | * Exclude rule for files to process 252 | */ 253 | exclude?: Rule | undefined; 254 | /** 255 | * Parallel processing configuration 256 | */ 257 | parallel?: Parallel | undefined; 258 | }; 259 | type InternalPluginOptions = BasePluginOptions & { 260 | minimizer: { 261 | implementation: MinimizerImplementation; 262 | options: MinimizerOptions; 263 | }; 264 | }; 265 | type DefinedDefaultMinimizerAndOptions = 266 | T extends import("html-minifier-terser").Options 267 | ? { 268 | minify?: MinimizerImplementation | undefined; 269 | minimizerOptions?: MinimizerOptions | undefined; 270 | } 271 | : { 272 | minify: MinimizerImplementation; 273 | minimizerOptions?: MinimizerOptions | undefined; 274 | }; 275 | type PluginOptions = 276 | BasePluginOptions & DefinedDefaultMinimizerAndOptions; 277 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | /** @typedef {import("./index.js").MinimizedResult} MinimizedResult */ 2 | /** @typedef {import("./index.js").CustomOptions} CustomOptions */ 3 | /** @typedef {import("./index.js").Input} Input */ 4 | // eslint-disable-next-line jsdoc/no-restricted-syntax 5 | /** @typedef {any} EXPECTED_ANY */ 6 | 7 | const notSettled = Symbol("not-settled"); 8 | 9 | /** 10 | * @template T 11 | * @typedef {() => Promise} Task 12 | */ 13 | 14 | /** 15 | * Run tasks with limited concurrency. 16 | * @template T 17 | * @param {number} limit Limit of tasks that run at once. 18 | * @param {Task[]} tasks List of tasks to run. 19 | * @returns {Promise} A promise that fulfills to an array of the results 20 | */ 21 | function throttleAll(limit, tasks) { 22 | return new Promise((resolve, reject) => { 23 | const result = Array.from({ length: tasks.length }).fill(notSettled); 24 | 25 | const entries = tasks.entries(); 26 | 27 | const next = () => { 28 | const { done, value } = entries.next(); 29 | 30 | if (done) { 31 | const isLast = !result.includes(notSettled); 32 | 33 | if (isLast) resolve(/** @type {T[]} */ (result)); 34 | 35 | return; 36 | } 37 | 38 | const [index, task] = value; 39 | 40 | /** 41 | * @param {T} value The resolved value 42 | */ 43 | const onFulfilled = (value) => { 44 | result[index] = value; 45 | next(); 46 | }; 47 | 48 | task().then(onFulfilled, reject); 49 | }; 50 | 51 | for (const _ of Array.from({ length: limit }, () => 0)) { 52 | next(); 53 | } 54 | }); 55 | } 56 | 57 | /** 58 | * @param {Input} input The input to minify 59 | * @param {CustomOptions=} minimizerOptions The minimizer options 60 | * @returns {Promise} Promise that resolves to the minified result 61 | */ 62 | /* istanbul ignore next */ 63 | /** 64 | * @param {Input} input The input to minify 65 | * @param {CustomOptions=} minimizerOptions The minimizer options 66 | * @returns {Promise} Promise that resolves to the minified result 67 | */ 68 | async function htmlMinifierTerser(input, minimizerOptions = {}) { 69 | const htmlMinifier = require("html-minifier-terser"); 70 | 71 | const [[, code]] = Object.entries(input); 72 | /** @type {import("html-minifier-terser").Options} */ 73 | const defaultMinimizerOptions = { 74 | caseSensitive: true, 75 | // `collapseBooleanAttributes` is not always safe, since this can break CSS attribute selectors and not safe for XHTML 76 | collapseWhitespace: true, 77 | conservativeCollapse: true, 78 | keepClosingSlash: true, 79 | // We need ability to use cssnano, or setup own function without extra dependencies 80 | minifyCSS: true, 81 | minifyJS: true, 82 | // `minifyURLs` is unsafe, because we can't guarantee what the base URL is 83 | // `removeAttributeQuotes` is not safe in some rare cases, also HTML spec recommends against doing this 84 | removeComments: true, 85 | // `removeEmptyAttributes` is not safe, can affect certain style or script behavior, look at https://github.com/webpack-contrib/html-loader/issues/323 86 | // `removeRedundantAttributes` is not safe, can affect certain style or script behavior, look at https://github.com/webpack-contrib/html-loader/issues/323 87 | removeScriptTypeAttributes: true, 88 | removeStyleLinkTypeAttributes: true, 89 | // `useShortDoctype` is not safe for XHTML 90 | }; 91 | const result = await htmlMinifier.minify(code, { 92 | ...defaultMinimizerOptions, 93 | ...minimizerOptions, 94 | }); 95 | 96 | return { code: result }; 97 | } 98 | 99 | /** 100 | * @returns {boolean} Whether worker threads are supported 101 | */ 102 | htmlMinifierTerser.supportsWorkerThreads = () => true; 103 | 104 | /** 105 | * @param {Input} input The input to minify 106 | * @param {CustomOptions=} minimizerOptions The minimizer options 107 | * @returns {Promise} Promise that resolves to the minified result 108 | */ 109 | /* istanbul ignore next */ 110 | /** 111 | * @param {Input} input The input to minify 112 | * @param {CustomOptions=} minimizerOptions The minimizer options 113 | * @returns {Promise} Promise that resolves to the minified result 114 | */ 115 | async function minifyHtmlNode(input, minimizerOptions = {}) { 116 | const minifyHtmlPkg = require("@minify-html/node"); 117 | 118 | const [[, code]] = Object.entries(input); 119 | const options = 120 | /** @type {Parameters[1]} */ ({ 121 | ...minimizerOptions, 122 | }); 123 | const result = await minifyHtmlPkg.minify(Buffer.from(code), options); 124 | 125 | return { code: result.toString() }; 126 | } 127 | 128 | /** 129 | * @returns {boolean} Whether worker threads are supported 130 | */ 131 | minifyHtmlNode.supportsWorkerThreads = () => false; 132 | 133 | /** 134 | * @param {Input} input The input to minify 135 | * @param {CustomOptions=} minimizerOptions The minimizer options 136 | * @returns {Promise} Promise that resolves to the minified result 137 | */ 138 | /* istanbul ignore next */ 139 | /** 140 | * @param {Input} input The input to minify 141 | * @param {CustomOptions=} minimizerOptions The minimizer options 142 | * @returns {Promise} Promise that resolves to the minified result 143 | */ 144 | async function swcMinify(input, minimizerOptions = {}) { 145 | const swcMinifier = require("@swc/html"); 146 | 147 | const [[, code]] = Object.entries(input); 148 | const options = /** @type {import("@swc/html").Options} */ ({ 149 | ...minimizerOptions, 150 | }); 151 | const result = await swcMinifier.minify(Buffer.from(code), options); 152 | 153 | return { 154 | code: result.code, 155 | errors: result.errors 156 | ? result.errors.map((diagnostic) => { 157 | const error = 158 | /** @type {Error & { span: EXPECTED_ANY; level: EXPECTED_ANY }} */ ( 159 | new Error(diagnostic.message) 160 | ); 161 | 162 | error.span = diagnostic.span; 163 | 164 | error.level = diagnostic.level; 165 | 166 | return error; 167 | }) 168 | : undefined, 169 | }; 170 | } 171 | 172 | /** 173 | * @returns {boolean} Whether worker threads are supported 174 | */ 175 | swcMinify.supportsWorkerThreads = () => false; 176 | 177 | /** 178 | * @param {Input} input The input to minify 179 | * @param {CustomOptions=} minimizerOptions The minimizer options 180 | * @returns {Promise} Promise that resolves to the minified result 181 | */ 182 | /* istanbul ignore next */ 183 | /** 184 | * @param {Input} input The input to minify 185 | * @param {CustomOptions=} minimizerOptions The minimizer options 186 | * @returns {Promise} 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 |
  1. Coffee
  2. 30 |
  3. Tea
  4. 31 |
  5. Milk
  6. 32 |
33 | 34 | 35 | 36 | ", 37 | "parallel/foo-1.html": " Cache-1

My First Heading

My first paragraph.

An Unordered HTML List

  • Coffee
  • Tea
  • Milk

An Ordered HTML List

  1. Coffee
  2. Tea
  3. Milk
", 38 | "parallel/foo-2.html": " Cache-2

My First Heading

My first paragraph.

An Unordered HTML List

  • Coffee
  • Tea
  • Milk

An Ordered HTML List

  1. Coffee
  2. Tea
  3. Milk
", 39 | "parallel/foo-3.html": " Cache-3

My First Heading

My first paragraph.

An Unordered HTML List

  • Coffee
  • Tea
  • Milk

An Ordered HTML List

  1. Coffee
  2. Tea
  3. 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 |
  1. Coffee
  2. 65 |
  3. Tea
  4. 66 |
  5. Milk
  6. 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

  • Coffee
  • Tea
  • Milk

An Ordered HTML List

  1. Coffee
  2. Tea
  3. Milk
", 82 | "parallel/foo-1.html": " Cache-1

My First Heading

My first paragraph.

An Unordered HTML List

  • Coffee
  • Tea
  • Milk

An Ordered HTML List

  1. Coffee
  2. Tea
  3. Milk
", 83 | "parallel/foo-2.html": " Cache-2

My First Heading

My first paragraph.

An Unordered HTML List

  • Coffee
  • Tea
  • Milk

An Ordered HTML List

  1. Coffee
  2. Tea
  3. 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 |
  1. Coffee
  2. 109 |
  3. Tea
  4. 110 |
  5. Milk
  6. 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 |
  1. Coffee
  2. 141 |
  3. Tea
  4. 142 |
  5. Milk
  6. 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

  • Coffee
  • Tea
  • Milk

An Ordered HTML List

  1. Coffee
  2. Tea
  3. Milk
", 158 | "parallel/foo-1.html": " Cache-1

My First Heading

My first paragraph.

An Unordered HTML List

  • Coffee
  • Tea
  • Milk

An Ordered HTML List

  1. Coffee
  2. Tea
  3. Milk
", 159 | "parallel/foo-2.html": " Cache-2

My First Heading

My first paragraph.

An Unordered HTML List

  • Coffee
  • Tea
  • Milk

An Ordered HTML List

  1. Coffee
  2. Tea
  3. Milk
", 160 | "parallel/foo-3.html": " Cache-3

My First Heading

My first paragraph.

An Unordered HTML List

  • Coffee
  • Tea
  • Milk

An Ordered HTML List

  1. Coffee
  2. Tea
  3. Milk
", 161 | "parallel/foo-4.html": " Cache-4

My First Heading

My first paragraph.

An Unordered HTML List

  • Coffee
  • Tea
  • Milk

An Ordered HTML List

  1. Coffee
  2. Tea
  3. 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

  • Coffee
  • Tea
  • Milk

An Ordered HTML List

  1. Coffee
  2. Tea
  3. 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

  • Coffee
  • Tea
  • Milk

An Ordered HTML List

  1. Coffee
  2. Tea
  3. 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

  • Coffee
  • Tea
  • Milk

An Ordered HTML List

  1. Coffee
  2. Tea
  3. 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

  • Coffee
  • Tea
  • Milk

An Ordered HTML List

  1. Coffee
  2. Tea
  3. 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": "Document

My First Heading

My first paragraph.

An Unordered HTML List

  • Coffee
  • Tea
  • Milk

An Ordered HTML List

  1. Coffee
  2. Tea
  3. 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": "Document

My First Heading

My first paragraph.

An Unordered HTML List

  • Coffee
  • Tea
  • Milk

An Ordered HTML List

  1. Coffee
  2. Tea
  3. 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": "Document

My 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 |
  1. Coffee
  2. 168 |
  3. Tea
  4. 169 |
  5. Milk
  6. 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
  • test
  • test
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 |
2 | 3 | 4 | 5 |
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 | --------------------------------------------------------------------------------