├── .husky ├── pre-commit └── commit-msg ├── test ├── fixtures │ ├── loader-test.unknown │ ├── loader-test.txt │ ├── empty-entry.js │ ├── entry-with-css.js │ ├── simple-emit.js │ ├── simple.js │ ├── child-compilation.js │ ├── loader-test.css │ ├── loader-test.json │ ├── generator.js │ ├── nested │ │ ├── deep │ │ │ ├── loader-test.json │ │ │ ├── loader-test.svg │ │ │ ├── loader-test.gif │ │ │ ├── loader-test.jpg │ │ │ ├── loader-test.png │ │ │ ├── plugin-test.png │ │ │ └── loader.js │ │ └── multiple-loader-test-4.svg │ ├── plugin-test.svg │ ├── single-image-loader.js │ ├── generator-and-minimizer-4.js │ ├── generator-and-minimizer-5.js │ ├── generator-and-minimizer-7.js │ ├── loader-single.js │ ├── generator-and-minimizer-3.js │ ├── svgo-prefix-id.js │ ├── loader-corrupted.js │ ├── generator-asset-modules.js │ ├── minimizer-only-corrupted.js │ ├── multiple-entry.js │ ├── url.png │ ├── multiple-entry-2.js │ ├── newImg.png │ ├── generator-and-minimizer-animation.js │ ├── loader-test.gif │ ├── loader-test.jpg │ ├── loader-test.png │ ├── plugin-test.gif │ ├── plugin-test.jpg │ ├── plugin-test.png │ ├── animation-test.gif │ ├── test-corrupted.jpg │ ├── asset-inline.js │ ├── child-compilation-image.png │ ├── svg-and-jpg.js │ ├── unknown-and-jpg.js │ ├── loader-test.svg │ ├── loader.js │ ├── test-corrupted.svg │ ├── minimizer-only.js │ ├── validate-options.js │ ├── large-after-optimization.svg │ ├── loader-with-child.js │ ├── svgo-id.svg │ ├── generator-and-minimizer.js │ ├── loader-other-imports.js │ ├── loader-other-imports-1.js │ ├── cache │ │ └── absolute-url │ │ │ ├── https_upload.wikimedia.org │ │ │ ├── wikipedia_commons_7_70_Example_as_webp_186c80a4861cd02a331c.png │ │ │ └── wikipedia_commons_4_47_PNG_transparency_demonstration_1_foo_bar_as_webp_1a8ac3df16c88ee1d867.png │ │ │ ├── https_cdn.jsdelivr.net │ │ │ └── gh_webpack_media_e7485eb2_logo_icon_bddc4456f7d21daf0df8.svg │ │ │ └── lock.json │ ├── multiple-loader-test-2.svg │ ├── multiple-loader-test-4.svg │ ├── multiple-plugin-test-2.svg │ ├── multiple-plugin-test-4.svg │ ├── emitAssetLoader.js │ ├── style.css │ ├── generator-and-minimizer-resize-query.js │ ├── generator-and-minimizer-percent-resize-query.js │ ├── svg-with-id.svg │ ├── multiple-loader-test-1.svg │ ├── multiple-loader-test-3.svg │ ├── multiple-plugin-test-1.svg │ ├── multiple-plugin-test-3.svg │ ├── EmitWepbackPlugin.js │ └── emit-asset-in-child-compilation-loader.js ├── .eslintrc.json ├── imagemin-base64.js ├── plugin-loader-option.test.js ├── __snapshots__ │ ├── ImageminPlugin.test.js.snap │ ├── api.test.js.snap │ ├── validate-plugin-options.test.js.snap │ └── validate-loader-options.test.js.snap ├── api.test.js ├── utils.test.js ├── loader-severityError-option.test.js ├── validate-loader-options.test.js ├── plugin-deleteOriginalAssets-option.test.js ├── plugin-severityError-option.test.js ├── helpers.js ├── loader-minimizer-option.test.js └── loader-generator-option.test.js ├── src ├── squoosh-lib.d.ts ├── loader-options.json ├── worker.js ├── plugin-options.json └── loader.js ├── .gitattributes ├── .remarkrc.js ├── .prettierignore ├── jest.config.js ├── lint-staged.config.js ├── commitlint.config.js ├── .editorconfig ├── .gitignore ├── .github └── workflows │ ├── dependency-review.yml │ └── nodejs.yml ├── babel.config.js ├── eslint.config.mjs ├── tsconfig.json ├── types ├── worker.d.ts ├── loader.d.ts ├── utils.d.ts └── index.d.ts ├── LICENSE ├── .cspell.json ├── package.json └── CHANGELOG.md /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | lint-staged 2 | -------------------------------------------------------------------------------- /test/fixtures/loader-test.unknown: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/loader-test.txt: -------------------------------------------------------------------------------- 1 | TEXT 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | commitlint --edit $1 2 | -------------------------------------------------------------------------------- /test/fixtures/empty-entry.js: -------------------------------------------------------------------------------- 1 | export default 1; 2 | -------------------------------------------------------------------------------- /src/squoosh-lib.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@squoosh/lib"; 2 | -------------------------------------------------------------------------------- /test/fixtures/entry-with-css.js: -------------------------------------------------------------------------------- 1 | import "./style.css"; 2 | -------------------------------------------------------------------------------- /test/fixtures/simple-emit.js: -------------------------------------------------------------------------------- 1 | export default 'foo'; 2 | -------------------------------------------------------------------------------- /test/fixtures/simple.js: -------------------------------------------------------------------------------- 1 | require("./loader-test.jpg"); 2 | -------------------------------------------------------------------------------- /test/fixtures/child-compilation.js: -------------------------------------------------------------------------------- 1 | export default "foobar"; 2 | -------------------------------------------------------------------------------- /test/fixtures/loader-test.css: -------------------------------------------------------------------------------- 1 | a { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/loader-test.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "bar" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/generator.js: -------------------------------------------------------------------------------- 1 | require("./loader-test.png?as=webp"); 2 | -------------------------------------------------------------------------------- /test/fixtures/nested/deep/loader-test.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "bar" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/plugin-test.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/single-image-loader.js: -------------------------------------------------------------------------------- 1 | require("./loader-test.jpg?as=webp"); 2 | -------------------------------------------------------------------------------- /test/fixtures/nested/deep/loader-test.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/generator-and-minimizer-4.js: -------------------------------------------------------------------------------- 1 | require("./loader-test.png?as=avif"); 2 | -------------------------------------------------------------------------------- /test/fixtures/generator-and-minimizer-5.js: -------------------------------------------------------------------------------- 1 | require("./loader-test.txt?as=avif"); 2 | -------------------------------------------------------------------------------- /test/fixtures/generator-and-minimizer-7.js: -------------------------------------------------------------------------------- 1 | require("./loader-test.svg?as=webp"); 2 | -------------------------------------------------------------------------------- /test/fixtures/loader-single.js: -------------------------------------------------------------------------------- 1 | require("./nested/deep/loader-test.jpg?as=webp"); 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | bin/* eol=lf 3 | yarn.lock -diff 4 | package-lock.json -diff -------------------------------------------------------------------------------- /.remarkrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ["remark-preset-lint-itgalaxy"], 3 | }; 4 | -------------------------------------------------------------------------------- /test/fixtures/generator-and-minimizer-3.js: -------------------------------------------------------------------------------- 1 | require("./loader-test.png?as=webp-other"); 2 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "sourceType": "module" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/svgo-prefix-id.js: -------------------------------------------------------------------------------- 1 | console.log(new URL("../fixtures/svgo-id.svg", import.meta.url)); 2 | -------------------------------------------------------------------------------- /test/fixtures/loader-corrupted.js: -------------------------------------------------------------------------------- 1 | require("./test-corrupted.jpg"); 2 | require("./loader-test.png"); 3 | -------------------------------------------------------------------------------- /test/fixtures/generator-asset-modules.js: -------------------------------------------------------------------------------- 1 | console.log(new URL("../fixtures/loader-test.png?as=webp", import.meta.url)); -------------------------------------------------------------------------------- /test/fixtures/minimizer-only-corrupted.js: -------------------------------------------------------------------------------- 1 | require("./test-corrupted.jpg"); 2 | require("./test-corrupted.svg"); 3 | -------------------------------------------------------------------------------- /test/fixtures/multiple-entry.js: -------------------------------------------------------------------------------- 1 | import "./multiple-loader-test-1.svg"; 2 | import "./multiple-loader-test-2.svg"; 3 | -------------------------------------------------------------------------------- /test/fixtures/url.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webpack/image-minimizer-webpack-plugin/HEAD/test/fixtures/url.png -------------------------------------------------------------------------------- /test/fixtures/multiple-entry-2.js: -------------------------------------------------------------------------------- 1 | import "./multiple-loader-test-3.svg"; 2 | import "./multiple-loader-test-4.svg"; 3 | -------------------------------------------------------------------------------- /test/fixtures/newImg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webpack/image-minimizer-webpack-plugin/HEAD/test/fixtures/newImg.png -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /dist 3 | /node_modules 4 | /test/fixtures 5 | /test/outputs 6 | /test/bundled 7 | CHANGELOG.md 8 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: "node", 3 | collectCoverageFrom: ["src/**/*.{js,mjs,jsx}"], 4 | }; 5 | -------------------------------------------------------------------------------- /test/fixtures/generator-and-minimizer-animation.js: -------------------------------------------------------------------------------- 1 | require("./animation-test.gif"); 2 | require("./animation-test.gif?as=webp"); 3 | -------------------------------------------------------------------------------- /test/fixtures/loader-test.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webpack/image-minimizer-webpack-plugin/HEAD/test/fixtures/loader-test.gif -------------------------------------------------------------------------------- /test/fixtures/loader-test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webpack/image-minimizer-webpack-plugin/HEAD/test/fixtures/loader-test.jpg -------------------------------------------------------------------------------- /test/fixtures/loader-test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webpack/image-minimizer-webpack-plugin/HEAD/test/fixtures/loader-test.png -------------------------------------------------------------------------------- /test/fixtures/plugin-test.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webpack/image-minimizer-webpack-plugin/HEAD/test/fixtures/plugin-test.gif -------------------------------------------------------------------------------- /test/fixtures/plugin-test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webpack/image-minimizer-webpack-plugin/HEAD/test/fixtures/plugin-test.jpg -------------------------------------------------------------------------------- /test/fixtures/plugin-test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webpack/image-minimizer-webpack-plugin/HEAD/test/fixtures/plugin-test.png -------------------------------------------------------------------------------- /test/fixtures/animation-test.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webpack/image-minimizer-webpack-plugin/HEAD/test/fixtures/animation-test.gif -------------------------------------------------------------------------------- /test/fixtures/test-corrupted.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webpack/image-minimizer-webpack-plugin/HEAD/test/fixtures/test-corrupted.jpg -------------------------------------------------------------------------------- /test/fixtures/asset-inline.js: -------------------------------------------------------------------------------- 1 | import imageSvg from "./loader-test.svg"; 2 | 3 | console.log(imageSvg) 4 | 5 | export default imageSvg; 6 | -------------------------------------------------------------------------------- /test/fixtures/child-compilation-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webpack/image-minimizer-webpack-plugin/HEAD/test/fixtures/child-compilation-image.png -------------------------------------------------------------------------------- /test/fixtures/nested/deep/loader-test.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webpack/image-minimizer-webpack-plugin/HEAD/test/fixtures/nested/deep/loader-test.gif -------------------------------------------------------------------------------- /test/fixtures/nested/deep/loader-test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webpack/image-minimizer-webpack-plugin/HEAD/test/fixtures/nested/deep/loader-test.jpg -------------------------------------------------------------------------------- /test/fixtures/nested/deep/loader-test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webpack/image-minimizer-webpack-plugin/HEAD/test/fixtures/nested/deep/loader-test.png -------------------------------------------------------------------------------- /test/fixtures/nested/deep/plugin-test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webpack/image-minimizer-webpack-plugin/HEAD/test/fixtures/nested/deep/plugin-test.png -------------------------------------------------------------------------------- /test/fixtures/svg-and-jpg.js: -------------------------------------------------------------------------------- 1 | console.log(new URL("../fixtures/loader-test.svg", import.meta.url)); 2 | console.log(new URL("../fixtures/loader-test.jpg", import.meta.url)); 3 | -------------------------------------------------------------------------------- /test/fixtures/unknown-and-jpg.js: -------------------------------------------------------------------------------- 1 | console.log(new URL("../fixtures/loader-test.unknown", import.meta.url)); 2 | console.log(new URL("../fixtures/loader-test.jpg", import.meta.url)); 3 | -------------------------------------------------------------------------------- /test/fixtures/loader-test.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /test/fixtures/loader.js: -------------------------------------------------------------------------------- 1 | require("./loader-test.gif"); 2 | require("./loader-test.jpg"); 3 | require("./loader-test.png"); 4 | require("./loader-test.svg"); 5 | require("./loader-test.json"); 6 | -------------------------------------------------------------------------------- /test/fixtures/test-corrupted.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 2 | 3 | Sorry, your browser does not support inline SVG. 4 | 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/fixtures/loader-with-child.js: -------------------------------------------------------------------------------- 1 | require("./loader-test.gif"); 2 | require("./loader-test.jpg"); 3 | require("./loader-test.png"); 4 | require("./loader-test.svg"); 5 | require("./loader-test.json"); 6 | require("./child-compilation"); 7 | -------------------------------------------------------------------------------- /test/fixtures/svgo-id.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /test/fixtures/generator-and-minimizer.js: -------------------------------------------------------------------------------- 1 | require("./loader-test.gif"); 2 | require("./loader-test.jpg"); 3 | require("./loader-test.png"); 4 | require("./loader-test.svg"); 5 | require("./loader-test.json"); 6 | require("./loader-test.png?as=webp"); 7 | -------------------------------------------------------------------------------- /.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/imagemin-base64.js: -------------------------------------------------------------------------------- 1 | module.exports = () => (buffer) => { 2 | if (!Buffer.isBuffer(buffer)) { 3 | return Promise.reject(new TypeError("Expected a buffer")); 4 | } 5 | 6 | return Promise.resolve(Buffer.from(buffer.toString("base64"))); 7 | }; 8 | -------------------------------------------------------------------------------- /test/fixtures/loader-other-imports.js: -------------------------------------------------------------------------------- 1 | require("./loader-test.gif"); 2 | require("./loader-test.jpg"); 3 | require("./loader-test.png"); 4 | require("./loader-test.svg"); 5 | require("./loader-test.css"); 6 | require("./loader-test.txt"); 7 | require("./loader-test.json"); 8 | -------------------------------------------------------------------------------- /test/fixtures/loader-other-imports-1.js: -------------------------------------------------------------------------------- 1 | require("./loader-test.gif"); 2 | require("./loader-test.jpg"); 3 | require("./loader-test.png"); 4 | require("./loader-test.svg"); 5 | require("./loader-test.css"); 6 | require("./loader-test.txt"); 7 | require("./loader-test.json"); 8 | require("./loader-test.jpg?as=webp"); -------------------------------------------------------------------------------- /test/fixtures/cache/absolute-url/https_upload.wikimedia.org/wikipedia_commons_7_70_Example_as_webp_186c80a4861cd02a331c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webpack/image-minimizer-webpack-plugin/HEAD/test/fixtures/cache/absolute-url/https_upload.wikimedia.org/wikipedia_commons_7_70_Example_as_webp_186c80a4861cd02a331c.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .vscode 4 | logs 5 | *.log 6 | npm-debug.log* 7 | .cspellcache 8 | .eslintcache 9 | /coverage 10 | /dist 11 | /local 12 | /reports 13 | /node_modules 14 | /test/outputs 15 | /test/bundled 16 | .DS_Store 17 | Thumbs.db 18 | .idea 19 | *.iml 20 | *.sublime-project 21 | *.sublime-workspace 22 | -------------------------------------------------------------------------------- /test/fixtures/multiple-loader-test-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 410 5 | 6 | -------------------------------------------------------------------------------- /test/fixtures/multiple-loader-test-4.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 410 5 | 6 | -------------------------------------------------------------------------------- /test/fixtures/multiple-plugin-test-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 410 5 | 6 | -------------------------------------------------------------------------------- /test/fixtures/multiple-plugin-test-4.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 410 5 | 6 | -------------------------------------------------------------------------------- /test/fixtures/nested/multiple-loader-test-4.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 410 5 | 6 | -------------------------------------------------------------------------------- /test/fixtures/cache/absolute-url/https_upload.wikimedia.org/wikipedia_commons_4_47_PNG_transparency_demonstration_1_foo_bar_as_webp_1a8ac3df16c88ee1d867.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webpack/image-minimizer-webpack-plugin/HEAD/test/fixtures/cache/absolute-url/https_upload.wikimedia.org/wikipedia_commons_4_47_PNG_transparency_demonstration_1_foo_bar_as_webp_1a8ac3df16c88ee1d867.png -------------------------------------------------------------------------------- /test/fixtures/emitAssetLoader.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | export default function loader(content) { 5 | const filename = 'loader-test.jpg'; 6 | const fileContent = fs.readFileSync(path.resolve(filename)); 7 | this.emitFile(filename, fileContent); 8 | 9 | const callback = this.async(); 10 | 11 | return callback(null, content); 12 | } 13 | -------------------------------------------------------------------------------- /.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@v4 13 | - name: "Dependency Review" 14 | uses: actions/dependency-review-action@v4 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 | exclude: ["proposal-dynamic-import"], 13 | targets: { 14 | node: "18.12.0", 15 | }, 16 | }, 17 | ], 18 | ], 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "eslint/config"; 2 | import configs from "eslint-config-webpack/configs.js"; 3 | import jest from "eslint-plugin-jest"; 4 | 5 | export default defineConfig([ 6 | { 7 | extends: [configs["recommended-dirty"]], 8 | plugins: { 9 | jest, 10 | }, 11 | rules: { 12 | "jest/no-standalone-expect": [ 13 | "error", 14 | { additionalTestBlockFunctions: ["ifit"] }, 15 | ], 16 | }, 17 | }, 18 | ]); 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "module": "commonjs", 5 | "lib": ["es2023"], 6 | "allowJs": true, 7 | "checkJs": true, 8 | "strict": true, 9 | "types": ["node"], 10 | "resolveJsonModule": true, 11 | 12 | // `skipLibCheck` only for avoid bug of `svgo` v3.0.0 13 | // see: https://github.com/svg/svgo/issues/1700 14 | "skipLibCheck": true, 15 | 16 | "outDir": "./types" 17 | }, 18 | "include": ["./src/**/*"] 19 | } 20 | -------------------------------------------------------------------------------- /test/fixtures/style.css: -------------------------------------------------------------------------------- 1 | a { 2 | background: url("./url.png"); 3 | background: url("./url.png?as=webp"); 4 | } 5 | 6 | body { 7 | content: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2020%2020%27%3E%3Cpath%20d%3D%27M14.83%204.89l1.34.94-5.81%208.38H9.02L5.78%209.67l1.34-1.25%202.57%202.4z%27%20fill%3D%27#000%27%2F%3E%3C%2Fsvg%3E'); 8 | } 9 | 10 | body { 11 | content: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyMCAyMCc+PHBhdGggZD0nTTE0LjgzIDQuODlsMS4zNC45NC01LjgxIDguMzhIOS4wMkw1Ljc4IDkuNjdsMS4zNC0xLjI1IDIuNTcgMi40eicgZmlsbD0nIzAwMCcvPjwvc3ZnPg=='); 12 | } 13 | -------------------------------------------------------------------------------- /types/worker.d.ts: -------------------------------------------------------------------------------- 1 | export = worker; 2 | /** 3 | * @template T 4 | * @param {import("./index").InternalWorkerOptions} options worker options 5 | * @returns {Promise} worker result 6 | */ 7 | declare function worker( 8 | options: import("./index").InternalWorkerOptions, 9 | ): Promise; 10 | declare namespace worker { 11 | export { isFilenameProcessed, WorkerResult, FilenameFn }; 12 | } 13 | /** @typedef {import("./index").WorkerResult} WorkerResult */ 14 | /** @typedef {import("./index").FilenameFn} FilenameFn */ 15 | /** @type {unique symbol} */ 16 | declare const isFilenameProcessed: unique symbol; 17 | type WorkerResult = import("./index").WorkerResult; 18 | type FilenameFn = import("./index").FilenameFn; 19 | -------------------------------------------------------------------------------- /test/fixtures/cache/absolute-url/https_cdn.jsdelivr.net/gh_webpack_media_e7485eb2_logo_icon_bddc4456f7d21daf0df8.svg: -------------------------------------------------------------------------------- 1 | icon -------------------------------------------------------------------------------- /test/fixtures/generator-and-minimizer-resize-query.js: -------------------------------------------------------------------------------- 1 | require("./loader-test.png?width=100"); 2 | require("./loader-test.png?w=150"); 3 | require("./loader-test.png?height=200"); 4 | require("./loader-test.png?h=250"); 5 | require("./loader-test.png?width=300&height=auto"); 6 | require("./loader-test.png?width=auto&height=320"); 7 | require("./loader-test.png?width=350&height=350"); 8 | 9 | require("./loader-test.png?as=webp&width=100"); 10 | require("./loader-test.png?as=webp&w=150"); 11 | require("./loader-test.png?as=webp&height=200"); 12 | require("./loader-test.png?as=webp&h=250"); 13 | require("./loader-test.png?as=webp&width=300&height=auto"); 14 | require("./loader-test.png?as=webp&width=auto&height=320"); 15 | require("./loader-test.png?as=webp&width=350&height=350"); 16 | -------------------------------------------------------------------------------- /test/fixtures/generator-and-minimizer-percent-resize-query.js: -------------------------------------------------------------------------------- 1 | require("./loader-test.png?width=100&unit=percent"); 2 | require("./loader-test.png?w=150&u=percent"); 3 | require("./loader-test.png?height=200&unit=percent"); 4 | require("./loader-test.png?h=250&u=percent"); 5 | require("./loader-test.png?width=300&height=auto&unit=percent"); 6 | require("./loader-test.png?width=auto&height=320&unit=percent"); 7 | require("./loader-test.png?width=350&height=350&unit=percent"); 8 | 9 | require("./loader-test.png?as=webp&width=100&unit=percent"); 10 | require("./loader-test.png?as=webp&w=150&u=percent"); 11 | require("./loader-test.png?as=webp&height=200&unit=percent"); 12 | require("./loader-test.png?as=webp&h=250&u=percent"); 13 | require("./loader-test.png?as=webp&width=300&height=auto&unit=percent"); 14 | require("./loader-test.png?as=webp&width=auto&height=320&unit=percent"); 15 | require("./loader-test.png?as=webp&width=350&height=350&unit=percent"); 16 | -------------------------------------------------------------------------------- /test/fixtures/svg-with-id.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | referenced text 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /test/fixtures/multiple-loader-test-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/fixtures/multiple-loader-test-3.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/fixtures/multiple-plugin-test-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/fixtures/multiple-plugin-test-3.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /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. 21 | -------------------------------------------------------------------------------- /.cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2", 3 | "language": "en,en-gb", 4 | "words": [ 5 | "Squoosh", 6 | "squoosh", 7 | "ifit", 8 | "Mozjpeg", 9 | "mozjpeg", 10 | "pify", 11 | "oxipng", 12 | "pngquant", 13 | "gifsicle", 14 | "minifi", 15 | "webpz", 16 | "multipass", 17 | "MCEP", 18 | "tempy", 19 | "multipass", 20 | "zdmc", 21 | "fullhash", 22 | "emmited", 23 | "Wepback", 24 | "pathinfo", 25 | "datauri", 26 | "preproc", 27 | "heix", 28 | "hevx", 29 | "ftyp", 30 | "flif", 31 | "FUJIFILMCCD", 32 | "apng", 33 | "IHDR", 34 | "IDAT", 35 | "webp", 36 | "jpegtran", 37 | "minimication", 38 | "extname", 39 | "srcset", 40 | "webp", 41 | "optipng", 42 | "jpegtran", 43 | "libvips", 44 | "hspace", 45 | "commitlint", 46 | "nodenext" 47 | ], 48 | 49 | "ignorePaths": [ 50 | "CHANGELOG.md", 51 | "package.json", 52 | "dist/**", 53 | "**/__snapshots__/**", 54 | "package-lock.json", 55 | "types", 56 | "test/bundled", 57 | "/test/outputs", 58 | "*.webp", 59 | "*.css", 60 | "*.svg", 61 | "node_modules", 62 | "coverage", 63 | "*.log" 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /test/fixtures/EmitWepbackPlugin.js: -------------------------------------------------------------------------------- 1 | import fs from "fs/promises"; 2 | import path from "path"; 3 | 4 | export default class EmitWepbackPlugin { 5 | constructor(options = {}) { 6 | this.options = Object.assign( 7 | {}, 8 | { 9 | fileNames: ["plugin-test.jpg"], 10 | }, 11 | options 12 | ); 13 | } 14 | 15 | apply(compiler) { 16 | const plugin = { name: "EmitPlugin" }; 17 | const mapCache = new Map(); 18 | 19 | compiler.hooks.thisCompilation.tap(plugin, (compilation) => { 20 | compilation.hooks.additionalAssets.tapPromise(plugin, () => { 21 | const { fileNames } = this.options; 22 | const { RawSource } = compiler.webpack.sources; 23 | 24 | return Promise.all( 25 | fileNames.map(async (fileName) => { 26 | const filePath = path.join(__dirname, fileName); 27 | const data = await fs.readFile(filePath); 28 | 29 | let source = mapCache.get(fileName); 30 | 31 | if (!source) { 32 | source = new RawSource(data); 33 | mapCache.set(fileName, source); 34 | } 35 | 36 | compilation.emitAsset(fileName, source); 37 | }) 38 | ); 39 | }); 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/plugin-loader-option.test.js: -------------------------------------------------------------------------------- 1 | import ImageMinimizerPlugin from "../src/index.js"; 2 | import { isOptimized, plugins, runWebpack } from "./helpers"; 3 | 4 | describe("plugin loader option", () => { 5 | it("should optimizes all images (plugin standalone)", async () => { 6 | const stats = await runWebpack({ 7 | emitPlugin: true, 8 | imageminPluginOptions: { 9 | minimizer: { 10 | implementation: ImageMinimizerPlugin.imageminMinify, 11 | options: { plugins }, 12 | }, 13 | loader: false, 14 | }, 15 | }); 16 | const { compilation } = stats; 17 | const { warnings, errors } = compilation; 18 | 19 | expect(warnings).toHaveLength(0); 20 | expect(errors).toHaveLength(0); 21 | 22 | await expect(isOptimized("loader-test.gif", compilation)).resolves.toBe( 23 | true, 24 | ); 25 | await expect(isOptimized("loader-test.jpg", compilation)).resolves.toBe( 26 | true, 27 | ); 28 | await expect(isOptimized("loader-test.png", compilation)).resolves.toBe( 29 | true, 30 | ); 31 | await expect(isOptimized("loader-test.svg", compilation)).resolves.toBe( 32 | true, 33 | ); 34 | await expect(isOptimized("plugin-test.jpg", compilation)).resolves.toBe( 35 | true, 36 | ); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/fixtures/cache/absolute-url/lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "https://cdn.rawgit.com/webpack/media/e7485eb2/logo/icon.svg": { "resolved": "https://cdn.jsdelivr.net/gh/webpack/media@e7485eb2/logo/icon.svg", "integrity": "sha512-DyC5VS91HRdJSZ1G8BOJCzXEZfQCmBAu0W2v8tcJNq3bvmXrL8vdFqDpFNrI/SL7qLBSV7DiiR0rLZcb1FGebA==", "contentType": "image/svg+xml" }, 3 | "https://cdn.rawgit.com/webpack/media/e7485eb2/logo/icon.svg?as=webp": { "resolved": "https://cdn.jsdelivr.net/gh/webpack/media@e7485eb2/logo/icon.svg", "integrity": "sha512-DyC5VS91HRdJSZ1G8BOJCzXEZfQCmBAu0W2v8tcJNq3bvmXrL8vdFqDpFNrI/SL7qLBSV7DiiR0rLZcb1FGebA==", "contentType": "image/svg+xml" }, 4 | "https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png?foo=bar&as=webp": { "integrity": "sha512-oko0UnN7/aOH4loqI1F79zMZozo+Y5fbTYEvcfpE75439sR6INOke7En72cUAvbhcuKmmYUgZMxcGew9iBRTzQ==", "contentType": "image/png" }, 5 | "https://upload.wikimedia.org/wikipedia/commons/7/70/Example.png?as=webp": { "integrity": "sha512-5rIlhMQXmv1/+wxg+Xl12smI+bkT84f70stvfyIdKth6Nh75eM+Cyfl3mIUoiFwIaQeIXCGgqUPOceyBgZmvmg==", "contentType": "image/png" }, 6 | "https://upload.wikimedia.org/wikipedia/commons/7/70/Example.png?foo=bar&as=webp": { "integrity": "sha512-5rIlhMQXmv1/+wxg+Xl12smI+bkT84f70stvfyIdKth6Nh75eM+Cyfl3mIUoiFwIaQeIXCGgqUPOceyBgZmvmg==", "contentType": "image/png" }, 7 | "version": 1 8 | } 9 | -------------------------------------------------------------------------------- /types/loader.d.ts: -------------------------------------------------------------------------------- 1 | export = loader; 2 | /** 3 | * @template T 4 | * @this {import("webpack").LoaderContext>} 5 | * @param {Buffer} content content 6 | * @returns {Promise} processed content 7 | */ 8 | declare function loader( 9 | this: import("webpack").LoaderContext>, 10 | content: Buffer, 11 | ): Promise; 12 | declare namespace loader { 13 | export { 14 | raw, 15 | Schema, 16 | Compilation, 17 | WorkerResult, 18 | Minimizer, 19 | Generator, 20 | LoaderOptions, 21 | }; 22 | } 23 | declare var raw: boolean; 24 | type Schema = import("schema-utils/declarations/validate").Schema; 25 | type Compilation = import("webpack").Compilation; 26 | type WorkerResult = import("./utils").WorkerResult; 27 | /** 28 | * 29 | */ 30 | type Minimizer = import("./index").Minimizer; 31 | /** 32 | * 33 | */ 34 | type Generator = import("./index").Generator; 35 | /** 36 | * 37 | */ 38 | type LoaderOptions = { 39 | /** 40 | * allows to choose how errors are displayed. 41 | */ 42 | severityError?: string | undefined; 43 | /** 44 | * minimizer configuration 45 | */ 46 | minimizer?: (Minimizer | Minimizer[]) | undefined; 47 | /** 48 | * generator configuration 49 | */ 50 | generator?: Generator[] | undefined; 51 | }; 52 | -------------------------------------------------------------------------------- /test/fixtures/emit-asset-in-child-compilation-loader.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | 4 | // eslint-disable-next-line node/no-sync 5 | const imageContent = fs.readFileSync( 6 | path.resolve(__dirname, "./child-compilation-image.png") 7 | ); 8 | 9 | class ChildCompilationPlugin { 10 | constructor(options = {}) { 11 | this.options = options.options || {}; 12 | } 13 | 14 | apply(compiler) { 15 | const plugin = { name: this.constructor.name }; 16 | const { RawSource } = compiler.webpack.sources; 17 | 18 | compiler.hooks.compilation.tap(plugin, (compilation) => { 19 | compilation.hooks.additionalAssets.tapAsync(plugin, (callback) => { 20 | compilation.emitAsset( 21 | "child-compilation-image.png", 22 | new RawSource(imageContent) 23 | ); 24 | 25 | callback(); 26 | }); 27 | }); 28 | } 29 | } 30 | 31 | export default function loader() { 32 | const callback = this.async(); 33 | 34 | // eslint-disable-next-line no-underscore-dangle 35 | const childCompiler = this._compilation.createChildCompiler( 36 | "Child Compilation Plugin Test", 37 | this.options 38 | ); 39 | 40 | new ChildCompilationPlugin().apply(childCompiler); 41 | 42 | childCompiler.runAsChild((error) => { 43 | if (error) { 44 | return callback(error); 45 | } 46 | 47 | return callback(null, "export default 1"); 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /test/__snapshots__/ImageminPlugin.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`imagemin plugin should work with mini-css-extract-plugin (svgoMinify): main.css 1`] = ` 4 | "a { 5 | background: url(url.png); 6 | background: url(url.webp); 7 | } 8 | 9 | body { 10 | content: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2020%2020%27%3E%3Cpath%20d%3D%27M14.83%204.89l1.34.94-5.81%208.38H9.02L5.78%209.67l1.34-1.25%202.57%202.4z%27%20fill%3D%27%23000%27%2F%3E%3C%2Fsvg%3E"); 11 | } 12 | 13 | body { 14 | content: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyMCAyMCc+PHBhdGggZD0nTTE0LjgzIDQuODlsMS4zNC45NC01LjgxIDguMzhIOS4wMkw1Ljc4IDkuNjdsMS4zNC0xLjI1IDIuNTcgMi40eicgZmlsbD0nIzAwMCcvPjwvc3ZnPg==); 15 | } 16 | 17 | " 18 | `; 19 | 20 | exports[`imagemin plugin should work with mini-css-extract-plugin: main.css 1`] = ` 21 | "a { 22 | background: url(url.png); 23 | background: url(url.webp); 24 | } 25 | 26 | body { 27 | content: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2020%2020%22%3E%3Cpath%20d%3D%22m14.83%204.89%201.34.94-5.81%208.38H9.02L5.78%209.67l1.34-1.25%202.57%202.4z%22%2F%3E%3C%2Fsvg%3E"); 28 | } 29 | 30 | body { 31 | content: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMCAyMCI+PHBhdGggZD0ibTE0LjgzIDQuODkgMS4zNC45NC01LjgxIDguMzhIOS4wMkw1Ljc4IDkuNjdsMS4zNC0xLjI1IDIuNTcgMi40eiIvPjwvc3ZnPg==); 32 | } 33 | 34 | " 35 | `; 36 | -------------------------------------------------------------------------------- /test/__snapshots__/api.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing 2 | 3 | exports[`api normalizeImageminConfig should works 1`] = `"No plugins found for \`imagemin\`, please read documentation"`; 4 | 5 | exports[`api normalizeImageminConfig should works 2`] = `"No plugins found for \`imagemin\`, please read documentation"`; 6 | 7 | exports[`api normalizeImageminConfig should works 3`] = ` 8 | "Unknown plugin: imagemin-unknown 9 | 10 | Did you forget to install the plugin? 11 | You can install it with: 12 | 13 | $ npm install imagemin-unknown --save-dev 14 | $ yarn add imagemin-unknown --dev 15 | Cause: Cannot find module 'unknown' from 'src/utils.js'" 16 | `; 17 | 18 | exports[`api normalizeImageminConfig should works 4`] = ` 19 | "Unknown plugin: imagemin-unknown 20 | 21 | Did you forget to install the plugin? 22 | You can install it with: 23 | 24 | $ npm install imagemin-unknown --save-dev 25 | $ yarn add imagemin-unknown --dev 26 | Cause: Cannot find module 'imagemin-unknown' from 'src/utils.js'" 27 | `; 28 | 29 | exports[`api normalizeImageminConfig should works 5`] = `"No plugins found for \`imagemin\`, please read documentation"`; 30 | 31 | exports[`api normalizeImageminConfig should works 6`] = `"Invalid plugin configuration 'true', plugin configuration should be 'string' or '[string, object]'""`; 32 | 33 | exports[`api normalizeImageminConfig should works 7`] = ` 34 | { 35 | "plugins": [ 36 | [Function], 37 | ], 38 | } 39 | `; 40 | 41 | exports[`api normalizeImageminConfig should works 8`] = ` 42 | { 43 | "plugins": [ 44 | [Function], 45 | ], 46 | } 47 | `; 48 | 49 | exports[`api normalizeImageminConfig should works 9`] = ` 50 | { 51 | "plugins": [ 52 | [Function], 53 | ], 54 | } 55 | `; 56 | 57 | exports[`api normalizeImageminConfig should works 10`] = ` 58 | { 59 | "plugins": [ 60 | [Function], 61 | ], 62 | } 63 | `; 64 | -------------------------------------------------------------------------------- /test/api.test.js: -------------------------------------------------------------------------------- 1 | import ImageMinimizerPlugin from "../src/index"; 2 | 3 | describe("api", () => { 4 | describe("basic", () => { 5 | it("should exported", () => { 6 | expect(ImageMinimizerPlugin).toBeInstanceOf(Object); 7 | expect(typeof ImageMinimizerPlugin.loader).toBe("string"); 8 | expect(typeof ImageMinimizerPlugin.imageminNormalizeConfig).toBe( 9 | "function", 10 | ); 11 | expect(typeof ImageMinimizerPlugin.imageminMinify).toBe("function"); 12 | expect(typeof ImageMinimizerPlugin.imageminGenerate).toBe("function"); 13 | expect(typeof ImageMinimizerPlugin.squooshMinify).toBe("function"); 14 | expect(typeof ImageMinimizerPlugin.squooshGenerate).toBe("function"); 15 | expect(typeof ImageMinimizerPlugin.sharpMinify).toBe("function"); 16 | expect(typeof ImageMinimizerPlugin.sharpGenerate).toBe("function"); 17 | }); 18 | }); 19 | 20 | describe("normalizeImageminConfig", () => { 21 | it("should works", async () => { 22 | await expect(() => 23 | ImageMinimizerPlugin.imageminNormalizeConfig({}), 24 | ).rejects.toThrowErrorMatchingSnapshot(); 25 | await expect(() => 26 | ImageMinimizerPlugin.imageminNormalizeConfig({ plugins: [] }), 27 | ).rejects.toThrowErrorMatchingSnapshot(); 28 | await expect(() => 29 | ImageMinimizerPlugin.imageminNormalizeConfig({ plugins: ["unknown"] }), 30 | ).rejects.toThrowErrorMatchingSnapshot(); 31 | await expect(() => 32 | ImageMinimizerPlugin.imageminNormalizeConfig({ 33 | plugins: ["imagemin-unknown"], 34 | }), 35 | ).rejects.toThrowErrorMatchingSnapshot(); 36 | await expect(() => 37 | ImageMinimizerPlugin.imageminNormalizeConfig({}), 38 | ).rejects.toThrowErrorMatchingSnapshot(); 39 | await expect(() => 40 | ImageMinimizerPlugin.imageminNormalizeConfig({ plugins: [true] }, {}), 41 | ).rejects.toThrowErrorMatchingSnapshot(); 42 | 43 | await expect( 44 | ImageMinimizerPlugin.imageminNormalizeConfig({ plugins: ["mozjpeg"] }), 45 | ).resolves.toMatchSnapshot(); 46 | await expect( 47 | ImageMinimizerPlugin.imageminNormalizeConfig({ 48 | plugins: ["imagemin-mozjpeg"], 49 | }), 50 | ).resolves.toMatchSnapshot(); 51 | await expect( 52 | ImageMinimizerPlugin.imageminNormalizeConfig({ 53 | plugins: [["mozjpeg"]], 54 | }), 55 | ).resolves.toMatchSnapshot(); 56 | await expect( 57 | ImageMinimizerPlugin.imageminNormalizeConfig({ 58 | plugins: [["mozjpeg", { quality: 0 }]], 59 | }), 60 | ).resolves.toMatchSnapshot(); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /test/utils.test.js: -------------------------------------------------------------------------------- 1 | import { isAbsoluteURL, replaceFileExtension } from "../src/utils.js"; 2 | 3 | describe("utils", () => { 4 | it("should distinguish between relative and absolute file paths", () => { 5 | expect(isAbsoluteURL("/home/user/img.jpg")).toBe(true); 6 | expect(isAbsoluteURL("user/img.jpg")).toBe(false); 7 | expect(isAbsoluteURL("./user/img.jpg")).toBe(false); 8 | expect(isAbsoluteURL("../user/img.jpg")).toBe(false); 9 | 10 | expect(isAbsoluteURL("C:\\user\\img.jpg")).toBe(true); 11 | expect(isAbsoluteURL("CC:\\user\\img.jpg")).toBe(true); 12 | expect(isAbsoluteURL("user\\img.jpg")).toBe(false); 13 | expect(isAbsoluteURL(".\\user\\img.jpg")).toBe(false); 14 | expect(isAbsoluteURL("..\\user\\img.jpg")).toBe(false); 15 | 16 | expect(isAbsoluteURL("file:/user/img.jpg")).toBe(true); 17 | expect(isAbsoluteURL("file-url:/user/img.jpg")).toBe(true); 18 | expect(isAbsoluteURL("0file:/user/img.jpg")).toBe(false); 19 | }); 20 | 21 | it("should replace file extension", () => { 22 | expect(replaceFileExtension("img.jpg", "png")).toBe("img.png"); 23 | expect(replaceFileExtension(".img.jpg", "png")).toBe(".img.png"); 24 | 25 | expect(replaceFileExtension("/user/img.jpg", "png")).toBe("/user/img.png"); 26 | expect(replaceFileExtension("file:///user/img.jpg", "png")).toBe( 27 | "file:///user/img.png", 28 | ); 29 | expect(replaceFileExtension("C:\\user\\img.jpg", "png")).toBe( 30 | "C:\\user\\img.png", 31 | ); 32 | 33 | expect(replaceFileExtension("user/img.jpg", "png")).toBe("user/img.png"); 34 | expect(replaceFileExtension("user\\img.jpg", "png")).toBe("user\\img.png"); 35 | 36 | expect(replaceFileExtension("/user/img.jpg.gz", "png")).toBe( 37 | "/user/img.jpg.png", 38 | ); 39 | expect(replaceFileExtension("file:///user/img.jpg.gz", "png")).toBe( 40 | "file:///user/img.jpg.png", 41 | ); 42 | expect(replaceFileExtension("C:\\user\\img.jpg.gz", "png")).toBe( 43 | "C:\\user\\img.jpg.png", 44 | ); 45 | 46 | expect(replaceFileExtension("/user/img", "png")).toBe("/user/img"); 47 | expect(replaceFileExtension("file:///user/img", "png")).toBe( 48 | "file:///user/img", 49 | ); 50 | expect(replaceFileExtension("C:\\user\\img", "png")).toBe("C:\\user\\img"); 51 | 52 | expect(replaceFileExtension("/user/.img", "png")).toBe("/user/.png"); 53 | expect(replaceFileExtension("file:///user/.img", "png")).toBe( 54 | "file:///user/.png", 55 | ); 56 | expect(replaceFileExtension("C:\\user\\.img", "png")).toBe( 57 | "C:\\user\\.png", 58 | ); 59 | 60 | expect(replaceFileExtension("/use.r/img", "png")).toBe("/use.r/img"); 61 | expect(replaceFileExtension("file:///use.r/img", "png")).toBe( 62 | "file:///use.r/img", 63 | ); 64 | expect(replaceFileExtension("C:\\use.r\\img", "png")).toBe( 65 | "C:\\use.r\\img", 66 | ); 67 | 68 | expect(replaceFileExtension("C:\\user/img.jpg", "png")).toBe( 69 | "C:\\user/img.png", 70 | ); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: image-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@v4 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 | timeout-minutes: 10 80 | 81 | steps: 82 | - uses: actions/checkout@v4 83 | 84 | - name: Use Node.js ${{ matrix.node-version }} 85 | uses: actions/setup-node@v4 86 | with: 87 | node-version: ${{ matrix.node-version }} 88 | cache: "npm" 89 | 90 | - name: Install dependencies 91 | run: npm ci 92 | 93 | - name: Install webpack ${{ matrix.webpack-version }} 94 | if: matrix.webpack-version != 'latest' 95 | run: npm i webpack@${{ matrix.webpack-version }} 96 | 97 | - name: Run tests for webpack version ${{ matrix.webpack-version }} 98 | run: npm run test:coverage -- --ci 99 | 100 | - name: Submit coverage data to codecov 101 | uses: codecov/codecov-action@v5 102 | with: 103 | token: ${{ secrets.CODECOV_TOKEN }} 104 | -------------------------------------------------------------------------------- /src/loader-options.json: -------------------------------------------------------------------------------- 1 | { 2 | "definitions": { 3 | "Minimizer": { 4 | "type": "object", 5 | "additionalProperties": false, 6 | "properties": { 7 | "implementation": { 8 | "description": "Implementation of the minimizer function.", 9 | "instanceof": "Function" 10 | }, 11 | "options": { 12 | "description": "Options for the minimizer function.", 13 | "type": "object", 14 | "additionalProperties": true 15 | }, 16 | "filter": { 17 | "description": "Allows filtering of images.", 18 | "instanceof": "Function" 19 | }, 20 | "filename": { 21 | "description": "Allows to set the filename for the minimized asset.", 22 | "anyOf": [ 23 | { 24 | "type": "string", 25 | "minLength": 1 26 | }, 27 | { 28 | "instanceof": "Function" 29 | } 30 | ] 31 | } 32 | } 33 | }, 34 | "Generator": { 35 | "type": "object", 36 | "additionalProperties": false, 37 | "properties": { 38 | "type": { 39 | "description": "Type of generation", 40 | "enum": ["import"] 41 | }, 42 | "preset": { 43 | "description": "Name of preset, i.e. using in '?as=webp'.", 44 | "type": "string", 45 | "minLength": 1 46 | }, 47 | "implementation": { 48 | "description": "Implementation of the generator function.", 49 | "instanceof": "Function" 50 | }, 51 | "options": { 52 | "description": "Options for the generator function.", 53 | "type": "object", 54 | "additionalProperties": true 55 | }, 56 | "filter": { 57 | "description": "Allows filtering of images.", 58 | "instanceof": "Function" 59 | }, 60 | "filename": { 61 | "description": "Allows to set the filename for the minimized asset.", 62 | "anyOf": [ 63 | { 64 | "type": "string", 65 | "minLength": 1 66 | }, 67 | { 68 | "instanceof": "Function" 69 | } 70 | ] 71 | } 72 | }, 73 | "required": ["implementation", "preset"] 74 | } 75 | }, 76 | "title": "Image Minimizer Plugin Loader options", 77 | "type": "object", 78 | "additionalProperties": false, 79 | "properties": { 80 | "minimizer": { 81 | "description": "Allows you to setup the minimizer function and options.", 82 | "link": "https://github.com/webpack/image-minimizer-webpack-plugin#minimizer", 83 | "anyOf": [ 84 | { 85 | "type": "array", 86 | "minItems": 1, 87 | "items": { 88 | "$ref": "#/definitions/Minimizer" 89 | } 90 | }, 91 | { 92 | "$ref": "#/definitions/Minimizer" 93 | } 94 | ] 95 | }, 96 | "generator": { 97 | "description": "Allows you to setup the generator function and options.", 98 | "link": "https://github.com/webpack/image-minimizer-webpack-plugin#generator", 99 | "type": "array", 100 | "minItems": 1, 101 | "items": { 102 | "$ref": "#/definitions/Generator" 103 | } 104 | }, 105 | "severityError": { 106 | "description": "Allows to choose how errors are displayed.", 107 | "link": "https://github.com/webpack/image-minimizer-webpack-plugin#severityerror", 108 | "enum": ["off", "warning", "error"] 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /test/loader-severityError-option.test.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | 3 | import ImageMinimizerPlugin from "../src"; 4 | import { fixturesPath, isOptimized, plugins, runWebpack } from "./helpers"; 5 | 6 | describe("loader severityError option", () => { 7 | it("should throws error on corrupted images using `severityError` option with `error` value", async () => { 8 | const stats = await runWebpack({ 9 | entry: path.join(fixturesPath, "loader-corrupted.js"), 10 | imageminLoaderOptions: { 11 | severityError: "error", 12 | minimizer: { 13 | implementation: ImageMinimizerPlugin.imageminMinify, 14 | options: { 15 | plugins, 16 | }, 17 | }, 18 | }, 19 | }); 20 | const { compilation } = stats; 21 | const { assets, warnings, errors } = compilation; 22 | 23 | expect(warnings).toHaveLength(0); 24 | expect(errors).toHaveLength(1); 25 | expect(errors[0].message).toMatch( 26 | /(Corrupt JPEG data|Command failed with EPIPE)/, 27 | ); 28 | expect(Object.keys(assets)).toHaveLength(3); 29 | 30 | await expect(isOptimized("loader-test.png", compilation)).resolves.toBe( 31 | true, 32 | ); 33 | }); 34 | 35 | it("should throws error on corrupted images using `severityError` option with `warning` value", async () => { 36 | const stats = await runWebpack({ 37 | entry: path.join(fixturesPath, "loader-corrupted.js"), 38 | imageminLoaderOptions: { 39 | minimizer: { 40 | implementation: ImageMinimizerPlugin.imageminMinify, 41 | options: { 42 | plugins, 43 | }, 44 | }, 45 | severityError: "warning", 46 | }, 47 | }); 48 | const { compilation } = stats; 49 | const { assets, warnings, errors } = compilation; 50 | 51 | expect(warnings).toHaveLength(1); 52 | expect(errors).toHaveLength(0); 53 | expect(warnings[0].message).toMatch( 54 | /(Corrupt JPEG data|Command failed with EPIPE)/, 55 | ); 56 | expect(Object.keys(assets)).toHaveLength(3); 57 | 58 | await expect(isOptimized("loader-test.png", compilation)).resolves.toBe( 59 | true, 60 | ); 61 | }); 62 | 63 | it("should throws error on corrupted images using `severityError` option with `off` value", async () => { 64 | const stats = await runWebpack({ 65 | entry: path.join(fixturesPath, "loader-corrupted.js"), 66 | imageminLoaderOptions: { 67 | minimizer: { 68 | implementation: ImageMinimizerPlugin.imageminMinify, 69 | options: { 70 | plugins, 71 | }, 72 | }, 73 | severityError: "off", 74 | }, 75 | }); 76 | const { compilation } = stats; 77 | const { assets, warnings, errors } = compilation; 78 | 79 | expect(warnings).toHaveLength(0); 80 | expect(errors).toHaveLength(0); 81 | 82 | expect(Object.keys(assets)).toHaveLength(3); 83 | 84 | await expect(isOptimized("loader-test.png", compilation)).resolves.toBe( 85 | true, 86 | ); 87 | }); 88 | 89 | it("should throws error on corrupted images using mode `production` and `severityError` option not specify value", async () => { 90 | const stats = await runWebpack({ 91 | mode: "production", 92 | optimization: { 93 | emitOnErrors: true, 94 | }, 95 | entry: path.join(fixturesPath, "loader-corrupted.js"), 96 | imageminLoaderOptions: { 97 | minimizer: { 98 | implementation: ImageMinimizerPlugin.imageminMinify, 99 | options: { 100 | plugins, 101 | }, 102 | }, 103 | }, 104 | }); 105 | const { compilation } = stats; 106 | const { assets, warnings, errors } = compilation; 107 | 108 | expect(warnings).toHaveLength(0); 109 | expect(errors).toHaveLength(1); 110 | expect(errors[0].message).toMatch( 111 | /(Corrupt JPEG data|Command failed with EPIPE)/, 112 | ); 113 | expect(Object.keys(assets)).toHaveLength(3); 114 | 115 | await expect(isOptimized("loader-test.png", compilation)).resolves.toBe( 116 | true, 117 | ); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /src/worker.js: -------------------------------------------------------------------------------- 1 | /** @typedef {import("./index").WorkerResult} WorkerResult */ 2 | /** @typedef {import("./index").FilenameFn} FilenameFn */ 3 | 4 | /** @type {unique symbol} */ 5 | const isFilenameProcessed = Symbol("isFilenameProcessed"); 6 | 7 | /** 8 | * @template T 9 | * @param {WorkerResult} result worker result 10 | * @param {import("./index").InternalWorkerOptions} options worker options 11 | * @param {undefined | string | FilenameFn} filenameTemplate filename template 12 | */ 13 | function processFilenameTemplate(result, options, filenameTemplate) { 14 | if ( 15 | !result.info[isFilenameProcessed] && 16 | typeof filenameTemplate !== "undefined" && 17 | typeof options.generateFilename === "function" 18 | ) { 19 | result.filename = options.generateFilename(filenameTemplate, { 20 | filename: result.filename, 21 | }); 22 | 23 | result.filename = result.filename 24 | .replaceAll(/\[width\]/gi, result.info.width) 25 | .replaceAll(/\[height\]/gi, result.info.height); 26 | 27 | result.info[isFilenameProcessed] = true; 28 | } 29 | } 30 | 31 | /** 32 | * @template T 33 | * @param {WorkerResult} result worker result 34 | * @param {import("./index").InternalWorkerOptions} options worker options 35 | */ 36 | function processSeverityError(result, options) { 37 | if (options.severityError === "off") { 38 | result.warnings = []; 39 | result.errors = []; 40 | } else if (options.severityError === "warning") { 41 | result.warnings = [...result.warnings, ...result.errors]; 42 | result.errors = []; 43 | } 44 | } 45 | 46 | /** 47 | * @template T 48 | * @param {import("./index").InternalWorkerOptions} options worker options 49 | * @returns {Promise} worker result 50 | */ 51 | async function worker(options) { 52 | /** @type {WorkerResult} */ 53 | let result = { 54 | data: options.input, 55 | filename: options.filename, 56 | warnings: [], 57 | errors: [], 58 | info: { 59 | sourceFilename: 60 | options.info && 61 | typeof options.info === "object" && 62 | typeof options.info.sourceFilename === "string" 63 | ? options.info.sourceFilename 64 | : typeof options.filename === "string" 65 | ? options.filename 66 | : undefined, 67 | }, 68 | }; 69 | 70 | if (!result.data) { 71 | result.errors.push(new Error("Empty input")); 72 | return result; 73 | } 74 | 75 | const transformers = Array.isArray(options.transformer) 76 | ? options.transformer 77 | : [options.transformer]; 78 | 79 | /** @type {undefined | string | FilenameFn} */ 80 | let filenameTemplate; 81 | 82 | for (const transformer of transformers) { 83 | if ( 84 | typeof transformer.filter === "function" && 85 | // eslint-disable-next-line unicorn/no-array-method-this-argument 86 | !transformer.filter(options.input, options.filename) 87 | ) { 88 | continue; 89 | } 90 | 91 | /** @type {WorkerResult | null} */ 92 | let processedResult; 93 | 94 | try { 95 | processedResult = await transformer.implementation( 96 | result, 97 | transformer.options, 98 | ); 99 | } catch (error) { 100 | result.errors.push( 101 | error instanceof Error 102 | ? error 103 | : new Error(/** @type {string} */ (error)), 104 | ); 105 | 106 | return result; 107 | } 108 | 109 | if (processedResult && !Buffer.isBuffer(processedResult.data)) { 110 | result.errors.push( 111 | new Error( 112 | "minimizer function doesn't return the 'data' property or result is not a 'Buffer' value", 113 | ), 114 | ); 115 | 116 | return result; 117 | } 118 | 119 | if (processedResult) { 120 | result = processedResult; 121 | filenameTemplate ??= transformer.filename; 122 | } 123 | } 124 | 125 | result.info ??= {}; 126 | result.errors ??= []; 127 | result.warnings ??= []; 128 | result.filename ??= options.filename; 129 | 130 | processSeverityError(result, options); 131 | processFilenameTemplate(result, options, filenameTemplate); 132 | 133 | return result; 134 | } 135 | 136 | module.exports = worker; 137 | module.exports.isFilenameProcessed = isFilenameProcessed; 138 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "image-minimizer-webpack-plugin", 3 | "version": "4.1.4", 4 | "description": "Webpack loader and plugin to optimize (compress) images using imagemin", 5 | "keywords": [ 6 | "webpack", 7 | "loader", 8 | "plugin", 9 | "imagemin", 10 | "images", 11 | "minify", 12 | "compress", 13 | "optimize" 14 | ], 15 | "homepage": "https://github.com/webpack/image-minimizer-webpack-plugin", 16 | "bugs": "https://github.com/webpack/image-minimizer-webpack-plugin/issues", 17 | "repository": "webpack/image-minimizer-webpack-plugin", 18 | "funding": { 19 | "type": "opencollective", 20 | "url": "https://opencollective.com/webpack" 21 | }, 22 | "license": "MIT", 23 | "author": "Alexander Krasnoyarov (https://github.com/evilebottnawi)", 24 | "main": "dist/index.js", 25 | "types": "types/index.d.ts", 26 | "files": [ 27 | "dist", 28 | "types" 29 | ], 30 | "scripts": { 31 | "start": "npm run build -- -w", 32 | "clean": "del-cli dist types", 33 | "prebuild": "npm run clean", 34 | "build:types": "tsc --declaration --emitDeclarationOnly && prettier \"types/**/*.ts\" --write", 35 | "build:code": "cross-env NODE_ENV=production babel src -d dist --copy-files", 36 | "build": "npm-run-all -p \"build:**\"", 37 | "commitlint": "commitlint --from=main", 38 | "security": "npm audit --production", 39 | "lint:prettier": "prettier --cache --list-different .", 40 | "lint:code": "eslint --cache .", 41 | "lint:spelling": "cspell --cache --no-must-find-files --quiet \"**/*.*\"", 42 | "lint:types": "tsc --pretty --noEmit", 43 | "lint": "npm-run-all -l -p \"lint:**\"", 44 | "fix:code": "npm run lint:code -- --fix", 45 | "fix:prettier": "npm run lint:prettier -- --write", 46 | "fix": "npm-run-all -l fix:code fix:prettier", 47 | "test:only": "cross-env NODE_ENV=test node --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand", 48 | "test:watch": "npm run test:only -- --watch", 49 | "test:coverage": "npm run test:only -- --collectCoverageFrom=\"src/**/*.js\" --coverage", 50 | "pretest": "npm run lint", 51 | "test": "npm run test:coverage", 52 | "prepare": "husky && npm run build", 53 | "release": "standard-version" 54 | }, 55 | "overrides": { 56 | "sharp": "$sharp", 57 | "imagemin-avif": { 58 | "sharp": "$sharp" 59 | } 60 | }, 61 | "dependencies": { 62 | "schema-utils": "^4.2.0", 63 | "serialize-javascript": "^6.0.2" 64 | }, 65 | "devDependencies": { 66 | "@babel/cli": "^7.24.7", 67 | "@babel/core": "^7.25.2", 68 | "@babel/preset-env": "^7.25.3", 69 | "@commitlint/cli": "^19.3.0", 70 | "@commitlint/config-conventional": "^19.2.2", 71 | "@eslint/js": "^9.33.0", 72 | "@eslint/markdown": "^7.0.0", 73 | "@squoosh/lib": "^0.5.3", 74 | "@stylistic/eslint-plugin": "^5.2.3", 75 | "@types/imagemin": "^9.0.0", 76 | "@types/node": "^20.14.9", 77 | "@types/serialize-javascript": "^5.0.4", 78 | "copy-webpack-plugin": "^13.0.1", 79 | "cross-env": "^7.0.3", 80 | "cspell": "^8.13.1", 81 | "css-loader": "^7.1.2", 82 | "del-cli": "^6.0.0", 83 | "eslint": "^9.31.0", 84 | "eslint-config-prettier": "^10.1.8", 85 | "eslint-config-webpack": "^4.4.2", 86 | "eslint-plugin-import": "^2.32.0", 87 | "eslint-plugin-jest": "^29.0.1", 88 | "eslint-plugin-jsdoc": "^53.0.1", 89 | "eslint-plugin-n": "^17.21.0", 90 | "eslint-plugin-prettier": "^5.5.4", 91 | "eslint-plugin-unicorn": "^60.0.0", 92 | "file-loader": "^6.2.0", 93 | "file-type": "^16.5.4", 94 | "globals": "^16.3.0", 95 | "husky": "^9.1.4", 96 | "image-size": "^2.0.2", 97 | "imagemin": "^9.0.0", 98 | "imagemin-avif": "^0.1.6", 99 | "imagemin-gifsicle": "^7.0.0", 100 | "imagemin-mozjpeg": "^10.0.0", 101 | "imagemin-pngquant": "^10.0.0", 102 | "imagemin-svgo": "^11.0.1", 103 | "imagemin-webp": "^8.0.0", 104 | "jest": "^30.0.0", 105 | "lint-staged": "^15.2.8", 106 | "memfs": "^4.11.1", 107 | "mini-css-extract-plugin": "^2.9.0", 108 | "npm-run-all": "^4.1.5", 109 | "prettier": "^3.3.2", 110 | "remark-cli": "^12.0.1", 111 | "remark-preset-lint-itgalaxy": "^16.0.0", 112 | "sharp": "^0.34.3", 113 | "standard-version": "^9.5.0", 114 | "svgo": "^4.0.0", 115 | "tempy": "^3.1.0", 116 | "typescript": "^5.5.3", 117 | "typescript-eslint": "^8.38.0", 118 | "url-loader": "^4.1.1", 119 | "webpack": "^5.92.1" 120 | }, 121 | "peerDependencies": { 122 | "webpack": "^5.1.0" 123 | }, 124 | "peerDependenciesMeta": { 125 | "sharp": { 126 | "optional": true 127 | }, 128 | "@squoosh/lib": { 129 | "optional": true 130 | }, 131 | "imagemin": { 132 | "optional": true 133 | }, 134 | "svgo": { 135 | "optional": true 136 | } 137 | }, 138 | "engines": { 139 | "node": ">= 18.12.0" 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/plugin-options.json: -------------------------------------------------------------------------------- 1 | { 2 | "definitions": { 3 | "Rule": { 4 | "description": "Filtering rule as regex or string.", 5 | "anyOf": [ 6 | { 7 | "instanceof": "RegExp" 8 | }, 9 | { 10 | "type": "string", 11 | "minLength": 1 12 | } 13 | ] 14 | }, 15 | "Rules": { 16 | "description": "Filtering rules.", 17 | "anyOf": [ 18 | { 19 | "type": "array", 20 | "items": { 21 | "description": "A rule condition.", 22 | "oneOf": [ 23 | { 24 | "$ref": "#/definitions/Rule" 25 | } 26 | ] 27 | } 28 | }, 29 | { 30 | "$ref": "#/definitions/Rule" 31 | } 32 | ] 33 | }, 34 | "Minimizer": { 35 | "type": "object", 36 | "additionalProperties": false, 37 | "properties": { 38 | "implementation": { 39 | "description": "Implementation of the minimizer function.", 40 | "instanceof": "Function" 41 | }, 42 | "filter": { 43 | "description": "Allows filtering of images.", 44 | "instanceof": "Function" 45 | }, 46 | "filename": { 47 | "description": "Allows to set the filename for the minimized asset.", 48 | "anyOf": [ 49 | { 50 | "type": "string", 51 | "minLength": 1 52 | }, 53 | { 54 | "instanceof": "Function" 55 | } 56 | ] 57 | }, 58 | "options": { 59 | "description": "Options for the minimizer function.", 60 | "type": "object", 61 | "additionalProperties": true 62 | } 63 | } 64 | }, 65 | "Generator": { 66 | "type": "object", 67 | "additionalProperties": false, 68 | "properties": { 69 | "type": { 70 | "description": "Type of generation", 71 | "enum": ["import", "asset"] 72 | }, 73 | "preset": { 74 | "description": "Name of preset, i.e. using in '?as=webp'.", 75 | "type": "string", 76 | "minLength": 1 77 | }, 78 | "implementation": { 79 | "description": "Implementation of the generator function.", 80 | "instanceof": "Function" 81 | }, 82 | "options": { 83 | "description": "Options for the generator function.", 84 | "type": "object", 85 | "additionalProperties": true 86 | }, 87 | "filter": { 88 | "description": "Allows filtering of images.", 89 | "instanceof": "Function" 90 | }, 91 | "filename": { 92 | "description": "Allows to set the filename for the minimized asset.", 93 | "anyOf": [ 94 | { 95 | "type": "string", 96 | "minLength": 1 97 | }, 98 | { 99 | "instanceof": "Function" 100 | } 101 | ] 102 | } 103 | }, 104 | "required": ["implementation"] 105 | } 106 | }, 107 | "type": "object", 108 | "additionalProperties": false, 109 | "properties": { 110 | "test": { 111 | "description": "Include all modules that pass test assertion.", 112 | "link": "https://github.com/webpack/image-minimizer-webpack-plugin#test", 113 | "oneOf": [ 114 | { 115 | "$ref": "#/definitions/Rules" 116 | } 117 | ] 118 | }, 119 | "include": { 120 | "description": "Include all modules matching any of these conditions.", 121 | "link": "https://github.com/webpack/image-minimizer-webpack-plugin#include", 122 | "oneOf": [ 123 | { 124 | "$ref": "#/definitions/Rules" 125 | } 126 | ] 127 | }, 128 | "exclude": { 129 | "description": "Exclude all modules matching any of these conditions.", 130 | "link": "https://github.com/webpack/image-minimizer-webpack-plugin#exclude", 131 | "oneOf": [ 132 | { 133 | "$ref": "#/definitions/Rules" 134 | } 135 | ] 136 | }, 137 | "minimizer": { 138 | "description": "Allows you to setup the minimizer function and options.", 139 | "link": "https://github.com/webpack/image-minimizer-webpack-plugin#minimizer", 140 | "anyOf": [ 141 | { 142 | "type": "array", 143 | "minItems": 1, 144 | "items": { 145 | "$ref": "#/definitions/Minimizer" 146 | } 147 | }, 148 | { 149 | "$ref": "#/definitions/Minimizer" 150 | } 151 | ] 152 | }, 153 | "generator": { 154 | "description": "Allows you to setup the generator function and options.", 155 | "link": "https://github.com/webpack/image-minimizer-webpack-plugin#generator", 156 | "type": "array", 157 | "minItems": 1, 158 | "items": { 159 | "$ref": "#/definitions/Generator" 160 | } 161 | }, 162 | "severityError": { 163 | "description": "Allows to choose how errors are displayed.", 164 | "link": "https://github.com/webpack/image-minimizer-webpack-plugin#severityerror", 165 | "enum": ["off", "warning", "error"] 166 | }, 167 | "loader": { 168 | "description": "Automatically adding `imagemin-loader` (require for minification images using in `url-loader`, `svg-url-loader` or other).", 169 | "link": "https://github.com/webpack/image-minimizer-webpack-plugin#loader", 170 | "type": "boolean" 171 | }, 172 | "concurrency": { 173 | "description": "Number of concurrency optimization processes in one time.", 174 | "link": "https://github.com/webpack/image-minimizer-webpack-plugin#concurrency", 175 | "type": "number" 176 | }, 177 | "deleteOriginalAssets": { 178 | "type": "boolean", 179 | "description": "Allows to remove original assets after minimization.", 180 | "link": "https://github.com/webpack/image-minimizer-webpack-plugin#deleteoriginalassets" 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /test/validate-loader-options.test.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | 3 | import ImageMinimizerPlugin from "../src"; 4 | 5 | import { fixturesPath, plugins, runWebpack } from "./helpers"; 6 | 7 | describe("validate loader options", () => { 8 | const tests = { 9 | minimizer: { 10 | success: [ 11 | { 12 | implementation: ImageMinimizerPlugin.sharpMinify, 13 | }, 14 | { 15 | implementation: ImageMinimizerPlugin.sharpMinify, 16 | options: { 17 | encodeOptions: { 18 | mozjpeg: { 19 | quality: 90, 20 | }, 21 | }, 22 | }, 23 | }, 24 | [ 25 | { 26 | implementation: ImageMinimizerPlugin.sharpMinify, 27 | }, 28 | ], 29 | [ 30 | { 31 | implementation: ImageMinimizerPlugin.sharpMinify, 32 | options: { 33 | encodeOptions: { 34 | mozjpeg: { 35 | quality: 90, 36 | }, 37 | }, 38 | }, 39 | }, 40 | ], 41 | [ 42 | { 43 | implementation: ImageMinimizerPlugin.sharpMinify, 44 | }, 45 | { 46 | implementation: ImageMinimizerPlugin.sharpMinify, 47 | options: { 48 | encodeOptions: { 49 | mozjpeg: { 50 | quality: 90, 51 | }, 52 | }, 53 | }, 54 | }, 55 | ], 56 | { 57 | implementation: ImageMinimizerPlugin.sharpMinify, 58 | filter: () => false, 59 | }, 60 | [ 61 | { 62 | implementation: ImageMinimizerPlugin.sharpMinify, 63 | filter: () => false, 64 | }, 65 | ], 66 | { 67 | implementation: ImageMinimizerPlugin.sharpMinify, 68 | filename: "[name].[ext]", 69 | }, 70 | [ 71 | { 72 | implementation: ImageMinimizerPlugin.sharpMinify, 73 | filename: () => "[name].[ext]", 74 | }, 75 | ], 76 | ], 77 | failure: [ 78 | 1, 79 | true, 80 | false, 81 | null, 82 | [], 83 | { 84 | implementation: ImageMinimizerPlugin.sharpMinify, 85 | filter: true, 86 | }, 87 | { 88 | implementation: ImageMinimizerPlugin.sharpMinify, 89 | filename: true, 90 | }, 91 | ], 92 | }, 93 | generator: { 94 | success: [ 95 | [ 96 | { 97 | preset: "webp", 98 | implementation: ImageMinimizerPlugin.sharpGenerate, 99 | }, 100 | ], 101 | [ 102 | { 103 | preset: "one", 104 | implementation: ImageMinimizerPlugin.sharpGenerate, 105 | }, 106 | { 107 | preset: "two", 108 | implementation: ImageMinimizerPlugin.sharpGenerate, 109 | }, 110 | ], 111 | [ 112 | { 113 | preset: "webp", 114 | implementation: ImageMinimizerPlugin.sharpGenerate, 115 | filter: () => false, 116 | }, 117 | ], 118 | [ 119 | { 120 | preset: "webp", 121 | implementation: ImageMinimizerPlugin.sharpGenerate, 122 | filename: "[name][ext]", 123 | }, 124 | ], 125 | [ 126 | { 127 | preset: "webp", 128 | implementation: ImageMinimizerPlugin.sharpGenerate, 129 | filename: () => "[name][ext]", 130 | }, 131 | ], 132 | ], 133 | failure: [ 134 | 1, 135 | true, 136 | false, 137 | null, 138 | [], 139 | [ 140 | { 141 | preset: "webp", 142 | }, 143 | ], 144 | [ 145 | { 146 | preset: "webp", 147 | implementation: ImageMinimizerPlugin.sharpGenerate, 148 | filter: true, 149 | }, 150 | ], 151 | [ 152 | { 153 | preset: "webp", 154 | implementation: ImageMinimizerPlugin.sharpGenerate, 155 | filename: true, 156 | }, 157 | ], 158 | ], 159 | }, 160 | severityError: { 161 | success: ["error"], 162 | failure: [true, false, {}, [], () => {}], 163 | }, 164 | unknown: { 165 | success: [], 166 | failure: [1, true, false, "test", /test/, [], {}, { foo: "bar" }], 167 | }, 168 | }; 169 | 170 | function stringifyValue(value) { 171 | if ( 172 | Array.isArray(value) || 173 | (value && typeof value === "object" && value.constructor === Object) 174 | ) { 175 | return JSON.stringify(value); 176 | } 177 | 178 | return value; 179 | } 180 | 181 | function createTestCase(key, value, type) { 182 | it(`should ${ 183 | type === "success" ? "successfully validate" : "throw an error on" 184 | } the "${key}" option with "${stringifyValue(value)}" value`, async () => { 185 | const options = { 186 | entry: path.join(fixturesPath, "validate-options.js"), 187 | imageminLoaderOptions: { 188 | [key]: value, 189 | }, 190 | }; 191 | 192 | if (key !== "minimizer") { 193 | options.imageminLoaderOptions.minimizer = { 194 | implementation: ImageMinimizerPlugin.imageminMinify, 195 | options: { plugins }, 196 | }; 197 | } 198 | 199 | let stats; 200 | 201 | try { 202 | stats = await runWebpack(options); 203 | } finally { 204 | const shouldSuccess = type === "success"; 205 | const { 206 | compilation: { errors }, 207 | } = stats; 208 | 209 | expect(stats.hasErrors()).toBe(!shouldSuccess); 210 | expect(errors).toHaveLength(shouldSuccess ? 0 : 1); 211 | 212 | if (!shouldSuccess) { 213 | expect(() => { 214 | throw new Error(errors[0].error.message); 215 | }).toThrowErrorMatchingSnapshot(); 216 | } 217 | } 218 | }); 219 | } 220 | 221 | for (const [key, values] of Object.entries(tests)) { 222 | for (const type of Object.keys(values)) { 223 | for (const value of values[type]) { 224 | createTestCase(key, value, type); 225 | } 226 | } 227 | } 228 | }); 229 | -------------------------------------------------------------------------------- /types/utils.d.ts: -------------------------------------------------------------------------------- 1 | export type WorkerResult = import("./index").WorkerResult; 2 | export type CustomOptions = import("./index").CustomOptions; 3 | export type WebpackError = import("webpack").WebpackError; 4 | export type Module = import("webpack").Module; 5 | export type AssetInfo = import("webpack").AssetInfo; 6 | export type EXPECTED_ANY = any; 7 | export type Task = () => Promise; 8 | export type FunctionReturning = () => T; 9 | export type CustomSharpFormat = EXPECTED_ANY; 10 | export type Uint8ArrayUtf8ByteString = ( 11 | array: number[] | Uint8Array, 12 | start: number, 13 | end: number, 14 | ) => string; 15 | export type StringToBytes = (string: string) => number[]; 16 | export type MetaData = { 17 | /** 18 | * warnings 19 | */ 20 | warnings: Array; 21 | /** 22 | * errors 23 | */ 24 | errors: Array; 25 | }; 26 | export type SquooshImage = { 27 | /** 28 | * preprocess 29 | */ 30 | preprocess: (options: Record) => Promise; 31 | /** 32 | * encode 33 | */ 34 | encode: (options: Record) => Promise; 35 | /** 36 | * encoded with 37 | */ 38 | encodedWith: Record< 39 | string, 40 | { 41 | binary: Uint8Array; 42 | extension: string; 43 | } 44 | >; 45 | /** 46 | * decoded 47 | */ 48 | decoded: { 49 | bitmap: { 50 | width: number; 51 | height: number; 52 | }; 53 | }; 54 | }; 55 | export type SquooshImagePool = { 56 | /** 57 | * ingest image function 58 | */ 59 | ingestImage: (data: Uint8Array) => SquooshImage; 60 | /** 61 | * close function 62 | */ 63 | close: () => Promise; 64 | }; 65 | export type SizeSuffix = (width: number, height: number) => string; 66 | export const ABSOLUTE_URL_REGEX: RegExp; 67 | /** @type {WeakMap} */ 68 | export const IMAGE_MINIMIZER_PLUGIN_INFO_MAPPINGS: WeakMap; 69 | export const WINDOWS_PATH_REGEX: RegExp; 70 | /** 71 | * @param {WorkerResult} original original worker result 72 | * @param {CustomOptions=} options options 73 | * @returns {Promise} generated result 74 | */ 75 | export function imageminGenerate( 76 | original: WorkerResult, 77 | options?: CustomOptions | undefined, 78 | ): Promise; 79 | /** 80 | * @param {WorkerResult} original original worker result 81 | * @param {CustomOptions=} options options 82 | * @returns {Promise} minified result 83 | */ 84 | export function imageminMinify( 85 | original: WorkerResult, 86 | options?: CustomOptions | undefined, 87 | ): Promise; 88 | /** 89 | * @param {Record} imageminConfig imagemin configuration 90 | * @returns {Promise>} normalized imagemin configuration 91 | */ 92 | export function imageminNormalizeConfig( 93 | imageminConfig: Record, 94 | ): Promise>; 95 | /** 96 | * @param {string} url URL 97 | * @returns {boolean} true when URL is absolute, otherwise false 98 | */ 99 | export function isAbsoluteURL(url: string): boolean; 100 | /** 101 | * @template T 102 | * @typedef {() => T} FunctionReturning 103 | */ 104 | /** 105 | * @template T 106 | * @param {FunctionReturning} fn memorized function 107 | * @returns {FunctionReturning} new function 108 | */ 109 | export function memoize(fn: FunctionReturning): FunctionReturning; 110 | /** @typedef {import("./index").WorkerResult} WorkerResult */ 111 | /** @typedef {import("./index").CustomOptions} CustomOptions */ 112 | /** @typedef {import("webpack").WebpackError} WebpackError */ 113 | /** @typedef {import("webpack").Module} Module */ 114 | /** @typedef {import("webpack").AssetInfo} AssetInfo */ 115 | /** @typedef {any} EXPECTED_ANY */ 116 | /** 117 | * @template T 118 | * @typedef {() => Promise} Task 119 | */ 120 | /** 121 | * @param {string} filename file path without query params (e.g. `path/img.png`) 122 | * @param {string} ext new file extension without `.` (e.g. `webp`) 123 | * @returns {string} new filename `path/img.png` -> `path/img.webp` 124 | */ 125 | export function replaceFileExtension(filename: string, ext: string): string; 126 | /** 127 | * @param {WorkerResult} original original worker result 128 | * @param {CustomOptions=} options options 129 | * @returns {Promise} generated result 130 | */ 131 | export function sharpGenerate( 132 | original: WorkerResult, 133 | options?: CustomOptions | undefined, 134 | ): Promise; 135 | /** 136 | * @param {WorkerResult} original original worker result 137 | * @param {CustomOptions=} options options 138 | * @returns {Promise} minified result 139 | */ 140 | export function sharpMinify( 141 | original: WorkerResult, 142 | options?: CustomOptions | undefined, 143 | ): Promise; 144 | /** 145 | * @param {WorkerResult} original original worker result 146 | * @param {CustomOptions=} options options 147 | * @returns {Promise} generated result 148 | */ 149 | export function squooshGenerate( 150 | original: WorkerResult, 151 | options?: CustomOptions | undefined, 152 | ): Promise; 153 | export namespace squooshGenerate { 154 | export { squooshImagePoolSetup as setup }; 155 | export { squooshImagePoolTeardown as teardown }; 156 | } 157 | /** 158 | * @param {WorkerResult} original original worker result 159 | * @param {CustomOptions=} options options 160 | * @returns {Promise} minified result 161 | */ 162 | export function squooshMinify( 163 | original: WorkerResult, 164 | options?: CustomOptions | undefined, 165 | ): Promise; 166 | export namespace squooshMinify { 167 | export { squooshImagePoolSetup as setup }; 168 | export { squooshImagePoolTeardown as teardown }; 169 | } 170 | /** 171 | * @param {WorkerResult} original original worker result 172 | * @param {CustomOptions=} options options 173 | * @returns {Promise} minified result 174 | */ 175 | export function svgoMinify( 176 | original: WorkerResult, 177 | options?: CustomOptions | undefined, 178 | ): Promise; 179 | /** 180 | * Run tasks with limited concurrency. 181 | * @template T 182 | * @param {number} limit Limit of tasks that run at once. 183 | * @param {Task[]} tasks List of tasks to run. 184 | * @returns {Promise} A promise that fulfills to an array of the results 185 | */ 186 | export function throttleAll(limit: number, tasks: Task[]): Promise; 187 | /** 188 | * @returns {void} 189 | */ 190 | declare function squooshImagePoolSetup(): void; 191 | /** 192 | * @returns {Promise} 193 | */ 194 | declare function squooshImagePoolTeardown(): Promise; 195 | export {}; 196 | -------------------------------------------------------------------------------- /test/plugin-deleteOriginalAssets-option.test.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | 3 | import ImageMinimizerPlugin from "../src/index.js"; 4 | import { fixturesPath, plugins, runWebpack } from "./helpers"; 5 | 6 | describe('plugin "deleteOriginalAssets" option', () => { 7 | it("should minimize asset and delete original asset (default behavior)", async () => { 8 | const stats = await runWebpack({ 9 | entry: path.join(fixturesPath, "./empty-entry.js"), 10 | emitPlugin: true, 11 | emitPluginOptions: { fileNames: ["./nested/deep/plugin-test.png"] }, 12 | imageminPluginOptions: { 13 | minimizer: { 14 | implementation: ImageMinimizerPlugin.imageminMinify, 15 | filename: "[path]minimizer-[name][ext]", 16 | options: { plugins }, 17 | }, 18 | }, 19 | }); 20 | const { compilation } = stats; 21 | const { warnings, errors, assets } = compilation; 22 | const newAsset = Object.keys(assets).filter((asset) => 23 | asset.includes("./nested/deep/minimizer-plugin-test.png"), 24 | ); 25 | const originalAsset = Object.keys(assets).filter((asset) => 26 | asset.includes("./nested/deep/plugin-test.png"), 27 | ); 28 | 29 | expect(newAsset).toHaveLength(1); 30 | expect(originalAsset).toHaveLength(0); 31 | expect(warnings).toHaveLength(0); 32 | expect(errors).toHaveLength(0); 33 | }); 34 | 35 | it("should minimize asset and delete original asset when the name is the same (default behavior)", async () => { 36 | const stats = await runWebpack({ 37 | entry: path.join(fixturesPath, "./empty-entry.js"), 38 | emitPlugin: true, 39 | emitPluginOptions: { fileNames: ["./nested/deep/plugin-test.png"] }, 40 | imageminPluginOptions: { 41 | minimizer: { 42 | implementation: ImageMinimizerPlugin.imageminMinify, 43 | options: { plugins }, 44 | }, 45 | }, 46 | }); 47 | const { compilation } = stats; 48 | const { warnings, errors, assets } = compilation; 49 | const newAsset = Object.keys(assets).filter((asset) => 50 | asset.includes("./nested/deep/plugin-test.png"), 51 | ); 52 | 53 | expect(newAsset).toHaveLength(1); 54 | expect(Object.keys(assets)).toHaveLength(2); 55 | expect(warnings).toHaveLength(0); 56 | expect(errors).toHaveLength(0); 57 | }); 58 | 59 | it('should minimize asset and delete original asset and keep original asset when the "deleteOriginalAssets" option is "false"', async () => { 60 | const stats = await runWebpack({ 61 | entry: path.join(fixturesPath, "./empty-entry.js"), 62 | emitPlugin: true, 63 | emitPluginOptions: { fileNames: ["./nested/deep/plugin-test.png"] }, 64 | imageminPluginOptions: { 65 | deleteOriginalAssets: false, 66 | minimizer: { 67 | implementation: ImageMinimizerPlugin.imageminMinify, 68 | filename: "[path]minimizer-[name][ext]", 69 | options: { plugins: ["imagemin-pngquant"] }, 70 | }, 71 | }, 72 | }); 73 | const { compilation } = stats; 74 | const { warnings, errors, assets } = compilation; 75 | const newAsset = Object.keys(assets).filter((asset) => 76 | asset.includes("./nested/deep/minimizer-plugin-test.png"), 77 | ); 78 | const originalAsset = Object.keys(assets).filter((asset) => 79 | asset.includes("./nested/deep/plugin-test.png"), 80 | ); 81 | 82 | expect(newAsset).toHaveLength(1); 83 | expect(originalAsset).toHaveLength(1); 84 | expect(warnings).toHaveLength(0); 85 | expect(errors).toHaveLength(0); 86 | }); 87 | 88 | it('should transform asset and keep original asset when the "deleteOriginalAssets" option is "true"', async () => { 89 | const stats = await runWebpack({ 90 | entry: path.join(fixturesPath, "./empty-entry.js"), 91 | emitPlugin: true, 92 | emitPluginOptions: { fileNames: ["./nested/deep/plugin-test.png"] }, 93 | imageminPluginOptions: { 94 | deleteOriginalAssets: true, 95 | minimizer: { 96 | implementation: ImageMinimizerPlugin.imageminMinify, 97 | filename: "[path]minimizer-[name][ext]", 98 | options: { plugins: ["imagemin-pngquant"] }, 99 | }, 100 | }, 101 | }); 102 | const { compilation } = stats; 103 | const { warnings, errors, assets } = compilation; 104 | const newAsset = Object.keys(assets).filter((asset) => 105 | asset.includes("./nested/deep/minimizer-plugin-test.png"), 106 | ); 107 | const originalAsset = Object.keys(assets).filter((asset) => 108 | asset.includes("./nested/deep/plugin-test.png"), 109 | ); 110 | 111 | expect(newAsset).toHaveLength(1); 112 | expect(originalAsset).toHaveLength(0); 113 | expect(warnings).toHaveLength(0); 114 | expect(errors).toHaveLength(0); 115 | }); 116 | 117 | it('should transform asset and keep original asset when the "deleteOriginalAssets" option is "true" (multi compiler mode)', async () => { 118 | const multiStats = await runWebpack([ 119 | { 120 | entry: path.join(fixturesPath, "./empty-entry.js"), 121 | emitPlugin: true, 122 | emitPluginOptions: { fileNames: ["./nested/deep/plugin-test.png"] }, 123 | imageminPluginOptions: { 124 | deleteOriginalAssets: true, 125 | minimizer: { 126 | implementation: ImageMinimizerPlugin.imageminMinify, 127 | filename: "[path]one-minimized-[name][ext]", 128 | options: { plugins }, 129 | }, 130 | }, 131 | }, 132 | { 133 | entry: path.join(fixturesPath, "./empty-entry.js"), 134 | emitPlugin: true, 135 | emitPluginOptions: { fileNames: ["./nested/deep/plugin-test.png"] }, 136 | imageminPluginOptions: { 137 | minimizer: { 138 | implementation: ImageMinimizerPlugin.imageminMinify, 139 | filename: "[path]two-minimized-[name][ext]", 140 | options: { plugins }, 141 | }, 142 | }, 143 | }, 144 | ]); 145 | 146 | expect(multiStats.stats).toHaveLength(2); 147 | 148 | const [{ compilation }, { compilation: secondCompilation }] = 149 | multiStats.stats; 150 | const { warnings, errors, assets } = compilation; 151 | 152 | const transformedAssets = Object.keys(assets).filter((asset) => 153 | asset.includes("./nested/deep/one-minimized-plugin-test.png"), 154 | ); 155 | 156 | const originalAssets = Object.keys(assets).filter((asset) => 157 | asset.includes("./nested/deep/plugin-test.png"), 158 | ); 159 | 160 | const { warnings: secondWarnings, errors: secondErrors } = 161 | secondCompilation; 162 | 163 | expect(secondWarnings).toHaveLength(0); 164 | expect(secondErrors).toHaveLength(0); 165 | 166 | expect(transformedAssets).toHaveLength(1); 167 | expect(originalAssets).toHaveLength(0); 168 | expect(warnings).toHaveLength(0); 169 | expect(errors).toHaveLength(0); 170 | }); 171 | }); 172 | -------------------------------------------------------------------------------- /test/plugin-severityError-option.test.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | 3 | import ImageMinimizerPlugin from "../src/index.js"; 4 | import { fixturesPath, isOptimized, plugins, runWebpack } from "./helpers"; 5 | 6 | describe("plugin severityError option", () => { 7 | it("should optimizes images and throws error on corrupted images using `plugin.severityError` option with `error` value (by plugin)", async () => { 8 | const stats = await runWebpack({ 9 | emitPluginOptions: { 10 | fileNames: ["test-corrupted.jpg", "plugin-test.png"], 11 | }, 12 | entry: path.join(fixturesPath, "empty-entry.js"), 13 | imageminPluginOptions: { 14 | severityError: "error", 15 | minimizer: { 16 | implementation: ImageMinimizerPlugin.imageminMinify, 17 | options: { plugins }, 18 | }, 19 | }, 20 | }); 21 | const { compilation } = stats; 22 | const { warnings, errors } = compilation; 23 | 24 | expect(warnings).toHaveLength(0); 25 | expect(errors).toHaveLength(1); 26 | expect(errors[0].message).toMatch( 27 | /(Corrupt JPEG data|Command failed with EPIPE)/, 28 | ); 29 | 30 | await expect(isOptimized("plugin-test.png", compilation)).resolves.toBe( 31 | true, 32 | ); 33 | }); 34 | 35 | it("should optimizes images and not throws error or warnings on corrupted images using `plugin.severityError` option with `off` value (by plugin)", async () => { 36 | const stats = await runWebpack({ 37 | emitPluginOptions: { 38 | fileNames: ["test-corrupted.jpg", "plugin-test.png"], 39 | }, 40 | entry: path.join(fixturesPath, "empty-entry.js"), 41 | imageminPluginOptions: { 42 | severityError: "off", 43 | minimizer: { 44 | implementation: ImageMinimizerPlugin.imageminMinify, 45 | options: { plugins }, 46 | }, 47 | }, 48 | }); 49 | const { compilation } = stats; 50 | const { warnings, errors } = compilation; 51 | 52 | expect(warnings).toHaveLength(0); 53 | expect(errors).toHaveLength(0); 54 | 55 | await expect(isOptimized("plugin-test.png", compilation)).resolves.toBe( 56 | true, 57 | ); 58 | }); 59 | 60 | it("should optimizes images and throws warnings on corrupted images using `plugin.severityError` option with `warning` value (by plugin)", async () => { 61 | const stats = await runWebpack({ 62 | emitPluginOptions: { 63 | fileNames: ["test-corrupted.jpg", "plugin-test.png"], 64 | }, 65 | entry: path.join(fixturesPath, "empty-entry.js"), 66 | imageminPluginOptions: { 67 | severityError: "warning", 68 | minimizer: { 69 | implementation: ImageMinimizerPlugin.imageminMinify, 70 | options: { plugins }, 71 | }, 72 | }, 73 | }); 74 | const { compilation } = stats; 75 | const { warnings, errors } = compilation; 76 | 77 | expect(warnings).toHaveLength(1); 78 | expect(errors).toHaveLength(0); 79 | expect(warnings[0].message).toMatch( 80 | /(Corrupt JPEG data|Command failed with EPIPE)/, 81 | ); 82 | 83 | await expect(isOptimized("plugin-test.png", compilation)).resolves.toBe( 84 | true, 85 | ); 86 | }); 87 | 88 | it("should optimizes images and throws error on corrupted images when `plugin.severityError` option not specify (by plugin)", async () => { 89 | const stats = await runWebpack({ 90 | emitPluginOptions: { 91 | fileNames: ["test-corrupted.jpg", "plugin-test.png"], 92 | }, 93 | entry: path.join(fixturesPath, "empty-entry.js"), 94 | imageminPluginOptions: { 95 | minimizer: { 96 | implementation: ImageMinimizerPlugin.imageminMinify, 97 | options: { plugins }, 98 | }, 99 | }, 100 | }); 101 | const { compilation } = stats; 102 | const { warnings, errors } = compilation; 103 | 104 | expect(warnings).toHaveLength(0); 105 | expect(errors).toHaveLength(1); 106 | expect(errors[0].message).toMatch( 107 | /(Corrupt JPEG data|Command failed with EPIPE|Command failed with ENOTCONN)/, 108 | ); 109 | 110 | await expect(isOptimized("plugin-test.png", compilation)).resolves.toBe( 111 | true, 112 | ); 113 | }); 114 | 115 | it("should optimizes images and throws errors on corrupted images using `plugin.severityError` option with `error` value (by loader)", async () => { 116 | const stats = await runWebpack({ 117 | entry: path.join(fixturesPath, "loader-corrupted.js"), 118 | imageminPluginOptions: { 119 | severityError: "error", 120 | minimizer: { 121 | implementation: ImageMinimizerPlugin.imageminMinify, 122 | options: { plugins }, 123 | }, 124 | }, 125 | }); 126 | const { compilation } = stats; 127 | const { warnings, errors } = compilation; 128 | 129 | expect(warnings).toHaveLength(0); 130 | expect(errors).toHaveLength(2); 131 | 132 | // From loader 133 | expect(errors[0].message).toMatch( 134 | /(Corrupt JPEG data|Command failed with EPIPE)/, 135 | ); 136 | // From plugin 137 | expect(errors[1].message).toMatch( 138 | /(Corrupt JPEG data|Command failed with EPIPE)/, 139 | ); 140 | 141 | await expect(isOptimized("loader-test.png", compilation)).resolves.toBe( 142 | true, 143 | ); 144 | }); 145 | 146 | it("should throws errors on corrupted images when `plugin.severityError` option not specify (by loader)", async () => { 147 | const stats = await runWebpack({ 148 | entry: path.join(fixturesPath, "loader-corrupted.js"), 149 | imageminPluginOptions: { 150 | minimizer: { 151 | implementation: ImageMinimizerPlugin.imageminMinify, 152 | options: { plugins }, 153 | }, 154 | }, 155 | }); 156 | const { compilation } = stats; 157 | const { warnings, errors } = compilation; 158 | 159 | expect(warnings).toHaveLength(0); 160 | expect(errors).toHaveLength(2); 161 | 162 | // From loader 163 | expect(errors[0].message).toMatch( 164 | /(Corrupt JPEG data|Command failed with EPIPE)/, 165 | ); 166 | // From plugin 167 | expect(errors[1].message).toMatch( 168 | /(Corrupt JPEG data|Command failed with EPIPE)/, 169 | ); 170 | 171 | await expect(isOptimized("loader-test.png", compilation)).resolves.toBe( 172 | true, 173 | ); 174 | }); 175 | 176 | it("should optimizes images and throws errors on corrupted images using `plugin.severityError` option with `off` value (by loader)", async () => { 177 | const stats = await runWebpack({ 178 | entry: path.join(fixturesPath, "loader-corrupted.js"), 179 | imageminPluginOptions: { 180 | severityError: "off", 181 | minimizer: { 182 | implementation: ImageMinimizerPlugin.imageminMinify, 183 | options: { plugins }, 184 | }, 185 | }, 186 | }); 187 | const { compilation } = stats; 188 | const { warnings, errors } = compilation; 189 | 190 | expect(warnings).toHaveLength(0); 191 | expect(errors).toHaveLength(0); 192 | 193 | await expect(isOptimized("loader-test.png", compilation)).resolves.toBe( 194 | true, 195 | ); 196 | }); 197 | }); 198 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | export = ImageMinimizerPlugin; 2 | /** 3 | * @template T, [G=T] 4 | * @extends {WebpackPluginInstance} 5 | */ 6 | declare class ImageMinimizerPlugin { 7 | /** 8 | * @param {PluginOptions=} options Plugin options. 9 | */ 10 | constructor(options?: PluginOptions | undefined); 11 | /** 12 | * @private 13 | */ 14 | private options; 15 | /** 16 | * @private 17 | * @param {Compiler} compiler compiler 18 | * @param {Compilation} compilation compilation 19 | * @param {Record} assets assets 20 | * @returns {Promise} 21 | */ 22 | private optimize; 23 | /** 24 | * @private 25 | */ 26 | private setupAll; 27 | /** 28 | * @private 29 | */ 30 | private teardownAll; 31 | /** 32 | * @param {Compiler} compiler compiler 33 | */ 34 | apply(compiler: Compiler): void; 35 | } 36 | declare namespace ImageMinimizerPlugin { 37 | export { 38 | loader, 39 | imageminNormalizeConfig, 40 | imageminMinify, 41 | imageminGenerate, 42 | squooshMinify, 43 | squooshGenerate, 44 | sharpMinify, 45 | sharpGenerate, 46 | svgoMinify, 47 | Schema, 48 | WebpackPluginInstance, 49 | Compiler, 50 | Compilation, 51 | Asset, 52 | AssetInfo, 53 | Source, 54 | Module, 55 | TemplatePath, 56 | PathData, 57 | ImageminMinifyFunction, 58 | SquooshMinifyFunction, 59 | Rule, 60 | Rules, 61 | FilterFn, 62 | IsFilenameProcessed, 63 | WorkerResult, 64 | Task, 65 | CustomOptions, 66 | InferDefaultType, 67 | BasicTransformerOptions, 68 | ResizeOptions, 69 | BasicTransformerImplementation, 70 | BasicTransformerHelpers, 71 | TransformerFunction, 72 | FilenameFn, 73 | Transformer, 74 | Minimizer, 75 | Generator, 76 | InternalWorkerOptions, 77 | InternalLoaderOptions, 78 | PluginOptions, 79 | }; 80 | } 81 | declare var loader: string; 82 | import { imageminNormalizeConfig } from "./utils.js"; 83 | import { imageminMinify } from "./utils.js"; 84 | import { imageminGenerate } from "./utils.js"; 85 | import { squooshMinify } from "./utils.js"; 86 | import { squooshGenerate } from "./utils.js"; 87 | import { sharpMinify } from "./utils.js"; 88 | import { sharpGenerate } from "./utils.js"; 89 | import { svgoMinify } from "./utils.js"; 90 | type Schema = import("schema-utils").Schema; 91 | type WebpackPluginInstance = import("webpack").WebpackPluginInstance; 92 | type Compiler = import("webpack").Compiler; 93 | type Compilation = import("webpack").Compilation; 94 | type Asset = import("webpack").Asset; 95 | type AssetInfo = import("webpack").AssetInfo; 96 | type Source = import("webpack").sources.Source; 97 | type Module = import("webpack").Module; 98 | type TemplatePath = import("webpack").TemplatePath; 99 | type PathData = import("webpack").PathData; 100 | type ImageminMinifyFunction = typeof imageminMinify; 101 | type SquooshMinifyFunction = typeof squooshMinify; 102 | type Rule = RegExp | string; 103 | type Rules = Rule[] | Rule; 104 | type FilterFn = (source: Buffer, sourcePath: string) => boolean; 105 | type IsFilenameProcessed = typeof import("./worker").isFilenameProcessed; 106 | type WorkerResult = { 107 | /** 108 | * filename 109 | */ 110 | filename: string; 111 | /** 112 | * data buffer 113 | */ 114 | data: Buffer; 115 | /** 116 | * warnings 117 | */ 118 | warnings: Array; 119 | /** 120 | * errors 121 | */ 122 | errors: Array; 123 | /** 124 | * asset info 125 | */ 126 | info: AssetInfo & { 127 | [worker.isFilenameProcessed]?: boolean; 128 | }; 129 | }; 130 | type Task = { 131 | /** 132 | * task name 133 | */ 134 | name: string; 135 | /** 136 | * asset info 137 | */ 138 | info: AssetInfo; 139 | /** 140 | * input source 141 | */ 142 | inputSource: Source; 143 | /** 144 | * output 145 | */ 146 | output: 147 | | (WorkerResult & { 148 | source?: Source; 149 | }) 150 | | undefined; 151 | /** 152 | * cache item 153 | */ 154 | cacheItem: ReturnType["getItemCache"]>; 155 | /** 156 | * transformer 157 | */ 158 | transformer: Transformer | Transformer[]; 159 | }; 160 | type CustomOptions = { 161 | [key: string]: any; 162 | }; 163 | type InferDefaultType = T extends infer U ? U : CustomOptions; 164 | type BasicTransformerOptions = InferDefaultType | undefined; 165 | type ResizeOptions = { 166 | /** 167 | * width 168 | */ 169 | width?: number | undefined; 170 | /** 171 | * height 172 | */ 173 | height?: number | undefined; 174 | /** 175 | * unit 176 | */ 177 | unit?: ("px" | "percent") | undefined; 178 | /** 179 | * true when enabled, otherwise false 180 | */ 181 | enabled?: boolean | undefined; 182 | }; 183 | type BasicTransformerImplementation = ( 184 | original: WorkerResult, 185 | options?: BasicTransformerOptions | undefined, 186 | ) => Promise; 187 | type BasicTransformerHelpers = { 188 | /** 189 | * setup function 190 | */ 191 | setup?: (() => void) | undefined; 192 | /** 193 | * teardown function 194 | */ 195 | teardown?: (() => void) | undefined; 196 | }; 197 | type TransformerFunction = BasicTransformerImplementation & 198 | BasicTransformerHelpers; 199 | type FilenameFn = ( 200 | pathData: PathData, 201 | assetInfo?: AssetInfo | undefined, 202 | ) => string; 203 | type Transformer = { 204 | /** 205 | * implementation 206 | */ 207 | implementation: TransformerFunction; 208 | /** 209 | * options 210 | */ 211 | options?: BasicTransformerOptions | undefined; 212 | /** 213 | * filter 214 | */ 215 | filter?: FilterFn | undefined; 216 | /** 217 | * filename 218 | */ 219 | filename?: (string | FilenameFn) | undefined; 220 | /** 221 | * preset 222 | */ 223 | preset?: string | undefined; 224 | /** 225 | * type 226 | */ 227 | type?: ("import" | "asset") | undefined; 228 | }; 229 | type Minimizer = Omit, "preset" | "type">; 230 | type Generator = Transformer; 231 | type InternalWorkerOptions = { 232 | /** 233 | * filename 234 | */ 235 | filename: string; 236 | /** 237 | * asset info 238 | */ 239 | info?: AssetInfo | undefined; 240 | /** 241 | * input buffer 242 | */ 243 | input: Buffer; 244 | /** 245 | * transformer 246 | */ 247 | transformer: Transformer | Transformer[]; 248 | /** 249 | * severity error setting 250 | */ 251 | severityError?: string | undefined; 252 | /** 253 | * filename generator function 254 | */ 255 | generateFilename: (filename: TemplatePath, data: PathData) => string; 256 | }; 257 | type InternalLoaderOptions = import("./loader").LoaderOptions; 258 | type PluginOptions = { 259 | /** 260 | * test to match files against 261 | */ 262 | test?: Rule | undefined; 263 | /** 264 | * files to include 265 | */ 266 | include?: Rule | undefined; 267 | /** 268 | * files to exclude 269 | */ 270 | exclude?: Rule | undefined; 271 | /** 272 | * allows to set the minimizer 273 | */ 274 | minimizer?: 275 | | (T extends any[] 276 | ? { [P in keyof T]: Minimizer } 277 | : Minimizer | Minimizer[]) 278 | | undefined; 279 | /** 280 | * allows to set the generator 281 | */ 282 | generator?: 283 | | (G extends any[] ? { [P in keyof G]: Generator } : Generator[]) 284 | | undefined; 285 | /** 286 | * automatically adding `image-loader`. 287 | */ 288 | loader?: boolean | undefined; 289 | /** 290 | * maximum number of concurrency optimization processes in one time 291 | */ 292 | concurrency?: number | undefined; 293 | /** 294 | * allows to choose how errors are displayed 295 | */ 296 | severityError?: string | undefined; 297 | /** 298 | * allows to remove original assets, useful for converting to a `webp` and remove original assets 299 | */ 300 | deleteOriginalAssets?: boolean | undefined; 301 | }; 302 | import worker = require("./worker"); 303 | -------------------------------------------------------------------------------- /src/loader.js: -------------------------------------------------------------------------------- 1 | const path = require("node:path"); 2 | 3 | const schema = require("./loader-options.json"); 4 | const { 5 | ABSOLUTE_URL_REGEX, 6 | IMAGE_MINIMIZER_PLUGIN_INFO_MAPPINGS, 7 | WINDOWS_PATH_REGEX, 8 | } = require("./utils.js"); 9 | const worker = require("./worker"); 10 | 11 | /** @typedef {import("schema-utils/declarations/validate").Schema} Schema */ 12 | /** @typedef {import("webpack").Compilation} Compilation */ 13 | /** @typedef {import("./utils").WorkerResult} WorkerResult */ 14 | 15 | /** 16 | * @template T 17 | * @typedef {import("./index").Minimizer} Minimizer 18 | */ 19 | 20 | /** 21 | * @template T 22 | * @typedef {import("./index").Generator} Generator 23 | */ 24 | 25 | /** 26 | * @template T 27 | * @typedef {object} LoaderOptions 28 | * @property {string=} severityError allows to choose how errors are displayed. 29 | * @property {Minimizer | Minimizer[]=} minimizer minimizer configuration 30 | * @property {Generator[]=} generator generator configuration 31 | */ 32 | 33 | // Workaround - https://github.com/webpack/image-minimizer-webpack-plugin/issues/341 34 | /** 35 | * @template T 36 | * @param {import("webpack").LoaderContext>} loaderContext loader context 37 | * @param {WorkerResult} output worker result 38 | * @param {string} query query string 39 | */ 40 | function changeResource(loaderContext, output, query) { 41 | loaderContext.resourcePath = path.join( 42 | loaderContext.rootContext, 43 | output.filename, 44 | ); 45 | loaderContext.resourceQuery = query; 46 | } 47 | 48 | /** 49 | * @template T 50 | * @param {Minimizer[]} transformers transformers 51 | * @param {string | null} widthQuery width query 52 | * @param {string | null} heightQuery height query 53 | * @param {string | null} unitQuery unit query 54 | * @returns {Minimizer[]} processed transformers 55 | */ 56 | function processSizeQuery(transformers, widthQuery, heightQuery, unitQuery) { 57 | return transformers.map((transformer) => { 58 | const minimizer = { ...transformer }; 59 | 60 | const minimizerOptions = { 61 | .../** @type {{ options: import("./index").BasicTransformerOptions & { resize?: import("./index").ResizeOptions }}} */ 62 | (minimizer).options, 63 | }; 64 | 65 | minimizerOptions.resize = { ...minimizerOptions?.resize }; 66 | minimizer.options = minimizerOptions; 67 | 68 | if (widthQuery === "auto") { 69 | delete minimizerOptions.resize.width; 70 | } else if (widthQuery) { 71 | const width = Number.parseInt(widthQuery, 10); 72 | 73 | if (Number.isFinite(width) && width > 0) { 74 | minimizerOptions.resize.width = width; 75 | } 76 | } 77 | 78 | if (heightQuery === "auto") { 79 | delete minimizerOptions.resize.height; 80 | } else if (heightQuery) { 81 | const height = Number.parseInt(heightQuery, 10); 82 | 83 | if (Number.isFinite(height) && height > 0) { 84 | minimizerOptions.resize.height = height; 85 | } 86 | } 87 | 88 | if (unitQuery === "px" || unitQuery === "percent") { 89 | minimizerOptions.resize.unit = unitQuery; 90 | } 91 | 92 | return minimizer; 93 | }); 94 | } 95 | 96 | /** 97 | * @template T 98 | * @this {import("webpack").LoaderContext>} 99 | * @param {Buffer} content content 100 | * @returns {Promise} processed content 101 | */ 102 | async function loader(content) { 103 | // Avoid optimize twice 104 | const imageMinimizerPluginInfo = this._module 105 | ? IMAGE_MINIMIZER_PLUGIN_INFO_MAPPINGS.get(this._module) 106 | : undefined; 107 | 108 | if ( 109 | imageMinimizerPluginInfo?.minimized || 110 | imageMinimizerPluginInfo?.generated 111 | ) { 112 | return content; 113 | } 114 | 115 | const options = this.getOptions(/** @type {Schema} */ (schema)); 116 | const callback = this.async(); 117 | const { generator, minimizer, severityError } = options; 118 | 119 | if (!minimizer && !generator) { 120 | callback( 121 | new Error( 122 | "Not configured 'minimizer' or 'generator' options, please setup them", 123 | ), 124 | ); 125 | 126 | return; 127 | } 128 | 129 | let transformer = minimizer; 130 | 131 | const parsedQuery = 132 | this.resourceQuery.length > 0 133 | ? new URLSearchParams(this.resourceQuery) 134 | : null; 135 | 136 | if (parsedQuery) { 137 | const presetName = parsedQuery.get("as"); 138 | 139 | if (presetName) { 140 | if (!generator) { 141 | callback( 142 | new Error( 143 | "Please specify the 'generator' option to use 'as' query param for generation purposes.", 144 | ), 145 | ); 146 | 147 | return; 148 | } 149 | 150 | const presets = generator.filter((item) => item.preset === presetName); 151 | 152 | if (presets.length > 1) { 153 | callback( 154 | new Error( 155 | "Found several identical preset names, the 'preset' option should be unique", 156 | ), 157 | ); 158 | 159 | return; 160 | } 161 | 162 | if (presets.length === 0) { 163 | callback( 164 | new Error( 165 | `Can't find '${presetName}' preset in the 'generator' option`, 166 | ), 167 | ); 168 | 169 | return; 170 | } 171 | 172 | [transformer] = presets; 173 | } 174 | } 175 | 176 | if (!transformer) { 177 | callback(null, content); 178 | 179 | return; 180 | } 181 | 182 | if (parsedQuery) { 183 | const widthQuery = parsedQuery.get("width") ?? parsedQuery.get("w"); 184 | const heightQuery = parsedQuery.get("height") ?? parsedQuery.get("h"); 185 | const unitQuery = parsedQuery.get("unit") ?? parsedQuery.get("u"); 186 | 187 | if (widthQuery || heightQuery || unitQuery) { 188 | if (Array.isArray(transformer)) { 189 | transformer = processSizeQuery( 190 | transformer, 191 | widthQuery, 192 | heightQuery, 193 | unitQuery, 194 | ); 195 | } else { 196 | [transformer] = processSizeQuery( 197 | [transformer], 198 | widthQuery, 199 | heightQuery, 200 | unitQuery, 201 | ); 202 | } 203 | } 204 | } 205 | 206 | const filename = 207 | ABSOLUTE_URL_REGEX.test(this.resourcePath) && 208 | !WINDOWS_PATH_REGEX.test(this.resourcePath) 209 | ? this.resourcePath 210 | : path.relative(this.rootContext, this.resourcePath); 211 | 212 | const minifyOptions = 213 | /** @type {import("./index").InternalWorkerOptions} */ ({ 214 | input: content, 215 | filename, 216 | severityError, 217 | transformer, 218 | generateFilename: 219 | /** @type {Compilation} */ 220 | (this._compilation).getAssetPath.bind(this._compilation), 221 | }); 222 | 223 | const output = await worker(minifyOptions); 224 | 225 | if (output.errors && output.errors.length > 0) { 226 | for (const error of output.errors) { 227 | this.emitError(error); 228 | } 229 | 230 | callback(null, content); 231 | 232 | return; 233 | } 234 | 235 | if (output.warnings && output.warnings.length > 0) { 236 | for (const warning of output.warnings) { 237 | this.emitWarning(warning); 238 | } 239 | } 240 | 241 | // Change content of the data URI after minimizer 242 | if (this._module?.resourceResolveData?.encodedContent) { 243 | const isBase64 = /^base64$/i.test( 244 | /** @type string */ 245 | (this._module.resourceResolveData.encoding), 246 | ); 247 | 248 | this._module.resourceResolveData.encodedContent = isBase64 249 | ? output.data.toString("base64") 250 | : encodeURIComponent(output.data.toString("utf8")).replaceAll( 251 | /[!'()*]/g, 252 | (character) => 253 | `%${/** @type {number} */ (character.codePointAt(0)).toString(16)}`, 254 | ); 255 | } else { 256 | let query = this.resourceQuery; 257 | 258 | if (parsedQuery) { 259 | // Remove query param from the bundle due we need that only for bundle purposes 260 | for (const key of ["as", "width", "w", "height", "h"]) { 261 | parsedQuery.delete(key); 262 | } 263 | 264 | query = parsedQuery.toString(); 265 | query = query.length > 0 ? `?${query}` : ""; 266 | } 267 | 268 | // Old approach for `file-loader` and other old loaders 269 | changeResource(this, output, query); 270 | 271 | // Change name of assets modules after generator 272 | if (this._module && !this._module.matchResource) { 273 | this._module.matchResource = `${output.filename}${query}`; 274 | } 275 | } 276 | 277 | if (this._module) { 278 | IMAGE_MINIMIZER_PLUGIN_INFO_MAPPINGS.set(this._module, output.info); 279 | } 280 | 281 | callback(null, output.data); 282 | } 283 | 284 | loader.raw = true; 285 | 286 | module.exports = loader; 287 | -------------------------------------------------------------------------------- /test/__snapshots__/validate-plugin-options.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`validate plugin options should work 1`] = ` 4 | "Invalid options object. Image 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/image-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[`validate plugin options should work 2`] = ` 21 | "Invalid options object. Image 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/image-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[`validate plugin options should work 3`] = ` 36 | "Invalid options object. Image 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/image-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[`validate plugin options should work 4`] = ` 53 | "Invalid options object. Image 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/image-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[`validate plugin options should work 5`] = ` 68 | "Invalid options object. Image 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/image-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[`validate plugin options should work 6`] = ` 85 | "Invalid options object. Image 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/image-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[`validate plugin options should work 7`] = ` 100 | "Invalid options object. Image Minimizer Plugin has been initialized using an options object that does not match the API schema. 101 | - options.severityError should be one of these: 102 | "off" | "warning" | "error" 103 | -> Allows to choose how errors are displayed. 104 | -> Read more at https://github.com/webpack/image-minimizer-webpack-plugin#severityerror" 105 | `; 106 | 107 | exports[`validate plugin options should work 8`] = ` 108 | "Invalid options object. Image Minimizer Plugin has been initialized using an options object that does not match the API schema. 109 | - options.severityError should be one of these: 110 | "off" | "warning" | "error" 111 | -> Allows to choose how errors are displayed. 112 | -> Read more at https://github.com/webpack/image-minimizer-webpack-plugin#severityerror" 113 | `; 114 | 115 | exports[`validate plugin options should work 9`] = ` 116 | "Invalid options object. Image Minimizer Plugin has been initialized using an options object that does not match the API schema. 117 | - options.severityError should be one of these: 118 | "off" | "warning" | "error" 119 | -> Allows to choose how errors are displayed. 120 | -> Read more at https://github.com/webpack/image-minimizer-webpack-plugin#severityerror" 121 | `; 122 | 123 | exports[`validate plugin options should work 10`] = ` 124 | "Invalid options object. Image Minimizer Plugin has been initialized using an options object that does not match the API schema. 125 | - options.deleteOriginalAssets should be a boolean. 126 | -> Allows to remove original assets after minimization. 127 | -> Read more at https://github.com/webpack/image-minimizer-webpack-plugin#deleteoriginalassets" 128 | `; 129 | 130 | exports[`validate plugin options should work 11`] = ` 131 | "Invalid options object. Image Minimizer Plugin has been initialized using an options object that does not match the API schema. 132 | - options.minimizer should be one of these: 133 | [object { implementation?, filter?, filename?, options? }, ...] (should not have fewer than 1 item) | object { implementation?, filter?, filename?, options? } 134 | -> Allows you to setup the minimizer function and options. 135 | -> Read more at https://github.com/webpack/image-minimizer-webpack-plugin#minimizer 136 | Details: 137 | * options.minimizer should be an array: 138 | [object { implementation?, filter?, filename?, options? }, ...] (should not have fewer than 1 item) 139 | * options.minimizer should be an object: 140 | object { implementation?, filter?, filename?, options? }" 141 | `; 142 | 143 | exports[`validate plugin options should work 12`] = ` 144 | "Invalid options object. Image Minimizer Plugin has been initialized using an options object that does not match the API schema. 145 | - options.minimizer.filter should be an instance of function. 146 | -> Allows filtering of images." 147 | `; 148 | 149 | exports[`validate plugin options should work 13`] = ` 150 | "Invalid options object. Image Minimizer Plugin has been initialized using an options object that does not match the API schema. 151 | - options.minimizer[0].filter should be an instance of function. 152 | -> Allows filtering of images." 153 | `; 154 | 155 | exports[`validate plugin options should work 14`] = ` 156 | "Invalid options object. Image Minimizer Plugin has been initialized using an options object that does not match the API schema. 157 | - options.minimizer should be one of these: 158 | [object { implementation?, filter?, filename?, options? }, ...] (should not have fewer than 1 item) | object { implementation?, filter?, filename?, options? } 159 | -> Allows you to setup the minimizer function and options. 160 | -> Read more at https://github.com/webpack/image-minimizer-webpack-plugin#minimizer 161 | Details: 162 | * options.minimizer.filename should be one of these: 163 | non-empty string | function 164 | -> Allows to set the filename for the minimized asset. 165 | Details: 166 | * options.minimizer.filename should be a non-empty string. 167 | * options.minimizer.filename should be an instance of function." 168 | `; 169 | 170 | exports[`validate plugin options should work 15`] = ` 171 | "Invalid options object. Image Minimizer Plugin has been initialized using an options object that does not match the API schema. 172 | - options.minimizer should be one of these: 173 | [object { implementation?, filter?, filename?, options? }, ...] (should not have fewer than 1 item) | object { implementation?, filter?, filename?, options? } 174 | -> Allows you to setup the minimizer function and options. 175 | -> Read more at https://github.com/webpack/image-minimizer-webpack-plugin#minimizer 176 | Details: 177 | * options.minimizer.filename should be one of these: 178 | non-empty string | function 179 | -> Allows to set the filename for the minimized asset. 180 | Details: 181 | * options.minimizer.filename should be a non-empty string. 182 | * options.minimizer.filename should be an instance of function." 183 | `; 184 | 185 | exports[`validate plugin options should work 16`] = ` 186 | "Invalid options object. Image Minimizer Plugin has been initialized using an options object that does not match the API schema. 187 | - options.generator should be an array: 188 | [object { implementation, type?, preset?, options?, filter?, filename? }, ...] (should not have fewer than 1 item) 189 | -> Allows you to setup the generator function and options. 190 | -> Read more at https://github.com/webpack/image-minimizer-webpack-plugin#generator" 191 | `; 192 | 193 | exports[`validate plugin options should work 17`] = ` 194 | "Invalid options object. Image Minimizer Plugin has been initialized using an options object that does not match the API schema. 195 | - options.generator[0].filename should be one of these: 196 | non-empty string | function 197 | -> Allows to set the filename for the minimized asset. 198 | Details: 199 | * options.generator[0].filename should be a non-empty string. 200 | * options.generator[0].filename should be an instance of function." 201 | `; 202 | 203 | exports[`validate plugin options should work 18`] = ` 204 | "Invalid options object. Image Minimizer Plugin has been initialized using an options object that does not match the API schema. 205 | - options.generator[0].filter should be an instance of function. 206 | -> Allows filtering of images." 207 | `; 208 | 209 | exports[`validate plugin options should work 19`] = ` 210 | "Invalid options object. Image Minimizer Plugin has been initialized using an options object that does not match the API schema. 211 | - options.generator[0].type should be one of these: 212 | "import" | "asset" 213 | -> Type of generation 214 | - options.generator[0].filename should be one of these: 215 | non-empty string | function 216 | -> Allows to set the filename for the minimized asset. 217 | Details: 218 | * options.generator[0].filename should be a non-empty string. 219 | * options.generator[0].filename should be an instance of function." 220 | `; 221 | 222 | exports[`validate plugin options should work 20`] = ` 223 | "Invalid options object. Image Minimizer Plugin has been initialized using an options object that does not match the API schema. 224 | - options.concurrency should be a number. 225 | -> Number of concurrency optimization processes in one time. 226 | -> Read more at https://github.com/webpack/image-minimizer-webpack-plugin#concurrency" 227 | `; 228 | 229 | exports[`validate plugin options should work 21`] = ` 230 | "Invalid options object. Image Minimizer Plugin has been initialized using an options object that does not match the API schema. 231 | - options.concurrency should be a number. 232 | -> Number of concurrency optimization processes in one time. 233 | -> Read more at https://github.com/webpack/image-minimizer-webpack-plugin#concurrency" 234 | `; 235 | 236 | exports[`validate plugin options should work 22`] = ` 237 | "Invalid options object. Image Minimizer Plugin has been initialized using an options object that does not match the API schema. 238 | - options.loader should be a boolean. 239 | -> Automatically adding \`imagemin-loader\` (require for minification images using in \`url-loader\`, \`svg-url-loader\` or other). 240 | -> Read more at https://github.com/webpack/image-minimizer-webpack-plugin#loader" 241 | `; 242 | -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import path from "node:path"; 3 | 4 | import MiniCssExtractPlugin from "mini-css-extract-plugin"; 5 | import webpack from "webpack"; 6 | 7 | import ImageMinimizerPlugin from "../src/index"; 8 | 9 | import EmitPlugin from "./fixtures/EmitWepbackPlugin"; 10 | 11 | const plugins = ["gifsicle", "mozjpeg", "pngquant", "svgo"]; 12 | 13 | const fixturesPath = path.join(__dirname, "./fixtures"); 14 | 15 | /** 16 | * @param {import("webpack").Compiler} compiler The webpack compiler 17 | * @returns {Promise} The compilation stats 18 | */ 19 | function compile(compiler) { 20 | return new Promise((resolve, reject) => { 21 | compiler.run((err, stats) => { 22 | if (err) { 23 | return reject(err); 24 | } 25 | 26 | return resolve(stats); 27 | }); 28 | }); 29 | } 30 | 31 | /** 32 | * @param {unknown} maybeOptions The webpack options 33 | * @param {boolean} getCompiler Whether to get compiler 34 | * @returns {Promise} The compilation result 35 | */ 36 | async function runWebpack(maybeOptions, getCompiler = false) { 37 | const maybeMultiCompiler = Array.isArray(maybeOptions) 38 | ? maybeOptions 39 | : [maybeOptions]; 40 | 41 | const configs = []; 42 | const CopyPlugin = (await import("copy-webpack-plugin")).default; 43 | const { temporaryDirectory } = await import("tempy"); 44 | 45 | for (const options of maybeMultiCompiler) { 46 | const config = { 47 | externals: options.externals, 48 | experiments: options.experiments, 49 | devtool: false, 50 | bail: options.bail, 51 | context: fixturesPath, 52 | entry: options.entry || path.join(fixturesPath, "./loader.js"), 53 | mode: options.mode || "development", 54 | optimization: options.optimization, 55 | cache: options.cache, 56 | module: { 57 | rules: [ 58 | ...(!options.fileLoaderOff 59 | ? [ 60 | { 61 | test: options.test || /\.(jpe?g|png|gif|svg|webp)$/i, 62 | use: [ 63 | { 64 | loader: "file-loader", 65 | options: { 66 | name: options.name || "[path][name].[ext]", 67 | }, 68 | }, 69 | ], 70 | }, 71 | ] 72 | : []), 73 | ...(options.MCEP 74 | ? [ 75 | { 76 | test: /\.css$/, 77 | use: [ 78 | { 79 | loader: MiniCssExtractPlugin.loader, 80 | }, 81 | "css-loader", 82 | ], 83 | }, 84 | ] 85 | : []), 86 | ...(options.childPlugin 87 | ? [ 88 | { 89 | test: /child-compilation\.js$/, 90 | loader: path.resolve( 91 | __dirname, 92 | "./fixtures/emit-asset-in-child-compilation-loader.js", 93 | ), 94 | }, 95 | ] 96 | : []), 97 | ...(options.emitAssetPlugin 98 | ? [ 99 | { 100 | test: /simple-emit\.js$/, 101 | loader: path.resolve( 102 | __dirname, 103 | "./fixtures/emitAssetLoader.js", 104 | ), 105 | }, 106 | ] 107 | : []), 108 | ...(options.assetResource 109 | ? [ 110 | { 111 | test: /\.(jpe?g|png|gif|svg)$/i, 112 | type: "asset/resource", 113 | }, 114 | ] 115 | : []), 116 | ...(options.assetInline 117 | ? [ 118 | { 119 | test: /\.(jpe?g|png|gif|svg)$/i, 120 | type: "asset/inline", 121 | }, 122 | ] 123 | : []), 124 | ], 125 | }, 126 | output: { 127 | publicPath: "", 128 | filename: "bundle.js", 129 | pathinfo: false, 130 | assetModuleFilename: options.name || "[name][ext]", 131 | path: 132 | options.output && options.output.path 133 | ? options.output.path 134 | : temporaryDirectory(), 135 | }, 136 | plugins: [], 137 | }; 138 | 139 | if (options.experiments) { 140 | config.experiments = options.experiments; 141 | } 142 | 143 | if (options.output && options.output.assetModuleFilename) { 144 | config.output.assetModuleFilename = options.output.assetModuleFilename; 145 | } 146 | 147 | if (options.imageminLoaderOptions) { 148 | if (config.module.rules[0].use) { 149 | config.module.rules[0].use = [ 150 | ...config.module.rules[0].use, 151 | { 152 | loader: ImageMinimizerPlugin.loader, 153 | options: options.imageminLoaderOptions, 154 | }, 155 | ]; 156 | } else { 157 | config.module.rules.push({ 158 | test: /\.(jpe?g|png|gif|svg)$/i, 159 | loader: ImageMinimizerPlugin.loader, 160 | options: options.imageminLoaderOptions, 161 | }); 162 | } 163 | } 164 | 165 | if (options.emitPlugin || options.emitPluginOptions) { 166 | config.plugins = [ 167 | ...config.plugins, 168 | new EmitPlugin(options.emitPluginOptions), 169 | ]; 170 | } 171 | 172 | if (options.imageminPlugin || options.imageminPluginOptions) { 173 | const imageminPluginsOptions = 174 | Array.isArray(options.imageminPlugin) || 175 | Array.isArray(options.imageminPluginOptions) 176 | ? options.imageminPlugin || options.imageminPluginOptions 177 | : [options.imageminPlugin || options.imageminPluginOptions]; 178 | 179 | for (const imageminPluginOptions of imageminPluginsOptions) { 180 | const ImageMinimizerPluginCreated = new ImageMinimizerPlugin( 181 | typeof imageminPluginOptions === "boolean" 182 | ? { 183 | minimizerOptions: { 184 | plugins, 185 | }, 186 | } 187 | : imageminPluginOptions, 188 | ); 189 | 190 | if (options.asMinimizer) { 191 | if (!config.optimization) { 192 | config.optimization = {}; 193 | } 194 | 195 | config.optimization.minimize = true; 196 | config.optimization.minimizer = [ImageMinimizerPluginCreated]; 197 | } else { 198 | config.plugins = [...config.plugins, ImageMinimizerPluginCreated]; 199 | } 200 | } 201 | } 202 | 203 | if (options.MCEP) { 204 | config.plugins = [ 205 | ...config.plugins, 206 | new MiniCssExtractPlugin({ 207 | // Options similar to the same options in webpackOptions.output 208 | // both options are optional 209 | filename: "[name].css", 210 | chunkFilename: "[id].css", 211 | }), 212 | ]; 213 | } 214 | 215 | if (options.copyPlugin) { 216 | config.plugins = [ 217 | ...config.plugins, 218 | new CopyPlugin({ 219 | patterns: [{ from: "plugin-test.jpg" }], 220 | }), 221 | ]; 222 | } 223 | 224 | if (options.EmitNewAssetPlugin) { 225 | config.plugins = [ 226 | ...config.plugins, 227 | // eslint-disable-next-line no-use-before-define 228 | new EmitNewAssetPlugin({ 229 | name: "newImg.png", 230 | }), 231 | ]; 232 | } 233 | 234 | configs.push(config); 235 | } 236 | 237 | if (getCompiler) { 238 | return webpack(configs.length === 1 ? configs[0] : configs); 239 | } 240 | 241 | return new Promise((resolve, reject) => { 242 | webpack(configs.length === 1 ? configs[0] : configs, (err, stats) => { 243 | if (err) { 244 | reject(err); 245 | return; 246 | } 247 | 248 | resolve(stats); 249 | }); 250 | }); 251 | } 252 | 253 | /** 254 | * @param {string | string[]} originalPath The original path 255 | * @param {import("webpack").Compilation} compilation The compilation 256 | * @returns {Promise} Whether the asset is optimized 257 | */ 258 | async function isOptimized(originalPath, compilation) { 259 | const { assets } = compilation; 260 | let name = originalPath; 261 | let realName = originalPath; 262 | 263 | if (Array.isArray(originalPath)) { 264 | [name, realName] = originalPath; 265 | } 266 | 267 | const source = assets[name]; 268 | 269 | if (!source) { 270 | throw new Error("Can't find asset"); 271 | } 272 | 273 | const { path: outputPath } = compilation.options.output; 274 | const pathToOriginal = path.join(fixturesPath, realName); 275 | const pathToEmitted = path.join(outputPath, name); 276 | 277 | const imagemin = (await import("imagemin")).default; 278 | const imageminSvgo = (await import("imagemin-svgo")).default; 279 | const imageminGifsicle = (await import("imagemin-gifsicle")).default; 280 | const imageminMozjpeg = (await import("imagemin-mozjpeg")).default; 281 | const imageminPngquant = (await import("imagemin-pngquant")).default; 282 | const data = await fs.promises.readFile(pathToOriginal); 283 | const optimizedBuffer = Buffer.from( 284 | await imagemin.buffer(data, { 285 | plugins: [ 286 | imageminGifsicle(), 287 | imageminMozjpeg(), 288 | imageminPngquant(), 289 | imageminSvgo(), 290 | ], 291 | }), 292 | ); 293 | const generatedBuffer = await fs.promises.readFile(pathToEmitted); 294 | 295 | return optimizedBuffer.equals(generatedBuffer); 296 | } 297 | 298 | /** 299 | * @param {string} id The module id 300 | * @param {import("webpack").Module[]} modules The modules 301 | * @returns {boolean} Whether the module has the loader 302 | */ 303 | function hasLoader(id, modules) { 304 | return [...modules].some((module) => { 305 | if (!module.id.endsWith(id)) { 306 | return false; 307 | } 308 | 309 | const { loaders } = module; 310 | 311 | return loaders.find( 312 | (loader) => loader.loader === ImageMinimizerPlugin.loader, 313 | ); 314 | }); 315 | } 316 | 317 | /** 318 | * @param {string} asset The asset name 319 | * @param {import("webpack").Compiler} compiler The compiler 320 | * @param {import("webpack").Stats} stats The stats 321 | * @returns {string} The asset content 322 | */ 323 | function readAsset(asset, compiler, stats) { 324 | const usedFs = compiler.outputFileSystem; 325 | const outputPath = stats.compilation.outputOptions.path; 326 | 327 | let data = ""; 328 | let targetFile = asset; 329 | 330 | const queryStringIdx = targetFile.indexOf("?"); 331 | 332 | if (queryStringIdx >= 0) { 333 | targetFile = targetFile.slice(0, Math.max(0, queryStringIdx)); 334 | } 335 | 336 | try { 337 | data = usedFs.readFileSync(path.join(outputPath, targetFile)); 338 | } catch (error) { 339 | data = error.toString(); 340 | } 341 | 342 | return data; 343 | } 344 | 345 | /** 346 | * @param {string} string The string to normalize 347 | * @returns {string} The normalized path 348 | */ 349 | function normalizePath(string) { 350 | const isWin = process.platform === "win32"; 351 | 352 | if (isWin) { 353 | return string.replaceAll("\\", "/"); 354 | } 355 | 356 | return string; 357 | } 358 | 359 | /** 360 | * @param {string} dirPath The directory path 361 | * @returns {void} 362 | */ 363 | function clearDirectory(dirPath) { 364 | let files; 365 | 366 | try { 367 | files = fs.readdirSync(dirPath); 368 | } catch { 369 | return; 370 | } 371 | 372 | if (files.length > 0) { 373 | for (let i = 0; i < files.length; i++) { 374 | const filePath = `${dirPath}/${files[i]}`; 375 | if (fs.statSync(filePath).isFile()) { 376 | fs.unlinkSync(filePath); 377 | } else { 378 | clearDirectory(filePath); 379 | } 380 | } 381 | } 382 | 383 | fs.rmdirSync(dirPath); 384 | } 385 | 386 | /** 387 | * @param {boolean | () => boolean} predicate The predicate condition 388 | * @returns {import("@jest/globals").it | import("@jest/globals").xit} The test function 389 | */ 390 | function ifit(predicate) { 391 | const cond = typeof predicate === "function" ? predicate() : predicate; 392 | /* global it */ 393 | return cond ? it : it.skip; 394 | } 395 | 396 | /** 397 | * @returns {boolean} Whether squoosh tests should be run 398 | */ 399 | function needSquooshTest() { 400 | const needTest = typeof process.env.SQUOOSH_TEST !== "undefined"; 401 | 402 | // Disable tests for all and Nodejs > 16 403 | // see: https://github.com/webpack/image-minimizer-webpack-plugin/pull/345 404 | return needTest; 405 | } 406 | 407 | export default class EmitNewAssetPlugin { 408 | constructor(options = {}) { 409 | this.options = options; 410 | } 411 | 412 | apply(compiler) { 413 | const pluginName = this.constructor.name; 414 | 415 | const { RawSource } = compiler.webpack.sources; 416 | 417 | compiler.hooks.compilation.tap(pluginName, (compilation) => { 418 | compilation.hooks.processAssets.tap( 419 | { 420 | name: pluginName, 421 | stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_REPORT, 422 | }, 423 | () => { 424 | const file = fs.readFileSync( 425 | path.resolve(__dirname, "fixtures", "newImg.png"), 426 | ); 427 | 428 | compilation.emitAsset(this.options.name, new RawSource(file)); 429 | }, 430 | ); 431 | }); 432 | } 433 | } 434 | 435 | export { 436 | EmitNewAssetPlugin, 437 | clearDirectory, 438 | compile, 439 | fixturesPath, 440 | hasLoader, 441 | ifit, 442 | isOptimized, 443 | needSquooshTest, 444 | normalizePath, 445 | plugins, 446 | readAsset, 447 | runWebpack, 448 | }; 449 | -------------------------------------------------------------------------------- /test/__snapshots__/validate-loader-options.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`validate loader options should throw an error on the "generator" option with "[]" value 1`] = ` 4 | "Invalid options object. Image Minimizer Plugin Loader has been initialized using an options object that does not match the API schema. 5 | - options.generator should be a non-empty array. 6 | -> Allows you to setup the generator function and options. 7 | -> Read more at https://github.com/webpack/image-minimizer-webpack-plugin#generator" 8 | `; 9 | 10 | exports[`validate loader options should throw an error on the "generator" option with "[{"preset":"webp","filename":true}]" value 1`] = ` 11 | "Invalid options object. Image Minimizer Plugin Loader has been initialized using an options object that does not match the API schema. 12 | - options.generator[0].filename should be one of these: 13 | non-empty string | function 14 | -> Allows to set the filename for the minimized asset. 15 | Details: 16 | * options.generator[0].filename should be a non-empty string. 17 | * options.generator[0].filename should be an instance of function." 18 | `; 19 | 20 | exports[`validate loader options should throw an error on the "generator" option with "[{"preset":"webp","filter":true}]" value 1`] = ` 21 | "Invalid options object. Image Minimizer Plugin Loader has been initialized using an options object that does not match the API schema. 22 | - options.generator[0].filter should be an instance of function. 23 | -> Allows filtering of images." 24 | `; 25 | 26 | exports[`validate loader options should throw an error on the "generator" option with "[{"preset":"webp"}]" value 1`] = ` 27 | "Invalid options object. Image Minimizer Plugin Loader has been initialized using an options object that does not match the API schema. 28 | - options.generator[0] misses the property 'implementation'. Should be: 29 | function 30 | -> Implementation of the generator function." 31 | `; 32 | 33 | exports[`validate loader options should throw an error on the "generator" option with "1" value 1`] = ` 34 | "Invalid options object. Image Minimizer Plugin Loader has been initialized using an options object that does not match the API schema. 35 | - options.generator should be an array: 36 | [object { implementation, preset, type?, options?, filter?, filename? }, ...] (should not have fewer than 1 item) 37 | -> Allows you to setup the generator function and options. 38 | -> Read more at https://github.com/webpack/image-minimizer-webpack-plugin#generator" 39 | `; 40 | 41 | exports[`validate loader options should throw an error on the "generator" option with "false" value 1`] = ` 42 | "Invalid options object. Image Minimizer Plugin Loader has been initialized using an options object that does not match the API schema. 43 | - options.generator should be an array: 44 | [object { implementation, preset, type?, options?, filter?, filename? }, ...] (should not have fewer than 1 item) 45 | -> Allows you to setup the generator function and options. 46 | -> Read more at https://github.com/webpack/image-minimizer-webpack-plugin#generator" 47 | `; 48 | 49 | exports[`validate loader options should throw an error on the "generator" option with "null" value 1`] = ` 50 | "Invalid options object. Image Minimizer Plugin Loader has been initialized using an options object that does not match the API schema. 51 | - options.generator should be an array: 52 | [object { implementation, preset, type?, options?, filter?, filename? }, ...] (should not have fewer than 1 item) 53 | -> Allows you to setup the generator function and options. 54 | -> Read more at https://github.com/webpack/image-minimizer-webpack-plugin#generator" 55 | `; 56 | 57 | exports[`validate loader options should throw an error on the "generator" option with "true" value 1`] = ` 58 | "Invalid options object. Image Minimizer Plugin Loader has been initialized using an options object that does not match the API schema. 59 | - options.generator should be an array: 60 | [object { implementation, preset, type?, options?, filter?, filename? }, ...] (should not have fewer than 1 item) 61 | -> Allows you to setup the generator function and options. 62 | -> Read more at https://github.com/webpack/image-minimizer-webpack-plugin#generator" 63 | `; 64 | 65 | exports[`validate loader options should throw an error on the "minimizer" option with "[]" value 1`] = ` 66 | "Invalid options object. Image Minimizer Plugin Loader has been initialized using an options object that does not match the API schema. 67 | - options.minimizer should be a non-empty array." 68 | `; 69 | 70 | exports[`validate loader options should throw an error on the "minimizer" option with "{"filename":true}" value 1`] = ` 71 | "Invalid options object. Image Minimizer Plugin Loader has been initialized using an options object that does not match the API schema. 72 | - options.minimizer should be one of these: 73 | [object { implementation?, options?, filter?, filename? }, ...] (should not have fewer than 1 item) | object { implementation?, options?, filter?, filename? } 74 | -> Allows you to setup the minimizer function and options. 75 | -> Read more at https://github.com/webpack/image-minimizer-webpack-plugin#minimizer 76 | Details: 77 | * options.minimizer.filename should be one of these: 78 | non-empty string | function 79 | -> Allows to set the filename for the minimized asset. 80 | Details: 81 | * options.minimizer.filename should be a non-empty string. 82 | * options.minimizer.filename should be an instance of function." 83 | `; 84 | 85 | exports[`validate loader options should throw an error on the "minimizer" option with "{"filter":true}" value 1`] = ` 86 | "Invalid options object. Image Minimizer Plugin Loader has been initialized using an options object that does not match the API schema. 87 | - options.minimizer.filter should be an instance of function. 88 | -> Allows filtering of images." 89 | `; 90 | 91 | exports[`validate loader options should throw an error on the "minimizer" option with "1" value 1`] = ` 92 | "Invalid options object. Image Minimizer Plugin Loader has been initialized using an options object that does not match the API schema. 93 | - options.minimizer should be one of these: 94 | [object { implementation?, options?, filter?, filename? }, ...] (should not have fewer than 1 item) | object { implementation?, options?, filter?, filename? } 95 | -> Allows you to setup the minimizer function and options. 96 | -> Read more at https://github.com/webpack/image-minimizer-webpack-plugin#minimizer 97 | Details: 98 | * options.minimizer should be an array: 99 | [object { implementation?, options?, filter?, filename? }, ...] (should not have fewer than 1 item) 100 | * options.minimizer should be an object: 101 | object { implementation?, options?, filter?, filename? }" 102 | `; 103 | 104 | exports[`validate loader options should throw an error on the "minimizer" option with "false" value 1`] = ` 105 | "Invalid options object. Image Minimizer Plugin Loader has been initialized using an options object that does not match the API schema. 106 | - options.minimizer should be one of these: 107 | [object { implementation?, options?, filter?, filename? }, ...] (should not have fewer than 1 item) | object { implementation?, options?, filter?, filename? } 108 | -> Allows you to setup the minimizer function and options. 109 | -> Read more at https://github.com/webpack/image-minimizer-webpack-plugin#minimizer 110 | Details: 111 | * options.minimizer should be an array: 112 | [object { implementation?, options?, filter?, filename? }, ...] (should not have fewer than 1 item) 113 | * options.minimizer should be an object: 114 | object { implementation?, options?, filter?, filename? }" 115 | `; 116 | 117 | exports[`validate loader options should throw an error on the "minimizer" option with "null" value 1`] = ` 118 | "Invalid options object. Image Minimizer Plugin Loader has been initialized using an options object that does not match the API schema. 119 | - options.minimizer should be one of these: 120 | [object { implementation?, options?, filter?, filename? }, ...] (should not have fewer than 1 item) | object { implementation?, options?, filter?, filename? } 121 | -> Allows you to setup the minimizer function and options. 122 | -> Read more at https://github.com/webpack/image-minimizer-webpack-plugin#minimizer 123 | Details: 124 | * options.minimizer should be an array: 125 | [object { implementation?, options?, filter?, filename? }, ...] (should not have fewer than 1 item) 126 | * options.minimizer should be an object: 127 | object { implementation?, options?, filter?, filename? }" 128 | `; 129 | 130 | exports[`validate loader options should throw an error on the "minimizer" option with "true" value 1`] = ` 131 | "Invalid options object. Image Minimizer Plugin Loader has been initialized using an options object that does not match the API schema. 132 | - options.minimizer should be one of these: 133 | [object { implementation?, options?, filter?, filename? }, ...] (should not have fewer than 1 item) | object { implementation?, options?, filter?, filename? } 134 | -> Allows you to setup the minimizer function and options. 135 | -> Read more at https://github.com/webpack/image-minimizer-webpack-plugin#minimizer 136 | Details: 137 | * options.minimizer should be an array: 138 | [object { implementation?, options?, filter?, filename? }, ...] (should not have fewer than 1 item) 139 | * options.minimizer should be an object: 140 | object { implementation?, options?, filter?, filename? }" 141 | `; 142 | 143 | exports[`validate loader options should throw an error on the "severityError" option with "() => {}" value 1`] = ` 144 | "Invalid options object. Image Minimizer Plugin Loader has been initialized using an options object that does not match the API schema. 145 | - options.severityError should be one of these: 146 | "off" | "warning" | "error" 147 | -> Allows to choose how errors are displayed. 148 | -> Read more at https://github.com/webpack/image-minimizer-webpack-plugin#severityerror" 149 | `; 150 | 151 | exports[`validate loader options should throw an error on the "severityError" option with "[]" value 1`] = ` 152 | "Invalid options object. Image Minimizer Plugin Loader has been initialized using an options object that does not match the API schema. 153 | - options.severityError should be one of these: 154 | "off" | "warning" | "error" 155 | -> Allows to choose how errors are displayed. 156 | -> Read more at https://github.com/webpack/image-minimizer-webpack-plugin#severityerror" 157 | `; 158 | 159 | exports[`validate loader options should throw an error on the "severityError" option with "{}" value 1`] = ` 160 | "Invalid options object. Image Minimizer Plugin Loader has been initialized using an options object that does not match the API schema. 161 | - options.severityError should be one of these: 162 | "off" | "warning" | "error" 163 | -> Allows to choose how errors are displayed. 164 | -> Read more at https://github.com/webpack/image-minimizer-webpack-plugin#severityerror" 165 | `; 166 | 167 | exports[`validate loader options should throw an error on the "severityError" option with "false" value 1`] = ` 168 | "Invalid options object. Image Minimizer Plugin Loader has been initialized using an options object that does not match the API schema. 169 | - options.severityError should be one of these: 170 | "off" | "warning" | "error" 171 | -> Allows to choose how errors are displayed. 172 | -> Read more at https://github.com/webpack/image-minimizer-webpack-plugin#severityerror" 173 | `; 174 | 175 | exports[`validate loader options should throw an error on the "severityError" option with "true" value 1`] = ` 176 | "Invalid options object. Image Minimizer Plugin Loader has been initialized using an options object that does not match the API schema. 177 | - options.severityError should be one of these: 178 | "off" | "warning" | "error" 179 | -> Allows to choose how errors are displayed. 180 | -> Read more at https://github.com/webpack/image-minimizer-webpack-plugin#severityerror" 181 | `; 182 | 183 | exports[`validate loader options should throw an error on the "unknown" option with "/test/" value 1`] = ` 184 | "Invalid options object. Image Minimizer Plugin Loader has been initialized using an options object that does not match the API schema. 185 | - options has an unknown property 'unknown'. These properties are valid: 186 | object { minimizer?, generator?, severityError? }" 187 | `; 188 | 189 | exports[`validate loader options should throw an error on the "unknown" option with "[]" value 1`] = ` 190 | "Invalid options object. Image Minimizer Plugin Loader has been initialized using an options object that does not match the API schema. 191 | - options has an unknown property 'unknown'. These properties are valid: 192 | object { minimizer?, generator?, severityError? }" 193 | `; 194 | 195 | exports[`validate loader options should throw an error on the "unknown" option with "{"foo":"bar"}" value 1`] = ` 196 | "Invalid options object. Image Minimizer Plugin Loader has been initialized using an options object that does not match the API schema. 197 | - options has an unknown property 'unknown'. These properties are valid: 198 | object { minimizer?, generator?, severityError? }" 199 | `; 200 | 201 | exports[`validate loader options should throw an error on the "unknown" option with "{}" value 1`] = ` 202 | "Invalid options object. Image Minimizer Plugin Loader has been initialized using an options object that does not match the API schema. 203 | - options has an unknown property 'unknown'. These properties are valid: 204 | object { minimizer?, generator?, severityError? }" 205 | `; 206 | 207 | exports[`validate loader options should throw an error on the "unknown" option with "1" value 1`] = ` 208 | "Invalid options object. Image Minimizer Plugin Loader has been initialized using an options object that does not match the API schema. 209 | - options has an unknown property 'unknown'. These properties are valid: 210 | object { minimizer?, generator?, severityError? }" 211 | `; 212 | 213 | exports[`validate loader options should throw an error on the "unknown" option with "false" value 1`] = ` 214 | "Invalid options object. Image Minimizer Plugin Loader has been initialized using an options object that does not match the API schema. 215 | - options has an unknown property 'unknown'. These properties are valid: 216 | object { minimizer?, generator?, severityError? }" 217 | `; 218 | 219 | exports[`validate loader options should throw an error on the "unknown" option with "test" value 1`] = ` 220 | "Invalid options object. Image Minimizer Plugin Loader has been initialized using an options object that does not match the API schema. 221 | - options has an unknown property 'unknown'. These properties are valid: 222 | object { minimizer?, generator?, severityError? }" 223 | `; 224 | 225 | exports[`validate loader options should throw an error on the "unknown" option with "true" value 1`] = ` 226 | "Invalid options object. Image Minimizer Plugin Loader has been initialized using an options object that does not match the API schema. 227 | - options has an unknown property 'unknown'. These properties are valid: 228 | object { minimizer?, generator?, severityError? }" 229 | `; 230 | -------------------------------------------------------------------------------- /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 | ### [4.1.4](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/compare/v4.1.3...v4.1.4) (2025-08-14) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * concurrency default value to avoid overload CPU ([#479](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/issues/479)) ([c6f0ea3](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/commit/c6f0ea3338415519c31efb04515b34fb237ef3d4)) 11 | * types ([#477](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/issues/477)) ([3b61bc8](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/commit/3b61bc8e3e061ebaa4de1211cf3abb45eb453064)) 12 | 13 | ### [4.1.3](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/compare/v4.1.2...v4.1.3) (2024-12-18) 14 | 15 | 16 | ### Bug Fixes 17 | 18 | * crash when using filesystem cache ([#461](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/issues/461)) ([383d3d3](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/commit/383d3d3262b9bc44dcd7af527c8b82da4ecbd4aa)) 19 | 20 | ### [4.1.2](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/compare/v4.1.1...v4.1.2) (2024-12-18) 21 | 22 | 23 | ### Bug Fixes 24 | 25 | * crash when using filesystem cache ([#460](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/issues/460)) ([7627f0e](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/commit/7627f0ea2142440ad8c8f29fbbd2649fbf20b382)) 26 | 27 | ### [4.1.1](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/compare/v4.1.0...v4.1.1) (2024-11-19) 28 | 29 | 30 | ### Bug Fixes 31 | 32 | * better way to avoid optimize twice ([#457](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/issues/457)) ([1b2d40e](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/commit/1b2d40ed143a6c3ed1ef9db188dd48655de2e97b)) 33 | 34 | ## [4.1.0](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/compare/v4.0.2...v4.1.0) (2024-07-26) 35 | 36 | 37 | ### Features 38 | 39 | * add unit for percentage resize with sharp ([a83f491](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/commit/a83f4919bec28804fe3e8a03a453a30d3537aff6)) 40 | 41 | ### [4.0.2](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/compare/v4.0.1...v4.0.2) (2024-06-04) 42 | 43 | 44 | ### Bug Fixes 45 | 46 | * compatibility with `imagemin@9` ([#444](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/issues/444)) ([859c346](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/commit/859c346f35b28f230c627409cd91be0dd8bbf72f)) 47 | 48 | ### [4.0.1](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/compare/v4.0.0...v4.0.1) (2024-05-29) 49 | 50 | 51 | ### Bug Fixes 52 | 53 | * pass the original image path to svgo ([#441](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/issues/441)) ([d5522a6](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/commit/d5522a6326b52184b663b0c02035ce9464144307)) 54 | 55 | ## [4.0.0](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/compare/v3.8.3...v4.0.0) (2024-01-16) 56 | 57 | 58 | ### ⚠ BREAKING CHANGES 59 | 60 | * minimum supported Node.js version is `18.12.0` ([d3f2531](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/commit/d3f2531241c04e6115dfb7bab3baef1e04da3410)) 61 | 62 | ### [3.8.3](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/compare/v3.8.2...v3.8.3) (2023-06-17) 63 | 64 | 65 | ### Bug Fixes 66 | 67 | * lazy loading deps ([#411](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/issues/411)) ([93412bb](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/commit/93412bbed4ff96015d884e537ea81ceea5c285f9)) 68 | 69 | ### [3.8.2](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/compare/v3.8.1...v3.8.2) (2023-03-10) 70 | 71 | 72 | ### Bug Fixes 73 | 74 | * support for generating images from `svg` (`sharp`) ([5076734](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/commit/5076734ef843df125de765e8eb02ecf987ab839c)) 75 | 76 | ### [3.8.1](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/compare/v3.8.0...v3.8.1) (2022-11-13) 77 | 78 | 79 | ### Bug Fixes 80 | 81 | * fix some possible issues with file paths ([#377](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/issues/377)) ([e40308a](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/commit/e40308a8b6b83456aa9a15fbb572e6267b7b1763)) 82 | 83 | ## [3.8.0](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/compare/v3.7.0...v3.8.0) (2022-11-01) 84 | 85 | 86 | ### Features 87 | 88 | * add `svgo` implementation ([#369](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/issues/369)) ([0701188](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/commit/07011881677f0e4ba3e9ab0d8cbe7e7f3e3b8d59)) 89 | 90 | ## [3.7.0](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/compare/v3.6.1...v3.7.0) (2022-10-26) 91 | 92 | 93 | ### Features 94 | 95 | * added `info.sourceFilename` ([f8b3378](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/commit/f8b337833759835f3f67277c8bfab85986416de9)) 96 | 97 | ### [3.6.1](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/compare/v3.6.0...v3.6.1) (2022-09-19) 98 | 99 | 100 | ### Bug Fixes 101 | 102 | * fix resize option enabled flag (`squoosh`) ([#356](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/issues/356)) ([b2a5015](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/commit/b2a50151b9dcd9f79307695e52a2b12db72a0a7e)) 103 | * support for animated images (`sharp`) ([#358](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/issues/358)) ([3c30355](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/commit/3c3035536303f95cc74ebaed5247731789422965)) 104 | * throw an error on unsupported image formats (`sharp`) ([#359](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/issues/359)) ([c0b193b](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/commit/c0b193b38d4e488ca0651f2bea13065700cb3bf1)) 105 | 106 | ## [3.6.0](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/compare/v3.5.0...v3.6.0) (2022-09-16) 107 | 108 | 109 | ### Features 110 | 111 | * supported more resize options (only `sharp`) ([#355](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/issues/355)) ([d365db3](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/commit/d365db3db18476435cf8952aaa23801dd7e466ee)) 112 | 113 | ## [3.5.0](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/compare/v3.4.0...v3.5.0) (2022-09-15) 114 | 115 | 116 | ### Features 117 | 118 | * add `width`/`w` and `height`/`h` query parameters to resize image ([52ee1c8](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/commit/52ee1c84cf935e2ebbf4bfe38567a0cb73bd6c13)) 119 | 120 | 121 | ### Bug Fixes 122 | 123 | * `implementation` types ([#353](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/issues/353)) ([a57fcdf](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/commit/a57fcdfc7121f1ceda178dd7987623433745b21e)) 124 | 125 | ## [3.4.0](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/compare/v3.3.1...v3.4.0) (2022-09-09) 126 | 127 | 128 | ### Features 129 | 130 | * add `[width]` and `[height]` placeholders for the `filename` option ([#346](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/issues/346)) ([682c22b](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/commit/682c22b81f2f1af96e83f0e0805fd5406a209324)) 131 | 132 | 133 | ### Bug Fixes 134 | 135 | * types ([cd7c7a7](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/commit/cd7c7a7c7f12eb883399cf01ad7c9102e90b845b)) 136 | 137 | ### [3.3.1](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/compare/v3.3.0...v3.3.1) (2022-09-05) 138 | 139 | 140 | ### Bug Fixes 141 | 142 | * assets info for sharp ([#338](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/issues/338)) ([c897d30](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/commit/c897d30bed8532fec1312be62483281589402b0b)) 143 | * avoid renaming unsupported formats ([#339](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/issues/339)) ([18e30ef](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/commit/18e30ef3da70b39384f389e6729d56fb5b24af59)) 144 | * sharp types ([#337](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/issues/337)) ([ae3a03b](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/commit/ae3a03b926a6bce29dee2829490a99d16394a501)) 145 | 146 | ## [3.3.0](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/compare/v3.2.3...v3.3.0) (2022-08-12) 147 | 148 | 149 | ### Features 150 | 151 | * add `sharp` minifier/generator implementation ([#329](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/issues/329)) ([5c440f6](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/commit/5c440f6e8257fe0a4ebabcbe22a09063902a6c5e)) 152 | 153 | ### [3.2.3](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/compare/v3.2.2...v3.2.3) (2022-01-13) 154 | 155 | 156 | ### Bug Fixes 157 | 158 | * types ([#297](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/issues/297)) ([c61642f](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/commit/c61642f85b9dc17d45d79a42760c48fe41ffcd27)) 159 | 160 | ### [3.2.2](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/compare/v3.2.1...v3.2.2) (2022-01-07) 161 | 162 | 163 | ### Bug Fixes 164 | 165 | * perf for `squoosh` ([#295](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/issues/295)) ([2f4d1a2](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/commit/2f4d1a291e30b737ebff118804f7fee93c90fcd1)) 166 | 167 | ### [3.2.1](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/compare/v3.2.0...v3.2.1) (2022-01-03) 168 | 169 | 170 | ### Bug Fixes 171 | 172 | * memory leaking ([#293](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/issues/293)) ([043e571](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/commit/043e57114d701cf9dfe87b9dda3b185b99cbd399)) 173 | * respect encoding of data uri ([#294](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/issues/294)) ([a89b316](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/commit/a89b3164a41f403a2a48d18cb7f9b92353dd18b7)) 174 | 175 | ## [3.2.0](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/compare/v3.1.2...v3.2.0) (2021-12-25) 176 | 177 | 178 | ### Features 179 | 180 | * allow generating images from copied assets using the `type` option for the `generator` option ([fab9103](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/commit/fab910337ef3c119f991f0d71c682d5ab3a65b5c)) 181 | 182 | ### [3.1.2](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/compare/v3.1.1...v3.1.2) (2021-12-17) 183 | 184 | 185 | ### Bug Fixes 186 | 187 | * improve perf ([#285](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/issues/285)) ([435879d](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/commit/435879dd74528850e0ade0ec24c9db968cbc7344)) 188 | 189 | ### [3.1.1](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/compare/v3.1.0...v3.1.1) (2021-12-17) 190 | 191 | 192 | ### Bug Fixes 193 | 194 | * ignore unsupported data URI by mime type ([#284](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/issues/284)) ([d1b68c2](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/commit/d1b68c204ab604b37effc6614e939e2e36662095)) 195 | 196 | ## [3.1.0](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/compare/v3.0.1...v3.1.0) (2021-12-16) 197 | 198 | 199 | ### Features 200 | 201 | * removed cjs wrapper and generated types in commonjs format (`export =` and `namespaces` used in types), now you can directly use exported types ([#282](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/issues/282)) ([f0fa0a7](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/commit/f0fa0a7fb2531d9e78e37778dae5c0b267724c1b)) 202 | 203 | ### [3.0.1](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/compare/v3.0.0...v3.0.1) (2021-12-07) 204 | 205 | 206 | ### Bug Fixes 207 | 208 | * reduced memory consumption for `squoosh` ([#279](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/issues/279)) ([0d597b7](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/commit/0d597b751ca5eda293929ce8d71349572fbf0fb8)) 209 | * types ([028fad3](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/commit/028fad3403c890d691ebd636c7f55f6bf801a3b7)) 210 | 211 | ## [3.0.0](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/compare/v2.2.0...v3.0.0) (2021-12-05) 212 | 213 | There are a lot of breaking changes, the plugin has been completely rewritten, see the documentation for information and examples. 214 | 215 | ### ⚠ BREAKING CHANGES 216 | 217 | * minimum supported Node.js version is `12.13.0`, `imagemin` uses dynamic `import()` to load plugins, so your Node.js version should support it 218 | * by default, we don't install `imagemin`, so you need to run `npm i -D imagemin` to install `imagemin` 219 | * union `minify` and `minizerOptions` in one option - `minimizer`, you can use `minimizer.implementation` and `minimizer.options` to specify minimizer and options 220 | * image generation was rewritten, please use the `generator` option to configure image generation and use `new URL("./image.png?as=webp")`/`div { backgaround: url("./image.png?as=webp"); }`/etc in code to enable it (`import` and `require` are supported too) 221 | * `filter` and `filename` option was moved in the `minimizer`/`generator` option 222 | * `imageminNormalizeConfig` is now async function 223 | * default value of the `severityError` option is `"error"`, removed values: `true`, `false` and `auto` 224 | * don't add `.` (dot) before `[ext]` in the `filename` option 225 | 226 | ### Features 227 | 228 | * added `squoosh` support 229 | * added the `minimizer` option for image optimization 230 | * added the `generator` option for image generation 231 | * added ability to use multiple `minimizer` option feature 232 | * allow the `filename` option will be `Function` 233 | * improve error reporting 234 | * improve types 235 | * output helpful descriptions and links on errors 236 | * improve stats output 237 | 238 | ### Bug Fixes 239 | 240 | * support esm `imagemin` plugin 241 | * supports absolute URLs, i.e. `data:`/`http:`/`https:`/`file:` 242 | * double minification and memory leak 243 | * respect original errors 244 | * compatibility with asset modules 245 | 246 | ## [2.2.0](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/compare/v2.1.0...v2.2.0) (2021-01-09) 247 | 248 | 249 | ### Features 250 | 251 | * run optimize image assets added later by plugins ([#178](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/issues/178)) ([4939f93](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/commit/4939f93a55962c5812a693acc5eb441b78fe663c)) 252 | 253 | ## [2.1.0](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/compare/v2.0.0...v2.1.0) (2020-12-23) 254 | 255 | 256 | ### Features 257 | 258 | * add TypeScript definitions ([e78497b](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/commit/e78497b3f50d2cfc6368fcdc9de548a7ad76f559)) 259 | 260 | ## 2.0.0 (2020-12-17) 261 | 262 | 263 | ### ⚠ BREAKING CHANGES 264 | 265 | * minimum supported `webpack` version is `5.1.0` 266 | * removed the `cache` option in favor the [`cache`](https://webpack.js.org/configuration/other-options/#cache) option from webpack 267 | 268 | ## 1.0.0 (2020-10-07) 269 | 270 | Initial release. 271 | -------------------------------------------------------------------------------- /test/loader-minimizer-option.test.js: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import path from "node:path"; 3 | import { imageSize } from "image-size"; 4 | 5 | import ImageMinimizerPlugin from "../src"; 6 | 7 | import { 8 | ifit, 9 | isOptimized, 10 | needSquooshTest, 11 | plugins, 12 | runWebpack, 13 | } from "./helpers"; 14 | 15 | describe("loader minimizer option", () => { 16 | it("should work with object notation of the 'minifier' option", async () => { 17 | const stats = await runWebpack({ 18 | imageminLoader: true, 19 | imageminLoaderOptions: { 20 | minimizer: { 21 | implementation: ImageMinimizerPlugin.imageminMinify, 22 | options: { 23 | plugins: ["gifsicle", "mozjpeg", "pngquant", "svgo"], 24 | }, 25 | }, 26 | }, 27 | }); 28 | const { compilation } = stats; 29 | const { warnings, errors } = compilation; 30 | 31 | expect(warnings).toHaveLength(0); 32 | expect(errors).toHaveLength(0); 33 | 34 | await expect(isOptimized("loader-test.gif", compilation)).resolves.toBe( 35 | true, 36 | ); 37 | await expect(isOptimized("loader-test.jpg", compilation)).resolves.toBe( 38 | true, 39 | ); 40 | await expect(isOptimized("loader-test.png", compilation)).resolves.toBe( 41 | true, 42 | ); 43 | await expect(isOptimized("loader-test.svg", compilation)).resolves.toBe( 44 | true, 45 | ); 46 | }); 47 | 48 | it("should work with array notation of the 'minifier' option", async () => { 49 | const stats = await runWebpack({ 50 | imageminLoader: true, 51 | imageminLoaderOptions: { 52 | minimizer: [ 53 | { 54 | implementation: ImageMinimizerPlugin.imageminMinify, 55 | options: { 56 | plugins: ["gifsicle", "mozjpeg", "pngquant", "svgo"], 57 | }, 58 | }, 59 | ], 60 | }, 61 | }); 62 | const { compilation } = stats; 63 | const { warnings, errors } = compilation; 64 | 65 | expect(warnings).toHaveLength(0); 66 | expect(errors).toHaveLength(0); 67 | 68 | await expect(isOptimized("loader-test.gif", compilation)).resolves.toBe( 69 | true, 70 | ); 71 | await expect(isOptimized("loader-test.jpg", compilation)).resolves.toBe( 72 | true, 73 | ); 74 | await expect(isOptimized("loader-test.png", compilation)).resolves.toBe( 75 | true, 76 | ); 77 | await expect(isOptimized("loader-test.svg", compilation)).resolves.toBe( 78 | true, 79 | ); 80 | }); 81 | 82 | it("should work with array notation of the 'minifier' option #2", async () => { 83 | expect.assertions(14); 84 | 85 | const stats = await runWebpack({ 86 | imageminLoader: true, 87 | imageminLoaderOptions: { 88 | minimizer: [ 89 | { 90 | implementation: ImageMinimizerPlugin.imageminMinify, 91 | options: { 92 | plugins: ["gifsicle", "mozjpeg", "pngquant", "svgo"], 93 | }, 94 | }, 95 | { 96 | implementation: (input, minifiOptions) => { 97 | expect(input).toBeDefined(); 98 | expect(minifiOptions).toBeUndefined(); 99 | 100 | return input; 101 | }, 102 | }, 103 | ], 104 | }, 105 | }); 106 | const { compilation } = stats; 107 | const { warnings, errors } = compilation; 108 | 109 | expect(warnings).toHaveLength(0); 110 | expect(errors).toHaveLength(0); 111 | 112 | await expect(isOptimized("loader-test.gif", compilation)).resolves.toBe( 113 | true, 114 | ); 115 | await expect(isOptimized("loader-test.jpg", compilation)).resolves.toBe( 116 | true, 117 | ); 118 | await expect(isOptimized("loader-test.png", compilation)).resolves.toBe( 119 | true, 120 | ); 121 | await expect(isOptimized("loader-test.svg", compilation)).resolves.toBe( 122 | true, 123 | ); 124 | }); 125 | 126 | it("should work with array notation of the 'minifier' option #3", async () => { 127 | expect.assertions(14); 128 | 129 | const stats = await runWebpack({ 130 | imageminLoader: true, 131 | imageminLoaderOptions: { 132 | minimizer: [ 133 | { 134 | implementation: ImageMinimizerPlugin.imageminMinify, 135 | options: { 136 | plugins: ["gifsicle", "mozjpeg", "pngquant", "svgo"], 137 | }, 138 | }, 139 | { 140 | implementation: (input, minifiOptions) => { 141 | expect("options2" in minifiOptions).toBe(true); 142 | 143 | return input; 144 | }, 145 | options: { 146 | options2: "passed", 147 | }, 148 | }, 149 | { 150 | implementation: (input, minifiOptions) => { 151 | expect("options3" in minifiOptions).toBe(true); 152 | 153 | return input; 154 | }, 155 | options: { 156 | options3: "passed", 157 | }, 158 | }, 159 | ], 160 | }, 161 | }); 162 | const { compilation } = stats; 163 | const { warnings, errors } = compilation; 164 | 165 | expect(warnings).toHaveLength(0); 166 | expect(errors).toHaveLength(0); 167 | 168 | await expect(isOptimized("loader-test.gif", compilation)).resolves.toBe( 169 | true, 170 | ); 171 | await expect(isOptimized("loader-test.jpg", compilation)).resolves.toBe( 172 | true, 173 | ); 174 | await expect(isOptimized("loader-test.png", compilation)).resolves.toBe( 175 | true, 176 | ); 177 | await expect(isOptimized("loader-test.svg", compilation)).resolves.toBe( 178 | true, 179 | ); 180 | }); 181 | 182 | it("should work when minify is custom function", async () => { 183 | expect.assertions(14); 184 | 185 | const stats = await runWebpack({ 186 | imageminLoader: true, 187 | imageminLoaderOptions: { 188 | minimizer: { 189 | implementation: (input, minifiOptions) => { 190 | expect(input).toBeDefined(); 191 | expect(minifiOptions).toBeDefined(); 192 | 193 | return input; 194 | }, 195 | options: { 196 | plugins: ["gifsicle", "mozjpeg", "pngquant", "svgo"], 197 | }, 198 | }, 199 | }, 200 | }); 201 | const { compilation } = stats; 202 | const { warnings, errors } = compilation; 203 | 204 | expect(warnings).toHaveLength(0); 205 | expect(errors).toHaveLength(0); 206 | 207 | await expect(isOptimized("loader-test.gif", compilation)).resolves.toBe( 208 | false, 209 | ); 210 | await expect(isOptimized("loader-test.jpg", compilation)).resolves.toBe( 211 | false, 212 | ); 213 | await expect(isOptimized("loader-test.png", compilation)).resolves.toBe( 214 | false, 215 | ); 216 | await expect(isOptimized("loader-test.svg", compilation)).resolves.toBe( 217 | false, 218 | ); 219 | }); 220 | 221 | it("should emit errors", async () => { 222 | const stats = await runWebpack({ 223 | imageminLoader: true, 224 | imageminLoaderOptions: { 225 | minimizer: { 226 | implementation: () => { 227 | throw new Error("test error"); 228 | }, 229 | }, 230 | }, 231 | }); 232 | const { compilation } = stats; 233 | const { warnings, errors } = compilation; 234 | 235 | expect(errors).toHaveLength(4); 236 | expect(warnings).toHaveLength(0); 237 | 238 | await expect(isOptimized("loader-test.gif", compilation)).resolves.toBe( 239 | false, 240 | ); 241 | await expect(isOptimized("loader-test.jpg", compilation)).resolves.toBe( 242 | false, 243 | ); 244 | await expect(isOptimized("loader-test.png", compilation)).resolves.toBe( 245 | false, 246 | ); 247 | await expect(isOptimized("loader-test.svg", compilation)).resolves.toBe( 248 | false, 249 | ); 250 | }); 251 | 252 | it('should work with "imageminMinify" minifier', async () => { 253 | const stats = await runWebpack({ 254 | imageminLoader: true, 255 | imageminLoaderOptions: { 256 | minimizer: { 257 | implementation: ImageMinimizerPlugin.imageminMinify, 258 | options: { 259 | plugins: ["gifsicle", "mozjpeg", "pngquant", "svgo"], 260 | }, 261 | }, 262 | }, 263 | }); 264 | const { compilation } = stats; 265 | const { errors } = compilation; 266 | 267 | expect(errors).toHaveLength(0); 268 | expect(compilation.getAsset("loader-test.jpg").info.size).toBeLessThan(631); 269 | expect(compilation.getAsset("loader-test.png").info.size).toBeLessThan( 270 | 71000, 271 | ); 272 | }); 273 | 274 | ifit(needSquooshTest)( 275 | 'should work with "squooshMinify" minifier', 276 | async () => { 277 | const stats = await runWebpack({ 278 | imageminLoader: true, 279 | imageminLoaderOptions: { 280 | minimizer: { 281 | implementation: ImageMinimizerPlugin.squooshMinify, 282 | }, 283 | }, 284 | }); 285 | const { compilation } = stats; 286 | const { errors } = compilation; 287 | 288 | expect(errors).toHaveLength(0); 289 | expect(compilation.getAsset("loader-test.jpg").info.size).toBeLessThan( 290 | 631, 291 | ); 292 | expect(compilation.getAsset("loader-test.png").info.size).toBeLessThan( 293 | 71000, 294 | ); 295 | }, 296 | ); 297 | 298 | it('should work with "sharpMinify" minifier', async () => { 299 | const stats = await runWebpack({ 300 | imageminLoader: true, 301 | imageminLoaderOptions: { 302 | minimizer: { 303 | implementation: ImageMinimizerPlugin.sharpMinify, 304 | }, 305 | }, 306 | }); 307 | const { compilation } = stats; 308 | const { errors } = compilation; 309 | 310 | expect(errors).toHaveLength(0); 311 | expect(compilation.getAsset("loader-test.jpg").info.size).toBeLessThan(631); 312 | expect(compilation.getAsset("loader-test.png").info.size).toBeLessThan( 313 | 71000, 314 | ); 315 | }); 316 | 317 | it("should optimizes all images exclude filtered", async () => { 318 | const stats = await runWebpack({ 319 | imageminLoaderOptions: { 320 | minimizer: { 321 | implementation: ImageMinimizerPlugin.imageminMinify, 322 | filter: (source, filename) => { 323 | expect(source).toBeInstanceOf(Buffer); 324 | expect(typeof filename).toBe("string"); 325 | 326 | if (source.byteLength === 631) { 327 | return false; 328 | } 329 | 330 | return true; 331 | }, 332 | options: { plugins }, 333 | }, 334 | }, 335 | }); 336 | 337 | const { compilation } = stats; 338 | const { warnings, errors, assets } = compilation; 339 | 340 | expect(warnings).toHaveLength(0); 341 | expect(errors).toHaveLength(0); 342 | expect(Object.keys(assets)).toHaveLength(5); 343 | 344 | await expect(isOptimized("loader-test.gif", compilation)).resolves.toBe( 345 | true, 346 | ); 347 | await expect(isOptimized("loader-test.png", compilation)).resolves.toBe( 348 | true, 349 | ); 350 | await expect(isOptimized("loader-test.svg", compilation)).resolves.toBe( 351 | true, 352 | ); 353 | await expect(isOptimized("loader-test.jpg", compilation)).resolves.toBe( 354 | false, 355 | ); 356 | }); 357 | 358 | it("should optimizes all images exclude filtered for #2", async () => { 359 | const stats = await runWebpack({ 360 | imageminLoaderOptions: { 361 | minimizer: [ 362 | { 363 | implementation: ImageMinimizerPlugin.imageminMinify, 364 | filter: (source, filename) => { 365 | expect(source).toBeInstanceOf(Buffer); 366 | expect(typeof filename).toBe("string"); 367 | 368 | if (source.byteLength === 631) { 369 | return false; 370 | } 371 | 372 | return true; 373 | }, 374 | options: { plugins }, 375 | }, 376 | ], 377 | }, 378 | }); 379 | 380 | const { compilation } = stats; 381 | const { warnings, errors, assets } = compilation; 382 | 383 | expect(warnings).toHaveLength(0); 384 | expect(errors).toHaveLength(0); 385 | expect(Object.keys(assets)).toHaveLength(5); 386 | 387 | await expect(isOptimized("loader-test.gif", compilation)).resolves.toBe( 388 | true, 389 | ); 390 | await expect(isOptimized("loader-test.png", compilation)).resolves.toBe( 391 | true, 392 | ); 393 | await expect(isOptimized("loader-test.svg", compilation)).resolves.toBe( 394 | true, 395 | ); 396 | await expect(isOptimized("loader-test.jpg", compilation)).resolves.toBe( 397 | false, 398 | ); 399 | }); 400 | 401 | it("should optimizes all images exclude filtered for #3", async () => { 402 | const stats = await runWebpack({ 403 | imageminLoaderOptions: { 404 | minimizer: [ 405 | { 406 | implementation: ImageMinimizerPlugin.imageminMinify, 407 | options: { plugins }, 408 | filter: (source, filename) => { 409 | expect(source).toBeInstanceOf(Buffer); 410 | expect(typeof filename).toBe("string"); 411 | 412 | if (source.byteLength === 631 || /\.png$/.test(filename)) { 413 | return false; 414 | } 415 | 416 | return true; 417 | }, 418 | }, 419 | { 420 | implementation: ImageMinimizerPlugin.imageminMinify, 421 | options: { plugins }, 422 | }, 423 | ], 424 | }, 425 | }); 426 | 427 | const { compilation } = stats; 428 | const { warnings, errors, assets } = compilation; 429 | 430 | expect(warnings).toHaveLength(0); 431 | expect(errors).toHaveLength(0); 432 | expect(Object.keys(assets)).toHaveLength(5); 433 | 434 | await expect(isOptimized("loader-test.gif", compilation)).resolves.toBe( 435 | true, 436 | ); 437 | await expect(isOptimized("loader-test.png", compilation)).resolves.toBe( 438 | true, 439 | ); 440 | await expect(isOptimized("loader-test.svg", compilation)).resolves.toBe( 441 | true, 442 | ); 443 | await expect(isOptimized("loader-test.jpg", compilation)).resolves.toBe( 444 | true, 445 | ); 446 | }); 447 | 448 | ifit(needSquooshTest)( 449 | 'should minimize and resize with "squooshMinify" minifier', 450 | async () => { 451 | const stats = await runWebpack({ 452 | imageminLoader: true, 453 | imageminLoaderOptions: { 454 | minimizer: { 455 | implementation: ImageMinimizerPlugin.squooshMinify, 456 | options: { 457 | resize: { 458 | enabled: true, 459 | width: 100, 460 | height: 50, 461 | }, 462 | rotate: { 463 | numRotations: 90, 464 | }, 465 | }, 466 | }, 467 | }, 468 | }); 469 | const { compilation } = stats; 470 | const { errors } = compilation; 471 | 472 | const transformedAsset = path.resolve( 473 | __dirname, 474 | compilation.options.output.path, 475 | "./loader-test.png", 476 | ); 477 | const dimensions = imageSize(await fs.readFile(transformedAsset)); 478 | 479 | expect(dimensions.height).toBe(50); 480 | expect(dimensions.width).toBe(100); 481 | expect(errors).toHaveLength(0); 482 | expect(compilation.getAsset("loader-test.jpg").info.size).toBeLessThan( 483 | 631, 484 | ); 485 | expect(compilation.getAsset("loader-test.png").info.size).toBeLessThan( 486 | 71000, 487 | ); 488 | }, 489 | ); 490 | 491 | it('should minimize and resize with "sharpMinify" minifier', async () => { 492 | const stats = await runWebpack({ 493 | imageminLoader: true, 494 | imageminLoaderOptions: { 495 | minimizer: { 496 | implementation: ImageMinimizerPlugin.sharpMinify, 497 | options: { 498 | resize: { 499 | enabled: true, 500 | width: 100, 501 | height: 50, 502 | }, 503 | rotate: { 504 | numRotations: 90, 505 | }, 506 | }, 507 | }, 508 | }, 509 | }); 510 | const { compilation } = stats; 511 | const { errors } = compilation; 512 | 513 | const transformedAsset = path.resolve( 514 | __dirname, 515 | compilation.options.output.path, 516 | "./loader-test.png", 517 | ); 518 | const dimensions = imageSize(await fs.readFile(transformedAsset)); 519 | 520 | expect(dimensions.height).toBe(50); 521 | expect(dimensions.width).toBe(100); 522 | expect(errors).toHaveLength(0); 523 | expect(compilation.getAsset("loader-test.jpg").info.size).toBeLessThan(631); 524 | expect(compilation.getAsset("loader-test.png").info.size).toBeLessThan( 525 | 71000, 526 | ); 527 | }); 528 | }); 529 | -------------------------------------------------------------------------------- /test/loader-generator-option.test.js: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import path from "node:path"; 3 | import fileType from "file-type"; 4 | import { imageSize } from "image-size"; 5 | import ImageMinimizerPlugin from "../src"; 6 | 7 | import { fixturesPath, ifit, needSquooshTest, runWebpack } from "./helpers"; 8 | 9 | jest.setTimeout(10000); 10 | 11 | describe("loader generator option", () => { 12 | it("should work", async () => { 13 | const stats = await runWebpack({ 14 | entry: path.join(fixturesPath, "./loader-single.js"), 15 | imageminLoaderOptions: { 16 | generator: [ 17 | { 18 | preset: "webp", 19 | implementation: ImageMinimizerPlugin.sharpGenerate, 20 | options: { 21 | encodeOptions: { 22 | webp: { 23 | lossless: true, 24 | }, 25 | }, 26 | }, 27 | }, 28 | ], 29 | }, 30 | }); 31 | 32 | const { compilation } = stats; 33 | const { warnings, errors } = compilation; 34 | 35 | const transformedAsset = path.resolve( 36 | __dirname, 37 | compilation.options.output.path, 38 | "./nested/deep/loader-test.webp", 39 | ); 40 | 41 | const transformedExt = await fileType.fromFile(transformedAsset); 42 | 43 | expect(/image\/webp/i.test(transformedExt.mime)).toBe(true); 44 | expect(warnings).toHaveLength(0); 45 | expect(errors).toHaveLength(0); 46 | }); 47 | 48 | it("should throw error on duplicate presets", async () => { 49 | const stats = await runWebpack({ 50 | entry: path.join(fixturesPath, "./loader-single.js"), 51 | imageminLoaderOptions: { 52 | generator: [ 53 | { 54 | preset: "webp", 55 | implementation: ImageMinimizerPlugin.sharpGenerate, 56 | options: { 57 | encodeOptions: { 58 | webp: { 59 | lossless: true, 60 | }, 61 | }, 62 | }, 63 | }, 64 | { 65 | preset: "webp", 66 | implementation: ImageMinimizerPlugin.sharpGenerate, 67 | options: { 68 | encodeOptions: { 69 | webp: { 70 | lossless: true, 71 | }, 72 | }, 73 | }, 74 | }, 75 | ], 76 | }, 77 | }); 78 | 79 | const { compilation } = stats; 80 | const { warnings, errors } = compilation; 81 | 82 | expect(warnings).toHaveLength(0); 83 | expect(errors).toHaveLength(1); 84 | expect(errors[0].message).toMatch( 85 | /Found several identical preset names, the 'preset' option should be unique/, 86 | ); 87 | }); 88 | 89 | it("should throw error on not found preset", async () => { 90 | const stats = await runWebpack({ 91 | entry: path.join(fixturesPath, "./loader-single.js"), 92 | imageminLoaderOptions: { 93 | generator: [ 94 | { 95 | preset: "webpz", 96 | implementation: ImageMinimizerPlugin.sharpGenerate, 97 | options: { 98 | encodeOptions: { 99 | webp: { 100 | lossless: true, 101 | }, 102 | }, 103 | }, 104 | }, 105 | ], 106 | }, 107 | }); 108 | 109 | const { compilation } = stats; 110 | const { warnings, errors } = compilation; 111 | 112 | expect(warnings).toHaveLength(0); 113 | expect(errors).toHaveLength(1); 114 | expect(errors[0].message).toMatch( 115 | /Can't find 'webp' preset in the 'generator' option/, 116 | ); 117 | }); 118 | 119 | it("should generate the new webp image with other name using old loader approach", async () => { 120 | const stats = await runWebpack({ 121 | entry: path.join(fixturesPath, "./loader-single.js"), 122 | name: "foo-[name].[ext]", 123 | imageminLoaderOptions: { 124 | generator: [ 125 | { 126 | preset: "webp", 127 | implementation: ImageMinimizerPlugin.sharpGenerate, 128 | options: { 129 | encodeOptions: { 130 | webp: { 131 | lossless: true, 132 | }, 133 | }, 134 | }, 135 | }, 136 | ], 137 | }, 138 | }); 139 | 140 | const { compilation } = stats; 141 | const { warnings, errors } = compilation; 142 | 143 | const transformedAsset = path.resolve( 144 | __dirname, 145 | compilation.options.output.path, 146 | "foo-loader-test.webp", 147 | ); 148 | 149 | const transformedExt = await fileType.fromFile(transformedAsset); 150 | 151 | expect(/image\/webp/i.test(transformedExt.mime)).toBe(true); 152 | expect(warnings).toHaveLength(0); 153 | expect(errors).toHaveLength(0); 154 | }); 155 | 156 | it("should generate the new webp image with other name using asset modules name", async () => { 157 | const stats = await runWebpack({ 158 | entry: path.join(fixturesPath, "./loader-single.js"), 159 | fileLoaderOff: true, 160 | assetResource: true, 161 | name: "foo-[name][ext]", 162 | imageminLoaderOptions: { 163 | generator: [ 164 | { 165 | preset: "webp", 166 | implementation: ImageMinimizerPlugin.sharpGenerate, 167 | options: { 168 | encodeOptions: { 169 | webp: { 170 | lossless: true, 171 | }, 172 | }, 173 | }, 174 | }, 175 | ], 176 | }, 177 | }); 178 | 179 | const { compilation } = stats; 180 | const { warnings, errors } = compilation; 181 | 182 | const transformedAsset = path.resolve( 183 | __dirname, 184 | compilation.options.output.path, 185 | "foo-loader-test.webp", 186 | ); 187 | 188 | const transformedExt = await fileType.fromFile(transformedAsset); 189 | 190 | expect(/image\/webp/i.test(transformedExt.mime)).toBe(true); 191 | expect(warnings).toHaveLength(0); 192 | expect(errors).toHaveLength(0); 193 | }); 194 | 195 | ifit(needSquooshTest)( 196 | "should generate and resize (squooshGenerate)", 197 | async () => { 198 | const stats = await runWebpack({ 199 | entry: path.join(fixturesPath, "./loader-single.js"), 200 | imageminLoaderOptions: { 201 | generator: [ 202 | { 203 | preset: "webp", 204 | implementation: ImageMinimizerPlugin.squooshGenerate, 205 | options: { 206 | resize: { 207 | enabled: true, 208 | width: 100, 209 | height: 50, 210 | }, 211 | rotate: { 212 | numRotations: 90, 213 | }, 214 | encodeOptions: { 215 | webp: { 216 | lossless: 1, 217 | }, 218 | }, 219 | }, 220 | }, 221 | ], 222 | }, 223 | }); 224 | 225 | const { compilation } = stats; 226 | const { warnings, errors } = compilation; 227 | 228 | const transformedAsset = path.resolve( 229 | __dirname, 230 | compilation.options.output.path, 231 | "./nested/deep/loader-test.webp", 232 | ); 233 | 234 | const transformedExt = await fileType.fromFile(transformedAsset); 235 | const dimensions = imageSize(await fs.readFile(transformedAsset)); 236 | 237 | expect(dimensions.height).toBe(50); 238 | expect(dimensions.width).toBe(100); 239 | expect(/image\/webp/i.test(transformedExt.mime)).toBe(true); 240 | expect(warnings).toHaveLength(0); 241 | expect(errors).toHaveLength(0); 242 | }, 243 | ); 244 | 245 | ifit(needSquooshTest)( 246 | "should generate and not resize (squooshGenerate)", 247 | async () => { 248 | const stats = await runWebpack({ 249 | entry: path.join(fixturesPath, "./loader-single.js"), 250 | imageminLoaderOptions: { 251 | generator: [ 252 | { 253 | preset: "webp", 254 | implementation: ImageMinimizerPlugin.squooshGenerate, 255 | options: { 256 | resize: { 257 | enabled: false, 258 | width: 100, 259 | height: 50, 260 | }, 261 | rotate: { 262 | numRotations: 90, 263 | }, 264 | encodeOptions: { 265 | webp: { 266 | lossless: 1, 267 | }, 268 | }, 269 | }, 270 | }, 271 | ], 272 | }, 273 | }); 274 | 275 | const { compilation } = stats; 276 | const { warnings, errors } = compilation; 277 | 278 | const transformedAsset = path.resolve( 279 | __dirname, 280 | compilation.options.output.path, 281 | "./nested/deep/loader-test.webp", 282 | ); 283 | 284 | const transformedExt = await fileType.fromFile(transformedAsset); 285 | const dimensions = imageSize(await fs.readFile(transformedAsset)); 286 | 287 | expect(dimensions.height).toBe(1); 288 | expect(dimensions.width).toBe(1); 289 | expect(/image\/webp/i.test(transformedExt.mime)).toBe(true); 290 | expect(warnings).toHaveLength(0); 291 | expect(errors).toHaveLength(0); 292 | }, 293 | ); 294 | 295 | it("should generate and resize (sharpGenerate)", async () => { 296 | const stats = await runWebpack({ 297 | entry: path.join(fixturesPath, "./loader-single.js"), 298 | imageminLoaderOptions: { 299 | generator: [ 300 | { 301 | preset: "webp", 302 | implementation: ImageMinimizerPlugin.sharpGenerate, 303 | options: { 304 | resize: { 305 | enabled: true, 306 | width: 100, 307 | height: 50, 308 | }, 309 | encodeOptions: { 310 | webp: { 311 | lossless: true, 312 | }, 313 | }, 314 | }, 315 | }, 316 | ], 317 | }, 318 | }); 319 | 320 | const { compilation } = stats; 321 | const { warnings, errors } = compilation; 322 | 323 | const transformedAsset = path.resolve( 324 | __dirname, 325 | compilation.options.output.path, 326 | "./nested/deep/loader-test.webp", 327 | ); 328 | 329 | const transformedExt = await fileType.fromFile(transformedAsset); 330 | const dimensions = imageSize(await fs.readFile(transformedAsset)); 331 | 332 | expect(dimensions.height).toBe(50); 333 | expect(dimensions.width).toBe(100); 334 | expect(/image\/webp/i.test(transformedExt.mime)).toBe(true); 335 | expect(warnings).toHaveLength(0); 336 | expect(errors).toHaveLength(0); 337 | }); 338 | 339 | it("should generate and not resize (sharpGenerate)", async () => { 340 | const stats = await runWebpack({ 341 | entry: path.join(fixturesPath, "./loader-single.js"), 342 | imageminLoaderOptions: { 343 | generator: [ 344 | { 345 | preset: "webp", 346 | implementation: ImageMinimizerPlugin.sharpGenerate, 347 | options: { 348 | resize: { 349 | enabled: false, 350 | width: 100, 351 | height: 50, 352 | }, 353 | encodeOptions: { 354 | webp: { 355 | lossless: true, 356 | }, 357 | }, 358 | }, 359 | }, 360 | ], 361 | }, 362 | }); 363 | 364 | const { compilation } = stats; 365 | const { warnings, errors } = compilation; 366 | 367 | const transformedAsset = path.resolve( 368 | __dirname, 369 | compilation.options.output.path, 370 | "./nested/deep/loader-test.webp", 371 | ); 372 | 373 | const transformedExt = await fileType.fromFile(transformedAsset); 374 | const dimensions = imageSize(await fs.readFile(transformedAsset)); 375 | 376 | expect(dimensions.height).toBe(1); 377 | expect(dimensions.width).toBe(1); 378 | expect(/image\/webp/i.test(transformedExt.mime)).toBe(true); 379 | expect(warnings).toHaveLength(0); 380 | expect(errors).toHaveLength(0); 381 | }); 382 | 383 | it("should minify and rename (sharpMinify)", async () => { 384 | const stats = await runWebpack({ 385 | entry: path.join(fixturesPath, "./simple.js"), 386 | imageminLoaderOptions: { 387 | minimizer: { 388 | implementation: ImageMinimizerPlugin.sharpMinify, 389 | filename: "sharp-minify-[name]-[width]x[height][ext]", 390 | options: { 391 | encodeOptions: { 392 | jpeg: { quality: 90 }, 393 | }, 394 | }, 395 | }, 396 | }, 397 | }); 398 | 399 | const { compilation } = stats; 400 | const { warnings, errors } = compilation; 401 | 402 | const transformedAsset = path.resolve( 403 | __dirname, 404 | compilation.options.output.path, 405 | "./sharp-minify-loader-test-1x1.jpg", 406 | ); 407 | 408 | const transformedExt = await fileType.fromFile(transformedAsset); 409 | 410 | expect(/image\/jpeg/i.test(transformedExt.mime)).toBe(true); 411 | expect(warnings).toHaveLength(0); 412 | expect(errors).toHaveLength(0); 413 | }); 414 | 415 | it("should generate, resize and rename (sharpGenerate)", async () => { 416 | const stats = await runWebpack({ 417 | entry: path.join(fixturesPath, "./generator.js"), 418 | imageminLoaderOptions: { 419 | generator: [ 420 | { 421 | preset: "webp", 422 | implementation: ImageMinimizerPlugin.sharpGenerate, 423 | filename: "sharp-generate-[name]-[width]x[height][ext]", 424 | options: { 425 | resize: { 426 | enabled: true, 427 | width: 100, 428 | height: 50, 429 | }, 430 | encodeOptions: { 431 | webp: { 432 | lossless: true, 433 | }, 434 | }, 435 | }, 436 | }, 437 | ], 438 | }, 439 | }); 440 | 441 | const { compilation } = stats; 442 | const { warnings, errors } = compilation; 443 | 444 | const transformedAsset = path.resolve( 445 | __dirname, 446 | compilation.options.output.path, 447 | "./sharp-generate-loader-test-100x50.webp", 448 | ); 449 | 450 | const transformedExt = await fileType.fromFile(transformedAsset); 451 | const dimensions = imageSize(await fs.readFile(transformedAsset)); 452 | 453 | expect(dimensions.height).toBe(50); 454 | expect(dimensions.width).toBe(100); 455 | expect(/image\/webp/i.test(transformedExt.mime)).toBe(true); 456 | expect(warnings).toHaveLength(0); 457 | expect(errors).toHaveLength(0); 458 | }); 459 | 460 | it("should minify and rename", async () => { 461 | const stats = await runWebpack({ 462 | entry: path.join(fixturesPath, "./simple.js"), 463 | imageminLoaderOptions: { 464 | minimizer: { 465 | implementation: ImageMinimizerPlugin.sharpMinify, 466 | filename: "sharp-minify-[name]-[width]x[height][ext]", 467 | options: { 468 | encodeOptions: { 469 | jpeg: { quality: 90 }, 470 | }, 471 | }, 472 | }, 473 | }, 474 | }); 475 | 476 | const { compilation } = stats; 477 | const { warnings, errors } = compilation; 478 | 479 | const transformedAsset = path.resolve( 480 | __dirname, 481 | compilation.options.output.path, 482 | "./sharp-minify-loader-test-1x1.jpg", 483 | ); 484 | 485 | const transformedExt = await fileType.fromFile(transformedAsset); 486 | 487 | expect(/image\/jpeg/i.test(transformedExt.mime)).toBe(true); 488 | expect(warnings).toHaveLength(0); 489 | expect(errors).toHaveLength(0); 490 | }); 491 | 492 | it("should generate, resize and rename", async () => { 493 | const stats = await runWebpack({ 494 | entry: path.join(fixturesPath, "./generator.js"), 495 | imageminLoaderOptions: { 496 | generator: [ 497 | { 498 | preset: "webp", 499 | implementation: ImageMinimizerPlugin.sharpGenerate, 500 | filename: "sharp-generate-[name]-[width]x[height][ext]", 501 | options: { 502 | resize: { 503 | enabled: true, 504 | width: 100, 505 | height: 50, 506 | }, 507 | encodeOptions: { 508 | webp: { 509 | lossless: true, 510 | }, 511 | }, 512 | }, 513 | }, 514 | ], 515 | }, 516 | }); 517 | 518 | const { compilation } = stats; 519 | const { warnings, errors } = compilation; 520 | 521 | const transformedAsset = path.resolve( 522 | __dirname, 523 | compilation.options.output.path, 524 | "./sharp-generate-loader-test-100x50.webp", 525 | ); 526 | 527 | const transformedExt = await fileType.fromFile(transformedAsset); 528 | const dimensions = imageSize(await fs.readFile(transformedAsset)); 529 | 530 | expect(dimensions.height).toBe(50); 531 | expect(dimensions.width).toBe(100); 532 | expect(/image\/webp/i.test(transformedExt.mime)).toBe(true); 533 | expect(warnings).toHaveLength(0); 534 | expect(errors).toHaveLength(0); 535 | }); 536 | }); 537 | --------------------------------------------------------------------------------