├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── BUG.md │ ├── DOCS.md │ ├── FEATURE.md │ ├── MODIFICATION.md │ └── SUPPORT.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── node.js.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── CHANGELOG.md ├── LICENSE ├── README.md ├── azure-pipelines.yml ├── babel.config.js ├── commitlint.config.js ├── husky.config.js ├── lint-staged.config.js ├── package-lock.json ├── package.json ├── src ├── .gitignore ├── constants.js ├── index.js ├── lib │ ├── compress.js │ ├── emit.js │ └── inline.js └── options.json └── test ├── fixtures ├── images │ ├── jpg │ │ └── mini.jpg │ └── png │ │ ├── 1.png │ │ ├── 2.png │ │ └── 3.png └── index.js ├── helpers ├── compile.js ├── execute.js ├── getCompiler.js ├── getErrors.js ├── getWarnings.js ├── index.js ├── normalizeErrors.js ├── readAsset.js └── readAssets.js └── loader.test.js /.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 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /dist 3 | /node_modules 4 | /test/fixtures -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['@webpack-contrib/eslint-config-webpack', 'prettier'], 4 | }; 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | bin/* eol=lf 3 | yarn.lock -diff 4 | package-lock.json -diff -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug Report 3 | about: Something went awry and you'd like to tell us about it. 4 | --- 5 | 6 | 16 | 17 | - Operating System: 18 | - Node Version: 19 | - NPM Version: 20 | - webpack Version: 21 | - image-optimize-loader Version: 22 | 23 | ### Expected Behavior 24 | 25 | 26 | 27 | ### Actual Behavior 28 | 29 | 30 | 31 | ### Code 32 | 33 | ```js 34 | // webpack.config.js 35 | // If your code blocks are over 20 lines, please paste a link to a gist 36 | // (https://gist.github.com). 37 | ``` 38 | 39 | ```js 40 | // additional code, HEY YO remove this block if you don't need it 41 | ``` 42 | 43 | ### How Do We Reproduce? 44 | 45 | 51 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/DOCS.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 📚 Documentation 3 | about: Are the docs lacking or missing something? Do they need some new 🔥 hotness? Tell us here. 4 | --- 5 | 6 | 16 | 17 | Documentation Is: 18 | 19 | 20 | 21 | - [ ] Missing 22 | - [ ] Needed 23 | - [ ] Confusing 24 | - [ ] Not Sure? 25 | 26 | ### Please Explain in Detail... 27 | 28 | ### Your Proposal for Changes 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: ✨ Feature Request 3 | about: Suggest an idea for this project 4 | --- 5 | 6 | 16 | 17 | - Operating System: 18 | - Node Version: 19 | - NPM Version: 20 | - webpack Version: 21 | - image-optimize-loader Version: 22 | 23 | ### Feature Proposal 24 | 25 | ### Feature Use Case 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/MODIFICATION.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🔧 Modification Request 3 | about: Would you like something work differently? Have an alternative approach? This is the template for you. 4 | --- 5 | 6 | 16 | 17 | - Operating System: 18 | - Node Version: 19 | - NPM Version: 20 | - webpack Version: 21 | - image-optimize-loader Version: 22 | 23 | ### Expected Behavior / Situation 24 | 25 | ### Actual Behavior / Situation 26 | 27 | ### Modification Proposal 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/SUPPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🆘 Support, Help, and Advice 3 | about: 👉🏽 Need support, help, or advice? Don't open an issue! Head to StackOverflow or https://gitter.im/webpack/webpack. 4 | --- 5 | 6 | Hey there! If you need support, help, or advice then this is not the place to ask. 7 | Please visit [StackOverflow](https://stackoverflow.com/questions/tagged/webpack) 8 | or [the Webpack Gitter](https://gitter.im/webpack/webpack) instead. 9 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 10 | 11 | This PR contains a: 12 | 13 | - [ ] **bugfix** 14 | - [ ] new **feature** 15 | - [ ] **code refactor** 16 | - [ ] **test update** 17 | - [ ] **typo fix** 18 | - [ ] **metadata update** 19 | 20 | ### Motivation / Use-Case 21 | 22 | 27 | 28 | ### Breaking Changes 29 | 30 | 34 | 35 | ### Additional Info 36 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: test 5 | 6 | on: 7 | push: 8 | branches: [master] 9 | pull_request: 10 | branches: [master] 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [10.x, 12.x, 14.x] 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - run: npm ci 27 | - run: npm run lint 28 | 29 | test: 30 | runs-on: ubuntu-latest 31 | 32 | strategy: 33 | matrix: 34 | node-version: [10.x, 12.x, 14.x] 35 | 36 | steps: 37 | - uses: actions/checkout@v2 38 | - name: Use Node.js ${{ matrix.node-version }} 39 | uses: actions/setup-node@v1 40 | with: 41 | node-version: ${{ matrix.node-version }} 42 | - run: npm ci 43 | - run: npm i file-loader 44 | - run: npm test 45 | 46 | build: 47 | runs-on: ubuntu-latest 48 | 49 | strategy: 50 | matrix: 51 | node-version: [10.x, 12.x, 14.x] 52 | 53 | steps: 54 | - uses: actions/checkout@v2 55 | - name: Use Node.js ${{ matrix.node-version }} 56 | uses: actions/setup-node@v1 57 | with: 58 | node-version: ${{ matrix.node-version }} 59 | - run: npm ci 60 | - run: npm run build --if-present 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | *.log 3 | npm-debug.log* 4 | .eslintcache 5 | /coverage 6 | /dist 7 | /local 8 | /reports 9 | /node_modules 10 | .DS_Store 11 | Thumbs.db 12 | .idea 13 | *.iml 14 | .vscode 15 | *.sublime-project 16 | *.sublime-workspace -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /dist 3 | /node_modules 4 | /test/fixtures 5 | CHANGELOG.md -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | trailingComma: 'es5', 4 | arrowParens: 'always', 5 | }; 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 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. -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | 7 | # image-optimize-loader 8 | 9 | [![npm][npm]][npm-url] 10 | [![node][node]][node-url] 11 | [![deps][deps]][deps-url] 12 | ![test](https://github.com/GaoYYYang/image-optimize-loader/workflows/test/badge.svg?branch=master&event=push) 13 | 14 | This special **image optimize webpack loader** can: 15 | 16 | - Help you [**encode image** / **inline image**] and [**compress image** / **minify image**] when loaded with webpack. 17 | 18 | - Help you **tranform PNG/JPG images into WEBP** when needed. 19 | 20 | Its special encoding(inlining) ability is stronger than [url-loader](https://github.com/webpack-contrib/url-loader), especially useful for performance-optimization scenarios. And it will compress images automatically both for `emited image file` or `inlined images string`, without complicated configurations. 21 | 22 | ## Getting Started 23 | 24 | To begin, you'll need to install `img-optimize-loader`: 25 | 26 | ```console 27 | $ npm install img-optimize-loader --save-dev 28 | ``` 29 | 30 | Then, all you need to do is adding the `img-optimize-loader` to your `webpack` config. 31 | 32 | > You don't need to specify extra loaders like `file-loader` or `url-loader` for your images. `img-optimize-loader` will automaticlly handle everything. 33 | 34 | For example: 35 | 36 | **webpack.config.js** 37 | 38 | ```js 39 | module.exports = { 40 | module: { 41 | rules: [ 42 | { 43 | test: /\.(png|jpe?g|webp|git|svg|)$/i, 44 | use: [ 45 | { 46 | loader: 'img-optimize-loader', 47 | }, 48 | ], 49 | }, 50 | ], 51 | }, 52 | }; 53 | ``` 54 | 55 | **You can import your image. Compression and encoding will happen according to your configuration.** 56 | 57 | ```js 58 | import file from 'image.png'; 59 | ``` 60 | 61 | ## Features 62 | 63 | ### 1. Encode images and inline them into js/css files. 64 | 65 | > **Encode image with `base64`, `utf8`, `latin1`, `hex`, `ascii`, `binary`,`ucs2`** 66 | 67 | > **Inline image into JS / CSS files** 68 | 69 | When I use `url-loader` to encode images, I can only depend on [limit](https://github.com/webpack-contrib/url-loader#limit) configuration to decide whether to enable encodeing. **As we know, the image whose size was smaller than the limit, will always be encoded.** 70 | 71 | But I found problems when i want to inline a large size image into my entry jsbundle because this image is so important to my first screen rendering; Or when I don't want to inline trivial small images because there is no need to load them in time. I can't improve my page performance in this scene with `url-loader`. 72 | 73 | Now with `img-optimize-loader` we can take more flexible control on this. We can specify every image whether or not to be encoded easily(using file query)regardless of `limit` configuration. Still, if we don't specify it, `limit` configuration will take control. 74 | 75 | **index.js** 76 | 77 | ```js 78 | // Always let foo.png be encoded and inlined here regardless of 'limit configuration' 79 | import encodedImage from './encode.png?__inline'; 80 | 81 | // Always emit real image file regardless of 'limit configuration' 82 | import fileImage from './emit.png?__antiInline'; 83 | ``` 84 | 85 | The query symbol `__inline` and `__antiInline` can be customed by your self. 86 | 87 | ### 2. Compress your images 88 | 89 | > The compression algorithm is based on [imagemin](https://github.com/kevva/imagemin). It supports images in png, jpg, gif, webp, svg format. 90 | > 91 | > - **minify JPEG image** 92 | > - **minify PNG image** 93 | > - **minify GIF image** 94 | > - **minify WEBP image** 95 | > - **minify SVG image** 96 | 97 | We support 3 levels for you to compress images automaticlly. 98 | 99 | | level | description | 100 | | -------- | --------------------------------------------------------------------------------------- | 101 | | loseless | Only use lossless compress algorithm. Only support png/webp/svg images | 102 | | low | Cause a little distortion,and get small files. It will compress png/jpg/svg/gif images | 103 | | high | Cause more distortion,and get smaller files. It will compress png/jpg/svg/gif images | 104 | 105 | > To deal with webp images, please refer [webp](https://github.com/GaoYYYang/image-optimize-loader#3-transform-your-pngjpg-into-webp) 106 | 107 | **webpack.config.js** 108 | 109 | ```js 110 | module.exports = { 111 | module: { 112 | rules: [ 113 | { 114 | test: /\.(png|jpe?g|webp|git|svg|)$/i, 115 | use: [ 116 | { 117 | loader: `img-optimize-loader`, 118 | options: { 119 | compress: { 120 | // This will take more time and get smaller images. 121 | mode: 'high', // 'lossless', 'low' 122 | disableOnDevelopment: true, 123 | }, 124 | }, 125 | }, 126 | ], 127 | }, 128 | ], 129 | }, 130 | }; 131 | ``` 132 | 133 | And you can also adjust the compression manually using [more params](https://github.com/GaoYYYang/image-optimize-loader#compressmozjpeg). 134 | 135 | ```js 136 | module.exports = { 137 | module: { 138 | rules: [ 139 | { 140 | test: /\.(png|jpe?g|webp|git|svg|)$/i, 141 | use: [ 142 | { 143 | loader: 'img-optimize-loader', 144 | options: { 145 | compress: { 146 | // loseless compression for png 147 | optipng: { 148 | optimizationLevel: 4, 149 | }, 150 | // lossy compression for png. This will generate smaller file than optipng. 151 | pngquant: { 152 | quality: [0.2, 0.8], 153 | }, 154 | // Compression for webp. 155 | // You can also tranform jpg/png into webp. 156 | webp: { 157 | quality: 100, 158 | }, 159 | // Compression for svg. 160 | svgo: true, 161 | // Compression for gif. 162 | gifsicle: { 163 | optimizationLevel: 3, 164 | }, 165 | // Compression for jpg. 166 | mozjpeg: { 167 | progressive: true, 168 | quality: 60, 169 | }, 170 | }, 171 | }, 172 | }, 173 | ], 174 | }, 175 | ], 176 | }, 177 | }; 178 | ``` 179 | 180 | ### 3. Transform your png/jpg into webp 181 | 182 | When you enable `compress.webp`, it will transform your png/jpg into webp files, and there will be no png/jpg files generated. Your source code will directly use webp file instead of png/jpg. 183 | 184 | Generally, when you can use webp without incompatibility problem 185 | , there will be no need to use png or jpg any more, because webp files are always smaller than their png/jpg origin. 186 | 187 | **webpack.config.js** 188 | 189 | ```js 190 | module.exports = { 191 | module: { 192 | rules: [ 193 | { 194 | test: /\.(png|jpe?g|webp|git|svg|)$/i, 195 | use: [ 196 | { 197 | loader: `img-optimize-loader`, 198 | options: { 199 | compress: { 200 | // This will transform your png/jpg into webp. 201 | webp: true, 202 | disableOnDevelopment: true, 203 | }, 204 | }, 205 | }, 206 | ], 207 | }, 208 | ], 209 | }, 210 | }; 211 | ``` 212 | 213 | Referer to [webp configuration](https://github.com/GaoYYYang/image-optimize-loader#compresswebp) for details. 214 | 215 | **index.js** 216 | 217 | ```js 218 | // This two images will be transformed into webp and your source code will use the webp format. 219 | import encodedImage from './encode.png'; 220 | 221 | import fileImage from './test.jpg'; 222 | ``` 223 | 224 | ## Options 225 | 226 | ### `name` 227 | 228 | Type: `[string]` 229 | Default: `'imgs/[contenthash].[ext]'` 230 | 231 | Specifies a custom filename template for the target images(s) using the query parameter name. For example, to emit a image from your context directory into the output directory retaining the full directory structure, you might use: 232 | 233 | **webpack.config.js** 234 | 235 | ```js 236 | module.exports = { 237 | module: { 238 | rules: [ 239 | { 240 | loader: `img-optimize-loader`, 241 | options: { 242 | name: '[path][name].[ext]', 243 | }, 244 | }, 245 | ], 246 | }, 247 | }; 248 | ``` 249 | 250 | ### `esModule` 251 | 252 | Type: `[Boolean]` 253 | Default: `false` 254 | 255 | Decides js modules generated from image. Whether to use the ES modules or commonjs. 256 | 257 | **webpack.config.js** 258 | 259 | ```js 260 | module.exports = { 261 | module: { 262 | rules: [ 263 | { 264 | loader: `img-optimize-loader`, 265 | options: { 266 | esModule: false, 267 | }, 268 | }, 269 | ], 270 | }, 271 | }; 272 | ``` 273 | ### `outputPath` 274 | 275 | Type: `String|Function` 276 | Default: `undefined` 277 | 278 | Specify a filesystem path where the target file(s) will be placed. 279 | 280 | #### `String` 281 | 282 | **webpack.config.js** 283 | 284 | ```js 285 | module.exports = { 286 | module: { 287 | rules: [ 288 | { 289 | test: /\.(png|jpe?g|gif)$/i, 290 | loader: 'img-optimize-loader', 291 | options: { 292 | outputPath: 'images', 293 | }, 294 | }, 295 | ], 296 | }, 297 | }; 298 | ``` 299 | 300 | #### `Function` 301 | 302 | **webpack.config.js** 303 | 304 | ```js 305 | module.exports = { 306 | module: { 307 | rules: [ 308 | { 309 | test: /\.(png|jpe?g|gif)$/i, 310 | loader: 'img-optimize-loader', 311 | options: { 312 | outputPath: (url, resourcePath, context) => { 313 | return `output_path/${url}`; 314 | }, 315 | }, 316 | }, 317 | ], 318 | }, 319 | }; 320 | ``` 321 | 322 | ### `publicPath` 323 | 324 | Type: `String|Function` 325 | Default: [`__webpack_public_path__`](https://webpack.js.org/api/module-variables/#__webpack_public_path__-webpack-specific-)+outputPath 326 | 327 | Specifies a custom public path for the target file(s). 328 | 329 | #### `String` 330 | 331 | **webpack.config.js** 332 | 333 | ```js 334 | module.exports = { 335 | module: { 336 | rules: [ 337 | { 338 | test: /\.(png|jpe?g|gif)$/i, 339 | loader: 'img-optimize-loader', 340 | options: { 341 | publicPath: 'assets', 342 | }, 343 | }, 344 | ], 345 | }, 346 | }; 347 | ``` 348 | 349 | #### `Function` 350 | 351 | **webpack.config.js** 352 | 353 | ```js 354 | module.exports = { 355 | module: { 356 | rules: [ 357 | { 358 | test: /\.(png|jpe?g|gif)$/i, 359 | loader: 'img-optimize-loader', 360 | options: { 361 | publicPath: (url, resourcePath, context) => { 362 | return `public_path/${url}`; 363 | }, 364 | }, 365 | }, 366 | ], 367 | }, 368 | }; 369 | ``` 370 | 371 | ### `context` 372 | 373 | Type: `String` 374 | Default: [`context`](https://webpack.js.org/configuration/entry-context/#context) 375 | 376 | Specifies a custom file context. 377 | 378 | ### `emitFile` 379 | 380 | Type: `Boolean` 381 | Default: `true` 382 | 383 | If true, emits a file (writes a file to the filesystem). If false, the loader 384 | will return a public URI but **will not** emit the file. It is often useful to 385 | disable this option for server-side packages. 386 | 387 | 388 | ### `inline.symbol` 389 | 390 | Type: `[String]` 391 | Default: `__inline` 392 | 393 | Query symbol used to specify the image that should be encoded and inlined. 394 | 395 | **webpack.config.js** 396 | 397 | ```js 398 | module.exports = { 399 | module: { 400 | rules: [ 401 | { 402 | loader: `img-optimize-loader`, 403 | options: { 404 | inline: { 405 | symbol: '__inline', 406 | }, 407 | }, 408 | }, 409 | ], 410 | }, 411 | }; 412 | ``` 413 | 414 | ### `inline.antiSymbol` 415 | 416 | Type: `[String]` 417 | Default: `__antiInline` 418 | 419 | Query symbol used to specify the image that should not be encoded and inlined. 420 | 421 | **webpack.config.js** 422 | 423 | ```js 424 | module.exports = { 425 | module: { 426 | rules: [ 427 | { 428 | loader: `img-optimize-loader`, 429 | options: { 430 | inline: { 431 | antiSymbol: '__antiInline', 432 | }, 433 | }, 434 | }, 435 | ], 436 | }, 437 | }; 438 | ``` 439 | 440 | ### `inline.limit` 441 | 442 | Type: `[Boolean|Number|String]` 443 | Default: `5000` 444 | 445 | A Number or String specifying the maximum size of a encoded image in bytes. If the image size is equal or greater than the limit `file-loader` will be used (by default) and all query parameters are passed to it. 446 | 447 | **webpack.config.js** 448 | 449 | ```js 450 | module.exports = { 451 | module: { 452 | rules: [ 453 | { 454 | loader: `img-optimize-loader`, 455 | options: { 456 | inline: { 457 | antiSymbol: '__antiInline', 458 | }, 459 | }, 460 | }, 461 | ], 462 | }, 463 | }; 464 | ``` 465 | 466 | ### `inline.mimetype` 467 | 468 | Type: `Boolean|String` 469 | Default: based from [mime-types](https://github.com/jshttp/mime-types) 470 | 471 | Specify the `mimetype` which the file will be inlined with. 472 | If unspecified the `mimetype` value will be used from [mime-types](https://github.com/jshttp/mime-types). 473 | 474 | #### `Boolean` 475 | 476 | The `true` value allows to generation the `mimetype` part from [mime-types](https://github.com/jshttp/mime-types). 477 | The `false` value removes the `mediatype` part from a Data URL (if omitted, defaults to `text/plain;charset=US-ASCII`). 478 | 479 | **webpack.config.js** 480 | 481 | ```js 482 | module.exports = { 483 | module: { 484 | rules: [ 485 | { 486 | loader: `img-optimize-loader`, 487 | options: { 488 | inline: { 489 | mimetype: false, 490 | }, 491 | }, 492 | }, 493 | ], 494 | }, 495 | }; 496 | ``` 497 | 498 | #### `String` 499 | 500 | Sets the MIME type for the file to be transformed. 501 | 502 | **webpack.config.js** 503 | 504 | ```js 505 | module.exports = { 506 | module: { 507 | rules: [ 508 | { 509 | loader: `img-optimize-loader`, 510 | options: { 511 | inline: { 512 | mimetype: 'image/png', 513 | }, 514 | }, 515 | }, 516 | ], 517 | }, 518 | }; 519 | ``` 520 | 521 | ### `inline.encoding` 522 | 523 | Type: `Boolean|String` 524 | Default: `base64` 525 | 526 | Specify the `encoding` which the file will be inlined with. 527 | If unspecified the `encoding` will be `base64`. 528 | 529 | #### `Boolean` 530 | 531 | If you don't want to use any encoding you can set `encoding` to `false` however if you set it to `true` it will use the default encoding `base64`. 532 | 533 | **webpack.config.js** 534 | 535 | ```js 536 | module.exports = { 537 | module: { 538 | rules: [ 539 | { 540 | test: /\.svg$/i, 541 | use: [ 542 | { 543 | loader: `img-optimize-loader`, 544 | options: { 545 | inline: { 546 | encoding: false, 547 | }, 548 | }, 549 | }, 550 | ], 551 | }, 552 | ], 553 | }, 554 | }; 555 | ``` 556 | 557 | #### `String` 558 | 559 | It supports [Node.js Buffers and Character Encodings](https://nodejs.org/api/buffer.html#buffer_buffers_and_character_encodings) which are `["utf8","utf16le","latin1","base64","hex","ascii","binary","ucs2"]`. 560 | 561 | **webpack.config.js** 562 | 563 | ```js 564 | module.exports = { 565 | module: { 566 | rules: [ 567 | { 568 | loader: `img-optimize-loader`, 569 | options: { 570 | inline: { 571 | encoding: 'utf8', 572 | }, 573 | }, 574 | }, 575 | ], 576 | }, 577 | }; 578 | ``` 579 | 580 | ### `inline.generator` 581 | 582 | Type: `Function` 583 | Default: `(mimetype, encoding, content, resourcePath) => mimetype;encoding,base64_content` 584 | 585 | You can create you own custom implementation for encoding data. 586 | 587 | **webpack.config.js** 588 | 589 | ```js 590 | module.exports = { 591 | module: { 592 | rules: [ 593 | { 594 | loader: `img-optimize-loader`, 595 | options: { 596 | inline: { 597 | // The `mimetype` and `encoding` arguments will be obtained from your options 598 | // The `resourcePath` argument is path to file. 599 | generator: (content, mimetype, encoding, resourcePath) => { 600 | if (/\.html$/i.test(resourcePath)) { 601 | return `data:${mimetype},${content.toString()}`; 602 | } 603 | 604 | return `data:${mimetype}${ 605 | encoding ? `;${encoding}` : '' 606 | },${content.toString(encoding)}`; 607 | }, 608 | }, 609 | }, 610 | }, 611 | ], 612 | }, 613 | }; 614 | ``` 615 | 616 | ### `compress.mode` 617 | 618 | Type: `string` 619 | Default: `low` 620 | 621 | Specify the compress level. 622 | | level | description| 623 | |-|-| 624 | | loseless | Only use lossless compress algorithm. Only support png/webp/svg images| 625 | | low | Cause a little distortion,and get small files. It will compress png/jpg/svg/webp/gif images| 626 | | high | Cause more distortion,and get smaller files. It will compress png/jpg/svg/webp/gif images| 627 | 628 | **webpack.config.js** 629 | 630 | ```js 631 | module.exports = { 632 | module: { 633 | rules: [ 634 | { 635 | loader: `img-optimize-loader`, 636 | options: { 637 | compress: { 638 | mode: 'high', 639 | }, 640 | }, 641 | }, 642 | ], 643 | }, 644 | }; 645 | ``` 646 | 647 | ### `compress.mozjpeg` 648 | 649 | Type: `[Object|Boolean]` 650 | 651 | Compress jpg images. 652 | 653 | #### `Boolean` 654 | 655 | If you don't want to compress jpg files, you can set `mozjpeg` to `false` however if you set it to `true` it will generator the settings according to `compress.mode` configuration. 656 | 657 | **webpack.config.js** 658 | 659 | ```js 660 | module.exports = { 661 | module: { 662 | rules: [ 663 | { 664 | loader: `img-optimize-loader`, 665 | options: { 666 | compress: { 667 | mozjpeg: false, 668 | }, 669 | }, 670 | }, 671 | ], 672 | }, 673 | }; 674 | ``` 675 | 676 | #### `Object` 677 | 678 | **webpack.config.js** 679 | 680 | ```js 681 | module.exports = { 682 | module: { 683 | rules: [ 684 | { 685 | loader: `img-optimize-loader`, 686 | options: { 687 | compress: { 688 | mozjpeg: {}, 689 | }, 690 | }, 691 | }, 692 | ], 693 | }, 694 | }; 695 | ``` 696 | 697 | Link to [mozjpeg configuration](https://github.com/imagemin/imagemin-mozjpeg) 698 | 699 | ### `compress.optipng` 700 | 701 | Type: `[Object|Boolean]` 702 | 703 | Compress png images. 704 | 705 | #### `Boolean` 706 | 707 | If you don't want to use optipng to compress png files, you can set `optipng` to `false` however if you set it to `true` it will generator the settings according to `compress.mode` configuration. 708 | 709 | **webpack.config.js** 710 | 711 | ```js 712 | module.exports = { 713 | module: { 714 | rules: [ 715 | { 716 | loader: `img-optimize-loader`, 717 | options: { 718 | compress: { 719 | optipng: false, 720 | }, 721 | }, 722 | }, 723 | ], 724 | }, 725 | }; 726 | ``` 727 | 728 | #### `Object` 729 | 730 | **webpack.config.js** 731 | 732 | ```js 733 | module.exports = { 734 | module: { 735 | rules: [ 736 | { 737 | loader: `img-optimize-loader`, 738 | options: { 739 | compress: { 740 | optipng: {}, 741 | }, 742 | }, 743 | }, 744 | ], 745 | }, 746 | }; 747 | ``` 748 | 749 | Link to [optipng configuration](https://github.com/kevva/imagemin-optipng) 750 | 751 | ### `compress.pngquant` 752 | 753 | Type: `[Object|Boolean]` 754 | 755 | Compress png images. 756 | 757 | #### `Boolean` 758 | 759 | If you don't want to use pngquant to compress png files, you can set `pngquant` to `false` however if you set it to `true` it will generator the settings according to `compress.mode` configuration. 760 | 761 | **webpack.config.js** 762 | 763 | ```js 764 | module.exports = { 765 | module: { 766 | rules: [ 767 | { 768 | loader: `img-optimize-loader`, 769 | options: { 770 | compress: { 771 | pngquant: false, 772 | }, 773 | }, 774 | }, 775 | ], 776 | }, 777 | }; 778 | ``` 779 | 780 | #### `Object` 781 | 782 | **webpack.config.js** 783 | 784 | ```js 785 | module.exports = { 786 | module: { 787 | rules: [ 788 | { 789 | loader: `img-optimize-loader`, 790 | options: { 791 | compress: { 792 | pngquant: {}, 793 | }, 794 | }, 795 | }, 796 | ], 797 | }, 798 | }; 799 | ``` 800 | 801 | Link to [pngquant configuration](https://github.com/imagemin/imagemin-pngquant) 802 | 803 | ### `compress.svgo` 804 | 805 | Type: `[Object|Boolean]` 806 | 807 | Compress svg images. 808 | 809 | #### `Boolean` 810 | 811 | If you don't want to compress svg files, you can set `svgo` to `false` however if you set it to `true` it will generator the settings according to `compress.mode` configuration. 812 | 813 | **webpack.config.js** 814 | 815 | ```js 816 | module.exports = { 817 | module: { 818 | rules: [ 819 | { 820 | loader: `img-optimize-loader`, 821 | options: { 822 | compress: { 823 | svgo: false, 824 | }, 825 | }, 826 | }, 827 | ], 828 | }, 829 | }; 830 | ``` 831 | 832 | #### `Object` 833 | 834 | **webpack.config.js** 835 | 836 | ```js 837 | module.exports = { 838 | module: { 839 | rules: [ 840 | { 841 | loader: `img-optimize-loader`, 842 | options: { 843 | compress: { 844 | svgo: {}, 845 | }, 846 | }, 847 | }, 848 | ], 849 | }, 850 | }; 851 | ``` 852 | 853 | Link to [svgo configuration](https://github.com/kevva/imagemin-svgo) 854 | 855 | ### `compress.gifsicle` 856 | 857 | Type: `[Object|Boolean]` 858 | 859 | Compress gif images. 860 | 861 | #### `Boolean` 862 | 863 | If you don't want to compress gif files, you can set `gifsicle` to `false` however if you set it to `true` it will generator the settings according to `compress.mode` configuration. 864 | 865 | **webpack.config.js** 866 | 867 | ```js 868 | module.exports = { 869 | module: { 870 | rules: [ 871 | { 872 | loader: `img-optimize-loader`, 873 | options: { 874 | compress: { 875 | gifsicle: false, 876 | }, 877 | }, 878 | }, 879 | ], 880 | }, 881 | }; 882 | ``` 883 | 884 | #### `Object` 885 | 886 | **webpack.config.js** 887 | 888 | ```js 889 | module.exports = { 890 | module: { 891 | rules: [ 892 | { 893 | loader: `img-optimize-loader`, 894 | options: { 895 | compress: { 896 | gifsicle: {}, 897 | }, 898 | }, 899 | }, 900 | ], 901 | }, 902 | }; 903 | ``` 904 | 905 | ### `compress.webp` 906 | 907 | Type: `[Object|Boolean]` 908 | Default: `false` 909 | 910 | Transform png/jpg into webp. Compress webp files. 911 | 912 | #### `Boolean` 913 | 914 | If you want to transform png/jpg into webp, you can set `webp` to `true`. 915 | 916 | **webpack.config.js** 917 | 918 | ```js 919 | module.exports = { 920 | module: { 921 | rules: [ 922 | { 923 | loader: `img-optimize-loader`, 924 | options: { 925 | compress: { 926 | webp: true, 927 | }, 928 | }, 929 | }, 930 | ], 931 | }, 932 | }; 933 | ``` 934 | 935 | #### `Object` 936 | 937 | **webpack.config.js** 938 | 939 | ```js 940 | module.exports = { 941 | module: { 942 | rules: [ 943 | { 944 | loader: `img-optimize-loader`, 945 | options: { 946 | compress: { 947 | webp: {}, 948 | }, 949 | }, 950 | }, 951 | ], 952 | }, 953 | }; 954 | ``` 955 | 956 | Link to [webp configuration](https://github.com/imagemin/imagemin-webp#options) 957 | 958 | ## Inspiration 959 | 960 | - [image-webpack-loader](https://github.com/tcoopman/image-webpack-loader) 961 | - [url-loader](https://github.com/webpack/url-loader) 962 | - [imagemin](https://github.com/imagemin/imagemin) 963 | 964 | ## License 965 | 966 | [MIT](./LICENSE) 967 | 968 | [npm]: https://img.shields.io/npm/v/img-optimize-loader.svg 969 | [npm-url]: https://npmjs.com/package/img-optimize-loader 970 | [node]: https://img.shields.io/node/v/img-optimize-loader.svg 971 | [node-url]: https://nodejs.org 972 | [deps]: https://david-dm.org/GaoYYYang/image-optimize-loader.svg 973 | [deps-url]: https://david-dm.org/GaoYYYang/image-optimize-loader 974 | [tests]: https://github.com/GaoYYYang/image-optimize-loader/workflows/test/badge.svg 975 | [tests-url]: https://github.com/GaoYYYang/image-optimize-loader/actions 976 | [size]: https://packagephobia.now.sh/badge?p=img-optimize-loader 977 | [size-url]: https://packagephobia.now.sh/result?p=img-optimize-loader -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - master 3 | - next 4 | 5 | variables: 6 | npm_config_cache: $(Pipeline.Workspace)/.npm 7 | 8 | jobs: 9 | - job: Lint 10 | pool: 11 | vmImage: ubuntu-latest 12 | steps: 13 | - task: NodeTool@0 14 | inputs: 15 | versionSpec: ^10.13.0 16 | displayName: 'Install Node.js' 17 | - task: Npm@1 18 | inputs: 19 | command: custom 20 | customCommand: i -g npm@latest 21 | displayName: 'Install latest NPM' 22 | - script: | 23 | node -v 24 | npm -v 25 | displayName: 'Print versions' 26 | - task: CacheBeta@1 27 | inputs: 28 | key: npm | $(Agent.OS) | package-lock.json 29 | path: $(npm_config_cache) 30 | displayName: 'Cache npm' 31 | - script: npm ci 32 | displayName: 'Install dependencies' 33 | - script: npm run lint 34 | displayName: 'Run lint' 35 | - script: npm run security 36 | displayName: 'Run NPM audit' 37 | - script: ./node_modules/.bin/commitlint-azure-pipelines 38 | displayName: 'Run lint commit message' 39 | 40 | - job: Linux 41 | pool: 42 | vmImage: ubuntu-latest 43 | strategy: 44 | maxParallel: 4 45 | matrix: 46 | node-13: 47 | node_version: ^13.0.0 48 | webpack_version: latest 49 | node-12: 50 | node_version: ^12.0.0 51 | webpack_version: latest 52 | node-10: 53 | node_version: ^10.13.0 54 | webpack_version: latest 55 | node-10-canary: 56 | node_version: ^10.13.0 57 | webpack_version: next 58 | steps: 59 | - task: NodeTool@0 60 | inputs: 61 | versionSpec: $(node_version) 62 | displayName: 'Install Node.js $(node_version)' 63 | - task: Npm@1 64 | inputs: 65 | command: custom 66 | customCommand: i -g npm@latest 67 | displayName: 'Install latest NPM' 68 | - script: | 69 | node -v 70 | npm -v 71 | displayName: 'Print versions' 72 | - task: CacheBeta@1 73 | inputs: 74 | key: npm | $(Agent.OS) | package-lock.json 75 | path: $(npm_config_cache) 76 | displayName: 'Cache npm' 77 | - script: npm ci 78 | displayName: 'Install dependencies' 79 | - script: npm i webpack@$(webpack_version) 80 | displayName: 'Install "webpack@$(webpack_version)"' 81 | - script: npm run test:coverage -- --ci --reporters="default" --reporters="jest-junit" || $(continue_on_error) 82 | displayName: 'Run tests with coverage' 83 | - task: PublishTestResults@2 84 | inputs: 85 | testRunTitle: 'Linux with Node.js $(node_version)' 86 | testResultsFiles: '**/junit.xml' 87 | condition: succeededOrFailed() 88 | displayName: 'Publish test results' 89 | - script: curl -s https://codecov.io/bash | bash -s -- -t $(CODECOV_TOKEN) 90 | condition: succeededOrFailed() 91 | displayName: 'Submit coverage data to codecov' 92 | 93 | - job: macOS 94 | pool: 95 | vmImage: macOS-latest 96 | strategy: 97 | maxParallel: 4 98 | matrix: 99 | node-13: 100 | node_version: ^13.0.0 101 | webpack_version: latest 102 | node-12: 103 | node_version: ^12.0.0 104 | webpack_version: latest 105 | node-10: 106 | node_version: ^10.13.0 107 | webpack_version: latest 108 | node-10-canary: 109 | node_version: ^10.13.0 110 | webpack_version: next 111 | steps: 112 | - task: NodeTool@0 113 | inputs: 114 | versionSpec: $(node_version) 115 | displayName: 'Install Node.js $(node_version)' 116 | - task: Npm@1 117 | inputs: 118 | command: custom 119 | customCommand: i -g npm@latest 120 | displayName: 'Install latest NPM' 121 | - script: | 122 | node -v 123 | npm -v 124 | displayName: 'Print versions' 125 | - task: CacheBeta@1 126 | inputs: 127 | key: npm | $(Agent.OS) | package-lock.json 128 | path: $(npm_config_cache) 129 | displayName: 'Cache npm' 130 | - script: npm ci 131 | displayName: 'Install dependencies' 132 | - script: npm i webpack@$(webpack_version) 133 | displayName: 'Install "webpack@$(webpack_version)"' 134 | - script: npm run test:coverage -- --ci --reporters="default" --reporters="jest-junit" || $(continue_on_error) 135 | displayName: 'Run tests with coverage' 136 | - task: PublishTestResults@2 137 | inputs: 138 | testRunTitle: 'Linux with Node.js $(node_version)' 139 | testResultsFiles: '**/junit.xml' 140 | condition: succeededOrFailed() 141 | displayName: 'Publish test results' 142 | - script: curl -s https://codecov.io/bash | bash -s -- -t $(CODECOV_TOKEN) 143 | condition: succeededOrFailed() 144 | displayName: 'Submit coverage data to codecov' 145 | 146 | - job: Windows 147 | pool: 148 | vmImage: windows-latest 149 | strategy: 150 | maxParallel: 4 151 | matrix: 152 | node-13: 153 | node_version: ^13.0.0 154 | webpack_version: latest 155 | node-12: 156 | node_version: ^12.0.0 157 | webpack_version: latest 158 | node-10: 159 | node_version: ^10.13.0 160 | webpack_version: latest 161 | node-10-canary: 162 | node_version: ^10.13.0 163 | webpack_version: next 164 | steps: 165 | - script: 'git config --global core.autocrlf input' 166 | displayName: 'Config git core.autocrlf' 167 | - checkout: self 168 | - task: NodeTool@0 169 | inputs: 170 | versionSpec: $(node_version) 171 | displayName: 'Install Node.js $(node_version)' 172 | - task: Npm@1 173 | inputs: 174 | command: custom 175 | customCommand: i -g npm@latest 176 | displayName: 'Install latest NPM' 177 | - script: | 178 | node -v 179 | npm -v 180 | displayName: 'Print versions' 181 | - task: CacheBeta@1 182 | inputs: 183 | key: npm | $(Agent.OS) | package-lock.json 184 | path: $(npm_config_cache) 185 | displayName: 'Cache npm' 186 | - script: npm ci 187 | displayName: 'Install dependencies' 188 | - script: npm i webpack@$(webpack_version) 189 | displayName: 'Install "webpack@$(webpack_version)"' 190 | - script: npm run test:coverage -- --ci --reporters="default" --reporters="jest-junit" || $(continue_on_error) 191 | displayName: 'Run tests with coverage' 192 | - task: PublishTestResults@2 193 | inputs: 194 | testRunTitle: 'Linux with Node.js $(node_version)' 195 | testResultsFiles: '**/junit.xml' 196 | condition: succeededOrFailed() 197 | displayName: 'Publish test results' 198 | - script: curl -s https://codecov.io/bash | bash -s -- -t $(CODECOV_TOKEN) 199 | condition: succeededOrFailed() 200 | displayName: 'Submit coverage data to codecov' 201 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const MIN_BABEL_VERSION = 7; 2 | 3 | module.exports = (api) => { 4 | api.assertVersion(MIN_BABEL_VERSION); 5 | api.cache(true); 6 | 7 | return { 8 | presets: [ 9 | [ 10 | '@babel/preset-env', 11 | { 12 | targets: { 13 | node: '10.13.0', 14 | }, 15 | }, 16 | ], 17 | ], 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | }; 4 | -------------------------------------------------------------------------------- /husky.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | hooks: { 3 | 'pre-commit': 'lint-staged', 4 | 'commit-msg': 'commitlint -E HUSKY_GIT_PARAMS', 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '*.js': ['prettier --write', 'eslint --fix', 'git add'], 3 | '*.{json,md,yml,css,ts}': ['prettier --write', 'git add'], 4 | }; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "img-optimize-loader", 3 | "version": "1.0.7", 4 | "description": "Image webpack loader. Minify image, compress image, encode image(eg: base64) and inline image automaticlly. Support PNG, JPG, JPEG, GIF, WEBP, SVG.", 5 | "license": "MIT", 6 | "repository": "GaoYYYang/image-optimize-loader", 7 | "author": "GaoYYYang", 8 | "homepage": "https://github.com/GaoYYYang/image-optimize-loader", 9 | "bugs": "https://github.com/GaoYYYang/image-optimize-loader/issues", 10 | "main": "dist/index.js", 11 | "engines": { 12 | "node": ">= 10.13.0" 13 | }, 14 | "scripts": { 15 | "start": "npm run build -- -w", 16 | "clean": "del-cli dist", 17 | "prebuild": "npm run clean", 18 | "build": "cross-env NODE_ENV=production babel src -d dist --copy-files", 19 | "commitlint": "commitlint --from=master", 20 | "security": "npm audit", 21 | "lint:prettier": "prettier \"{**/*,*}.{js,json,md,yml,css}\" --write --list-different", 22 | "lint:js": "eslint --cache .", 23 | "lint": "npm-run-all -l -p \"lint:**\"", 24 | "test:only": "cross-env NODE_ENV=test jest", 25 | "test:watch": "npm run test:only -- --watch", 26 | "test:coverage": "npm run test:only -- --collectCoverageFrom=\"src/**/*.js\" --coverage", 27 | "test": "npm run test:coverage", 28 | "prepare": "npm run build", 29 | "release": "standard-version", 30 | "defaults": "webpack-defaults" 31 | }, 32 | "files": ["dist"], 33 | "peerDependencies": { 34 | "webpack": "^4.0.0 || ^5.0.0", 35 | "file-loader": "*" 36 | }, 37 | "dependencies": { 38 | "imagemin": "^7.0.1", 39 | "loader-utils": "^2.0.0", 40 | "mime-types": "^2.1.27", 41 | "schema-utils": "^2.7.0" 42 | }, 43 | "optionalDependencies": { 44 | "imagemin-gifsicle": "^6.0.1", 45 | "imagemin-mozjpeg": "^8.0.0", 46 | "imagemin-optipng": "^7.1.0", 47 | "imagemin-pngquant": "^8.0.0", 48 | "imagemin-svgo": "^7.0.0", 49 | "imagemin-webp": "^5.1.0" 50 | }, 51 | "devDependencies": { 52 | "@babel/cli": "^7.10.3", 53 | "@babel/core": "^7.10.3", 54 | "@babel/preset-env": "^7.10.3", 55 | "@commitlint/cli": "^9.0.1", 56 | "@commitlint/config-conventional": "^9.0.1", 57 | "@webpack-contrib/defaults": "^6.3.0", 58 | "@webpack-contrib/eslint-config-webpack": "^3.0.0", 59 | "babel-jest": "^26.1.0", 60 | "commitlint-azure-pipelines-cli": "^1.0.3", 61 | "cross-env": "^7.0.2", 62 | "del": "^5.1.0", 63 | "del-cli": "^3.0.1", 64 | "eslint": "^7.3.1", 65 | "eslint-config-prettier": "^6.11.0", 66 | "eslint-plugin-import": "^2.22.0", 67 | "husky": "^4.2.5", 68 | "jest": "^26.1.0", 69 | "jest-junit": "^11.0.1", 70 | "lint-staged": "^10.2.11", 71 | "memfs": "^3.2.0", 72 | "npm-run-all": "^4.1.5", 73 | "prettier": "^2.0.5", 74 | "standard-version": "^8.0.0", 75 | "webpack": "^4.43.0" 76 | }, 77 | "keywords": ["webpack image loader", "minify image", "compress image", "inline image", "encode image"] 78 | } 79 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .DS_Store 3 | *.log 4 | .idea 5 | .vscode 6 | .eslintcache 7 | /compress 8 | /test -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @file: constants 3 | * @author: GaoYYYang 4 | * @date: 06 30 2020 5:38:34 5 | */ 6 | 7 | const DEFAULT_INLINE_SYMBOL = '__inline'; 8 | const DEFAULT_ANTI_INLINE_SYMBOL = '__antiInline'; 9 | const DEFAULT_INLINE_LIMIT = 5000; 10 | const DEFAULT_INLINE_ENCODING = 'base64'; 11 | 12 | const DEFAULT_ES_MODULE = false; 13 | const DEFAULT_NAME = 'imgs/[name].[contenthash:8].[ext]'; 14 | 15 | const DEFAULT_INLINE_OPTION = { 16 | disable: false, 17 | symbol: DEFAULT_INLINE_SYMBOL, 18 | antiSymbol: DEFAULT_ANTI_INLINE_SYMBOL, 19 | limit: DEFAULT_INLINE_LIMIT, 20 | encoding: DEFAULT_INLINE_ENCODING, 21 | }; 22 | 23 | const LOSSY_LOW_COMPRESS_OPTION = { 24 | disable: false, 25 | disableOnDevelopment: true, 26 | // default optimizers 27 | optipng: { 28 | optimizationLevel: 2, 29 | }, 30 | 31 | svgo: true, 32 | 33 | pngquant: { 34 | quality: [0.5, 0.8], 35 | }, 36 | gifsicle: { 37 | optimizationLevel: 2, 38 | }, 39 | mozjpeg: { 40 | progressive: true, 41 | quality: 80, 42 | }, 43 | }; 44 | const LOSSY_HIGH_COMPRESS_OPTION = { 45 | disable: false, 46 | disableOnDevelopment: true, 47 | // default optimizers 48 | optipng: { 49 | optimizationLevel: 4, 50 | }, 51 | 52 | svgo: true, 53 | 54 | pngquant: { 55 | quality: [0.2, 0.8], 56 | }, 57 | gifsicle: { 58 | optimizationLevel: 3, 59 | }, 60 | mozjpeg: { 61 | progressive: true, 62 | quality: 60, 63 | }, 64 | }; 65 | 66 | const LOSELESS_COMPRESS_OPTION = { 67 | disable: false, 68 | disableOnDevelopment: true, 69 | // default optimizers 70 | optipng: { 71 | optimizationLevel: 2, 72 | }, 73 | 74 | svgo: true, 75 | }; 76 | 77 | export { 78 | DEFAULT_INLINE_OPTION, 79 | LOSSY_HIGH_COMPRESS_OPTION, 80 | LOSSY_LOW_COMPRESS_OPTION, 81 | LOSELESS_COMPRESS_OPTION, 82 | DEFAULT_ES_MODULE, 83 | DEFAULT_NAME, 84 | }; 85 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @file: main entry 3 | * @author: GaoYYYang 4 | * @date: 06 30 2020 4:51:22 5 | */ 6 | 7 | import path from 'path'; 8 | 9 | import validateOptions from 'schema-utils'; 10 | import { getOptions, parseQuery } from 'loader-utils'; 11 | 12 | import compress from './lib/compress'; 13 | import inline from './lib/inline'; 14 | import emit from './lib/emit'; 15 | 16 | import { 17 | DEFAULT_INLINE_OPTION, 18 | LOSSY_HIGH_COMPRESS_OPTION, 19 | LOSSY_LOW_COMPRESS_OPTION, 20 | LOSELESS_COMPRESS_OPTION, 21 | DEFAULT_ES_MODULE, 22 | DEFAULT_NAME 23 | } from './constants'; 24 | import schema from './options.json'; 25 | 26 | function checkNeedInline(option, size) { 27 | if (option.disable) { 28 | return false; 29 | } 30 | 31 | const { limit, symbol, antiSymbol } = option; 32 | const { resourceQuery } = this; 33 | const query = (resourceQuery && parseQuery(resourceQuery)) || {}; 34 | 35 | if (query[symbol]) { 36 | return true; 37 | } 38 | 39 | if (query[antiSymbol]) { 40 | return false; 41 | } 42 | 43 | if (typeof limit === 'boolean') { 44 | return limit; 45 | } 46 | 47 | if (typeof limit === 'string') { 48 | return size <= parseInt(limit, 10); 49 | } 50 | 51 | if (typeof limit === 'number') { 52 | return size <= limit; 53 | } 54 | 55 | return true; 56 | } 57 | 58 | function inlineOrEmit(options, data, callback) { 59 | if (checkNeedInline.call(this, options, data.length)) { 60 | inline.call(this, data, options, callback); 61 | } else { 62 | emit.call(this, data, options, callback); 63 | } 64 | } 65 | 66 | export default function loader(source) { 67 | const options = getOptions(this) || {}; 68 | const { mode, resourcePath } = this; 69 | const ext = path.extname(resourcePath); 70 | const { outputPath, publicPath, postTransformPublicPath, context, emitFile } = options; 71 | const esModule = options.esModule || DEFAULT_ES_MODULE; 72 | let name = options.name || DEFAULT_NAME; 73 | if (options.compress && options.compress.webp && /(png|jpe?g)$/i.test(ext)) { 74 | name = name.replace('[ext]', 'webp'); 75 | } 76 | validateOptions(schema, options, 'image-optimize-loader'); 77 | 78 | let compressOption = LOSSY_LOW_COMPRESS_OPTION; 79 | if (options.compress) { 80 | if (options.compress.mode === 'high') { 81 | compressOption = Object.assign(LOSSY_HIGH_COMPRESS_OPTION, options.compress); 82 | } else if (options.compress.mode === 'loseless') { 83 | compressOption = Object.assign(LOSELESS_COMPRESS_OPTION, options.compress); 84 | } else { 85 | compressOption = Object.assign(compressOption, options.compress); 86 | } 87 | } 88 | 89 | let inlineOption = DEFAULT_INLINE_OPTION; 90 | if (options.inline) { 91 | inlineOption = Object.assign(inlineOption, options.inline); 92 | } 93 | 94 | if (mode === 'production' || compressOption.disableOnDevelopment === false || !compressOption.disable) { 95 | const callback = this.async(); 96 | compress(source, compressOption) 97 | .then(data => { 98 | inlineOrEmit.call( 99 | this, 100 | { ...inlineOption, esModule, name, outputPath, publicPath, postTransformPublicPath, context, emitFile }, 101 | data, 102 | callback 103 | ); 104 | }) 105 | .catch(err => { 106 | callback(err); 107 | }); 108 | } else { 109 | inlineOrEmit.call( 110 | this, 111 | { ...inlineOption, esModule, name, outputPath, publicPath, postTransformPublicPath, context, emitFile }, 112 | source, 113 | this.callback 114 | ); 115 | } 116 | } 117 | 118 | module.exports.raw = true; 119 | -------------------------------------------------------------------------------- /src/lib/compress.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @file: use imagemin to compress images 3 | * @author: GaoYYYang 4 | * @date: 06 30 2020 4:49:54 5 | */ 6 | 7 | import imagemin from 'imagemin'; 8 | 9 | export default function compress(content, options) { 10 | const plugins = []; 11 | Object.keys(options).forEach((type) => { 12 | let thisOption = options[type]; 13 | if (typeof thisOption === 'boolean' && thisOption) { 14 | thisOption = {}; 15 | } else if (typeof thisOption !== 'object') { 16 | thisOption = false; 17 | } 18 | if (thisOption) { 19 | try { 20 | /* eslint-disable global-require */ 21 | /* eslint-disable import/no-dynamic-require */ 22 | plugins.push(require(`imagemin-${type}`)(thisOption)); 23 | } catch (e) { 24 | /* jump when require failed */ 25 | } 26 | } 27 | }); 28 | 29 | return imagemin.buffer(content, { 30 | plugins, 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /src/lib/emit.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @file: use file-loader to emit files 3 | * @author: GaoYYYang 4 | * @date: 06 30 2020 3:55:59 5 | */ 6 | 7 | /* eslint-disable import/no-unresolved */ 8 | const fileLoader = require('file-loader'); 9 | 10 | export default function emit(content, options, callback) { 11 | const { name, esModule, outputPath, publicPath, postTransformPublicPath, context, emitFile } = options; 12 | const fallbackLoaderContext = Object.assign({}, this, { 13 | query: { 14 | esModule, 15 | name, 16 | outputPath, 17 | publicPath, 18 | postTransformPublicPath, 19 | context, 20 | emitFile, 21 | }, 22 | }); 23 | 24 | callback(null, fileLoader.call(fallbackLoaderContext, content)); 25 | } 26 | -------------------------------------------------------------------------------- /src/lib/inline.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @file: convert buffer to encoding format 3 | * @author: GaoYYYang 4 | * @date: 06 30 2020 4:43:36 5 | */ 6 | 7 | import path from 'path'; 8 | 9 | import mime from 'mime-types'; 10 | 11 | function getMimetype(mimetype, resourcePath) { 12 | if (typeof mimetype === 'boolean') { 13 | if (mimetype) { 14 | const resolvedMimeType = mime.contentType(path.extname(resourcePath)); 15 | 16 | if (!resolvedMimeType) { 17 | return ''; 18 | } 19 | 20 | return resolvedMimeType.replace(/;\s+charset/i, ';charset'); 21 | } 22 | 23 | return ''; 24 | } 25 | 26 | if (typeof mimetype === 'string') { 27 | return mimetype; 28 | } 29 | 30 | const resolvedMimeType = mime.contentType(path.extname(resourcePath)); 31 | 32 | if (!resolvedMimeType) { 33 | return ''; 34 | } 35 | 36 | return resolvedMimeType.replace(/;\s+charset/i, ';charset'); 37 | } 38 | 39 | function getEncoding(encoding) { 40 | if (typeof encoding === 'boolean') { 41 | return encoding ? 'base64' : ''; 42 | } 43 | 44 | if (typeof encoding === 'string') { 45 | return encoding; 46 | } 47 | 48 | return 'base64'; 49 | } 50 | 51 | function getEncodedData(generator, mimetype, encoding, content, resourcePath) { 52 | if (generator) { 53 | return generator(content, mimetype, encoding, resourcePath); 54 | } 55 | 56 | return `data:${mimetype}${encoding ? `;${encoding}` : ''},${content.toString( 57 | encoding 58 | )}`; 59 | } 60 | 61 | export default function inline(data, inlineOption, callback) { 62 | const { generator, esModule } = inlineOption; 63 | let { mimetype, encoding } = inlineOption; 64 | const { resourcePath } = this; 65 | 66 | mimetype = getMimetype(inlineOption.mimetype, resourcePath); 67 | encoding = getEncoding(inlineOption.encoding); 68 | 69 | let content = data; 70 | if (typeof content === 'string') { 71 | // eslint-disable-next-line no-param-reassign 72 | content = Buffer.from(content); 73 | } 74 | content = getEncodedData(generator, mimetype, encoding, data, resourcePath); 75 | 76 | callback( 77 | null, 78 | `${esModule ? 'export default' : 'module.exports ='} ${JSON.stringify( 79 | content 80 | )}` 81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /src/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "name": { 5 | "type": "string", 6 | "description": "To specify the image file output path." 7 | }, 8 | "outputPath": { 9 | "description": "A filesystem path where the target file(s) will be placed (https://github.com/webpack-contrib/file-loader#outputpath).", 10 | "anyOf": [ 11 | { 12 | "type": "string" 13 | }, 14 | { 15 | "instanceof": "Function" 16 | } 17 | ] 18 | }, 19 | "emitFile": { 20 | "description": "Enables/Disables emit files (https://github.com/webpack-contrib/file-loader#emitfile).", 21 | "type": "boolean" 22 | }, 23 | "publicPath": { 24 | "description": "A custom public path for the target file(s) (https://github.com/webpack-contrib/file-loader#publicpath).", 25 | "anyOf": [ 26 | { 27 | "type": "string" 28 | }, 29 | { 30 | "instanceof": "Function" 31 | } 32 | ] 33 | }, 34 | "postTransformPublicPath": { 35 | "description": "A custom transformation function for post-processing the publicPath (https://github.com/webpack-contrib/file-loader#posttransformpublicpath).", 36 | "instanceof": "Function" 37 | }, 38 | "context": { 39 | "description": "A custom file context (https://github.com/webpack-contrib/file-loader#context).", 40 | "type": "string" 41 | }, 42 | "esModule": { 43 | "type": "boolean" 44 | }, 45 | "inline": { 46 | "type": "object", 47 | "properties": { 48 | "symbol": { 49 | "type": "string", 50 | "description": "Symboled image will be base64 encoded. Default with __inline" 51 | }, 52 | "antiSymbol": { 53 | "type": "string", 54 | "description": "Symboled image will be base64 encoded. Default with __antiInline" 55 | }, 56 | "limit": { 57 | "description": "Enables/Disables transformation target file into base64 URIs. Default with 5000", 58 | "type": ["boolean", "number", "string"] 59 | }, 60 | "encoding": { 61 | "description": "Specify the encoding which the file will be inlined with. Default with base64", 62 | "oneOf": [ 63 | { 64 | "type": "boolean" 65 | }, 66 | { 67 | "enum": ["utf8", "utf16le", "latin1", "base64", "hex", "ascii", "binary", "ucs2"] 68 | } 69 | ] 70 | }, 71 | "mimetype": { 72 | "description": "The MIME type for the file to be transformed.", 73 | "oneOf": [ 74 | { 75 | "type": "boolean" 76 | }, 77 | { 78 | "type": "string" 79 | } 80 | ] 81 | }, 82 | "generator": { 83 | "description": "Adding custom implementation for encoding files.", 84 | "instanceof": "Function" 85 | }, 86 | "disable": { 87 | "type": "boolean" 88 | } 89 | } 90 | }, 91 | "compress": { 92 | "type": "object", 93 | "properties": { 94 | "mode": { 95 | "enum": ["lossless", "high", "low"] 96 | }, 97 | "mozjpeg": { 98 | "additionalProperties": true, 99 | "oneOf": [ 100 | { 101 | "type": "object" 102 | }, 103 | { 104 | "type": "boolean" 105 | } 106 | ] 107 | }, 108 | "optipng": { 109 | "additionalProperties": true, 110 | "oneOf": [ 111 | { 112 | "type": "object" 113 | }, 114 | { 115 | "type": "boolean" 116 | } 117 | ] 118 | }, 119 | "pngquant": { 120 | "additionalProperties": true, 121 | "oneOf": [ 122 | { 123 | "type": "object" 124 | }, 125 | { 126 | "type": "boolean" 127 | } 128 | ] 129 | }, 130 | "gifsicle": { 131 | "additionalProperties": true, 132 | "oneOf": [ 133 | { 134 | "type": "object" 135 | }, 136 | { 137 | "type": "boolean" 138 | } 139 | ] 140 | }, 141 | "webp": { 142 | "additionalProperties": true, 143 | "oneOf": [ 144 | { 145 | "type": "object" 146 | }, 147 | { 148 | "type": "boolean" 149 | } 150 | ] 151 | }, 152 | "svgo": { 153 | "additionalProperties": true, 154 | "oneOf": [ 155 | { 156 | "type": "object" 157 | }, 158 | { 159 | "type": "boolean" 160 | } 161 | ] 162 | }, 163 | "disableOnDevelopment": { 164 | "type": "boolean" 165 | }, 166 | "disable": { 167 | "type": "boolean" 168 | } 169 | } 170 | } 171 | }, 172 | "additionalProperties": false 173 | } 174 | -------------------------------------------------------------------------------- /test/fixtures/images/jpg/mini.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GaoYYYang/image-optimize-loader/74924b7c1d2cb371f85d494456cf2fa06f7369e6/test/fixtures/images/jpg/mini.jpg -------------------------------------------------------------------------------- /test/fixtures/images/png/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GaoYYYang/image-optimize-loader/74924b7c1d2cb371f85d494456cf2fa06f7369e6/test/fixtures/images/png/1.png -------------------------------------------------------------------------------- /test/fixtures/images/png/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GaoYYYang/image-optimize-loader/74924b7c1d2cb371f85d494456cf2fa06f7369e6/test/fixtures/images/png/2.png -------------------------------------------------------------------------------- /test/fixtures/images/png/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GaoYYYang/image-optimize-loader/74924b7c1d2cb371f85d494456cf2fa06f7369e6/test/fixtures/images/png/3.png -------------------------------------------------------------------------------- /test/fixtures/index.js: -------------------------------------------------------------------------------- 1 | import minijpg from './images/jpg/mini.jpg'; 2 | import png1 from './images/png/1.png'; 3 | import png2 from './images/png/2.png'; 4 | import png3 from './images/png/3.png'; 5 | -------------------------------------------------------------------------------- /test/helpers/compile.js: -------------------------------------------------------------------------------- 1 | export default (compiler) => 2 | new Promise((resolve, reject) => { 3 | compiler.run((error, stats) => { 4 | if (error) { 5 | return reject(error); 6 | } 7 | 8 | return resolve(stats); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /test/helpers/execute.js: -------------------------------------------------------------------------------- 1 | import Module from 'module'; 2 | import path from 'path'; 3 | 4 | const parentModule = module; 5 | 6 | export default (code) => { 7 | const resource = 'test.js'; 8 | const module = new Module(resource, parentModule); 9 | // eslint-disable-next-line no-underscore-dangle 10 | module.paths = Module._nodeModulePaths( 11 | path.resolve(__dirname, '../fixtures') 12 | ); 13 | module.filename = resource; 14 | 15 | // eslint-disable-next-line no-underscore-dangle 16 | module._compile( 17 | `let __export__;${code};module.exports = __export__;`, 18 | resource 19 | ); 20 | 21 | return module.exports; 22 | }; 23 | -------------------------------------------------------------------------------- /test/helpers/getCompiler.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import webpack from 'webpack'; 4 | import { createFsFromVolume, Volume } from 'memfs'; 5 | 6 | export default (fixture, loaderOptions = {}, config = {}) => { 7 | const fullConfig = { 8 | mode: 'production', 9 | devtool: config.devtool || false, 10 | context: path.resolve(__dirname, '../fixtures'), 11 | entry: { main: path.resolve(__dirname, '../fixtures', fixture) }, 12 | output: { 13 | path: path.resolve(__dirname, '../outputs'), 14 | filename: '[name].bundle.js', 15 | chunkFilename: '[name].chunk.js', 16 | }, 17 | module: { 18 | rules: [ 19 | // { 20 | // test: /\.js$/i, 21 | // rules: [ 22 | // { 23 | // loader: path.resolve(__dirname, '../../src'), 24 | // options: loaderOptions || {}, 25 | // }, 26 | // ], 27 | // }, 28 | { 29 | test: /\.(png|jpg)$/i, 30 | rules: [ 31 | { 32 | loader: path.resolve(__dirname, '../../dist/index'), 33 | options: loaderOptions || {}, 34 | }, 35 | ], 36 | }, 37 | ], 38 | }, 39 | plugins: [], 40 | ...config, 41 | }; 42 | 43 | const compiler = webpack(fullConfig); 44 | 45 | if (!config.outputFileSystem) { 46 | const outputFileSystem = createFsFromVolume(new Volume()); 47 | // Todo remove when we drop webpack@4 support 48 | outputFileSystem.join = path.join.bind(path); 49 | 50 | compiler.outputFileSystem = outputFileSystem; 51 | } 52 | 53 | return compiler; 54 | }; 55 | -------------------------------------------------------------------------------- /test/helpers/getErrors.js: -------------------------------------------------------------------------------- 1 | import normalizeErrors from './normalizeErrors'; 2 | 3 | export default (stats) => { 4 | return normalizeErrors(stats.compilation.errors); 5 | }; 6 | -------------------------------------------------------------------------------- /test/helpers/getWarnings.js: -------------------------------------------------------------------------------- 1 | import normalizeErrors from './normalizeErrors'; 2 | 3 | export default (stats) => { 4 | return normalizeErrors(stats.compilation.warnings); 5 | }; 6 | -------------------------------------------------------------------------------- /test/helpers/index.js: -------------------------------------------------------------------------------- 1 | import compile from './compile'; 2 | import execute from './execute'; 3 | import getCompiler from './getCompiler'; 4 | import getErrors from './getErrors'; 5 | import getWarnings from './getWarnings'; 6 | import normalizeErrors from './normalizeErrors'; 7 | import readAsset from './readAsset'; 8 | import readsAssets from './readAssets'; 9 | 10 | export { 11 | compile, 12 | execute, 13 | getCompiler, 14 | getErrors, 15 | getWarnings, 16 | normalizeErrors, 17 | readAsset, 18 | readsAssets, 19 | }; 20 | -------------------------------------------------------------------------------- /test/helpers/normalizeErrors.js: -------------------------------------------------------------------------------- 1 | function removeCWD(str) { 2 | const isWin = process.platform === 'win32'; 3 | let cwd = process.cwd(); 4 | 5 | if (isWin) { 6 | // eslint-disable-next-line no-param-reassign 7 | str = str.replace(/\\/g, '/'); 8 | // eslint-disable-next-line no-param-reassign 9 | cwd = cwd.replace(/\\/g, '/'); 10 | } 11 | 12 | return str.replace(new RegExp(cwd, 'g'), ''); 13 | } 14 | 15 | export default (errors) => { 16 | return errors.map((error) => 17 | removeCWD(error.toString().split('\n').slice(0, 2).join('\n')) 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /test/helpers/readAsset.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | export default (asset, compiler, stats) => { 4 | const usedFs = compiler.outputFileSystem; 5 | const outputPath = stats.compilation.outputOptions.path; 6 | 7 | let data = ''; 8 | let targetFile = asset; 9 | 10 | const queryStringIdx = targetFile.indexOf('?'); 11 | 12 | if (queryStringIdx >= 0) { 13 | targetFile = targetFile.substr(0, queryStringIdx); 14 | } 15 | 16 | try { 17 | data = usedFs.readFileSync(path.join(outputPath, targetFile)).toString(); 18 | } catch (error) { 19 | data = error.toString(); 20 | } 21 | 22 | return data; 23 | }; 24 | -------------------------------------------------------------------------------- /test/helpers/readAssets.js: -------------------------------------------------------------------------------- 1 | import readAsset from './readAsset'; 2 | 3 | export default function readAssets(compiler, stats) { 4 | const assets = {}; 5 | 6 | Object.keys(stats.compilation.assets).forEach((asset) => { 7 | assets[asset] = readAsset(asset, compiler, stats); 8 | }); 9 | 10 | return assets; 11 | } 12 | -------------------------------------------------------------------------------- /test/loader.test.js: -------------------------------------------------------------------------------- 1 | import { compile, getCompiler, getErrors, getWarnings, readAsset } from './helpers'; 2 | 3 | test('loader', async () => { 4 | const compiler = getCompiler('index.js'); 5 | const stats = await compile(compiler); 6 | expect(readAsset('main.bundle.js', compiler, stats)).toBeTruthy(); 7 | console.log(getErrors(stats)); 8 | console.log(getWarnings(stats)); 9 | expect(getErrors(stats).length).toBe(0); 10 | expect(getWarnings(stats).length).toBe(0); 11 | }); 12 | --------------------------------------------------------------------------------