├── .codecov.yml
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── UPGRADING.md
├── __tests__
├── index.test.js
└── loaders
│ ├── file-loader.test.js
│ ├── img-loader.test.js
│ ├── index.test.js
│ ├── url-loader.test.js
│ └── webp-loader.test.js
├── example
├── .gitignore
├── README.md
├── next.config.js
├── package-lock.json
├── package.json
└── pages
│ ├── images
│ ├── ben-den-engelsen-unsplash.jpg
│ ├── spencer-davis-unsplash.jpg
│ ├── stage-7-photography-unsplash.jpg
│ └── victor-rodriguez-unsplash.jpg
│ └── index.js
├── lib
├── config.js
├── index.js
├── loaders
│ ├── file-loader.js
│ ├── image-trace-loader.js
│ ├── img-loader.js
│ ├── index.js
│ ├── lqip-loader
│ │ ├── colors-export-loader.js
│ │ ├── index.js
│ │ └── picture-export-loader.js
│ ├── raw-loader
│ │ └── export-loader.js
│ ├── responsive-loader.js
│ ├── svg-sprite-loader
│ │ ├── component.js
│ │ ├── index.js
│ │ └── svg-runtime-generator.js
│ ├── url-loader.js
│ └── webp-loader.js
├── migrater.js
└── resource-queries.js
├── package-lock.json
└── package.json
/.codecov.yml:
--------------------------------------------------------------------------------
1 | coverage:
2 | status:
3 | project:
4 | default:
5 | # basic
6 | target: 0%
7 | threshold: 100%
8 | patch:
9 | default:
10 | # basic
11 | target: 0%
12 | threshold: 100%
13 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | example/
2 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "airbnb-base",
3 | "plugins": [
4 | "jest"
5 | ],
6 | "env": {
7 | "jest/globals": true
8 | },
9 | "rules": {
10 | "jest/no-disabled-tests": "error",
11 | "jest/no-focused-tests": "error",
12 | "jest/no-identical-title": "error",
13 | "import/prefer-default-export": "ignore",
14 | "import/no-extraneous-dependencies": "ignore"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # os
2 | .DS_Store
3 |
4 | # editor
5 | .idea
6 | .vscode
7 |
8 | # node
9 | node_modules
10 | npm-debug.log*
11 |
12 | # project
13 | coverage
14 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 12
4 | - 10
5 | - 8
6 | before_install:
7 | - npm install -g greenkeeper-lockfile
8 | install:
9 | - npm install
10 | - npm install -g codecov
11 | before_script:
12 | - greenkeeper-lockfile-update
13 | script:
14 | - npm test
15 | - npm run lint
16 | - codecov
17 | after_script:
18 | - greenkeeper-lockfile-upload
19 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Cyril Wanner
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # :sunrise: next-optimized-images [](https://www.npmjs.com/package/next-optimized-images) [](https://github.com/cyrilwanner/next-optimized-images/blob/master/LICENSE) [](https://www.npmjs.com/package/next-optimized-images)
2 |
3 | **:bulb: Version 3 is coming!**
4 | It introduces a complete rewrite with many new features and bugfixes. If you want to help developing and testing the upcoming major version, please check out the [canary branch](https://github.com/cyrilwanner/next-optimized-images/tree/canary) for installation instructions and more information about the new features. ([RFC issue](https://github.com/cyrilwanner/next-optimized-images/issues/120))
5 |
6 | ---
7 |
8 | Automatically optimize images used in [next.js](https://github.com/zeit/next.js) projects (`jpeg`, `png`, `svg`, `webp` and `gif`).
9 |
10 | Image sizes can often get reduced between 20-60%, but this is not the only thing `next-optimized-images` does:
11 |
12 | * **Reduces image size** by optimizing images during build
13 | * Improves loading speed by providing **progressive images** (for formats that support it)
14 | * **Inlines small images** to save HTTP requests and additional roundtrips
15 | * Adds a content hash to the file name so images can get cached on CDN level and in the browser for a long time
16 | * Same image URLs over multiple builds for long time caching
17 | * Provides **[query params](#query-params)** for file-specific handling/settings
18 | * jpeg/png images can be **converted to [`webp` on the fly](#webp)** for an even smaller size
19 | * Provides the possibility to use **[`SVG sprites`](#sprite)** for a better performance when using the same icons multiple times (e.g. in a list)
20 | * Can **[resize](#resize)** images or generate different **placeholders while lazy loading** images: [low quality images](#lqip), [dominant colors](#lqip-colors) or [image outlines](#trace)
21 |
22 | ## Table of contents
23 |
24 | - [Installation](#installation)
25 | - [Optimization Packages](#optimization-packages)
26 | - [Usage](#usage)
27 | - [Query Params](#query-params)
28 | - [Configuration](#configuration)
29 | - [Example](#example)
30 | - [See also](#see-also)
31 | - [License](#license)
32 |
33 | ## Installation
34 |
35 | ```
36 | npm install next-optimized-images
37 | ```
38 |
39 | *Node >= 8 is required for version 2. If you need to support older node versions, you can still use [version 1](https://github.com/cyrilwanner/next-optimized-images/tree/v1#readme) of next-optimized-images.*
40 |
41 | Enable the plugin in your Next.js configuration file:
42 |
43 | ```javascript
44 | // next.config.js
45 | const withPlugins = require('next-compose-plugins');
46 | const optimizedImages = require('next-optimized-images');
47 |
48 | module.exports = withPlugins([
49 | [optimizedImages, {
50 | /* config for next-optimized-images */
51 | }],
52 |
53 | // your other plugins here
54 |
55 | ]);
56 | ```
57 |
58 | See the [configuration](#configuration) section for all available options.
59 |
60 | :warning: **From version 2 on, images won't get optimized out of the box anymore. You have to install the optimization packages you really need in addition to this plugin.**
61 | This doesn't force you to download big optimization libraries you don't even use.
62 | Please check out the table of all [optional optimization packages](#optimization-packages).
63 |
64 | The example above uses [next-compose-plugins](https://github.com/cyrilwanner/next-compose-plugins) for a cleaner API when using many plugins, see its readme for a more detailed example. `next-optimized-images` also works with the standard plugin api:
65 |
66 | ```javascript
67 | // next.config.js
68 | const withOptimizedImages = require('next-optimized-images');
69 |
70 | module.exports = withOptimizedImages({
71 | /* config for next-optimized-images */
72 |
73 | // your config for other plugins or the general next.js here...
74 | });
75 | ```
76 |
77 | ## Optimization Packages
78 |
79 | Starting from version 2, you have to install the optimization packages you need in your project in addition to this plugin. `next-optimized-images` then detects all the supported packages and uses them.
80 |
81 | **So you only have to install these packages with npm, there is no additional step needed after that.**
82 |
83 | The following optimization packages are available and supported:
84 |
85 | | Optimization Package | Description | Project Link |
86 | | -------------------- | ----------- | ------------ |
87 | | `imagemin-mozjpeg` | Optimizes JPEG images. | [Link](https://www.npmjs.com/package/imagemin-mozjpeg)
88 | | `imagemin-optipng` | Optimizes PNG images. | [Link](https://www.npmjs.com/package/imagemin-optipng)
89 | | `imagemin-pngquant` | Alternative for optimizing PNG images. | [Link](https://www.npmjs.com/package/imagemin-pngquant)
90 | | `imagemin-gifsicle` | Optimizes GIF images. | [Link](https://www.npmjs.com/package/imagemin-gifsicle)
91 | | `imagemin-svgo` | Optimizes SVG images and icons. | [Link](https://www.npmjs.com/package/imagemin-svgo)
92 | | `svg-sprite-loader` | Adds the possibility to use svg sprites for a better performance. Read the [sprite](#sprite) section for more information. | [Link](https://www.npmjs.com/package/svg-sprite-loader)
93 | | `webp-loader` | Optimizes WebP images and can convert JPEG/PNG images to WebP on the fly ([webp resource query](#webp)). | [Link](https://www.npmjs.com/package/webp-loader)
94 | | `lqip-loader` | Generates low quality image placeholders and can extract the dominant colors of an image ([lqip resource query](#lqip)) | [Link](https://www.npmjs.com/package/lqip-loader)
95 | | `responsive-loader` | Can resize images on the fly and create multiple versions of it for a `srcset`. **Important**: You need to additionally install either `jimp` (node implementation, slower) or `sharp` (binary, faster) | [Link](https://www.npmjs.com/package/responsive-loader)
96 | | `image-trace-loader` | Generates SVG image [outlines](https://twitter.com/mikaelainalem/status/918213244954861569) which can be used as a placeholder while loading the original image ([trace resource query](#trace)). | [Link](https://www.npmjs.com/package/image-trace-loader)
97 |
98 | > Example: If you have JPG, PNG, and SVG images in your project, you would then need to run
99 | > ```bash
100 | > npm install imagemin-mozjpeg imagemin-optipng imagemin-svgo
101 | > ```
102 |
103 | To install *all* optional packages, run:
104 | ```bash
105 | npm install imagemin-mozjpeg imagemin-optipng imagemin-gifsicle imagemin-svgo svg-sprite-loader webp-loader lqip-loader responsive-loader jimp image-trace-loader
106 | ```
107 |
108 | :warning: Please note that by default, images are only optimized for **production builds, not development builds**. However, this can get changed with the [`optimizeImagesInDev` config](#optimizeimagesindev).
109 |
110 | :bulb: Depending on your build/deployment setup, it is also possibile to install these as devDependencies. Just make sure that the packages are available when you build your project.
111 |
112 | :information_source: Since version 2.5, `ico` files are also optionally supported but need to be enabled in the [`handleImages` config](#handleimages).
113 |
114 | ## Usage
115 |
116 | You can now import or require your images directly in your react components:
117 |
118 | ```javascript
119 | import React from 'react';
120 |
121 | export default () => (
122 |
127 | );
128 |
129 | /**
130 | * Results in:
131 | *
132 | *
133 | *
134 | *
135 | *
136 | *
137 | */
138 | ```
139 |
140 | Please be aware that images only get optimized [in production by default](#optimizeimagesindev) to reduce the build time in your development environment.
141 |
142 | If you are using CSS modules, this package also detects images and optimized them in `url()` values in your css/sass/less files:
143 |
144 | ```scss
145 | .Header {
146 | background-image: url('./images/my-image.jpg');
147 | }
148 |
149 | /**
150 | * Results in:
151 | *
152 | * .Header {
153 | * background-image: url('/_next/static/images/my-image-5216de428a8e8bd01a4aa3673d2d1391.jpg');
154 | * }
155 | */
156 | ```
157 |
158 | If the file is below the [limit for inlining images](#inlineimagelimit), the `require(...)` will return a base64 data-uri (`data:image/jpeg;base64,...`).
159 |
160 | Otherwise, `next-optimized-images` will copy your image into the static folder of next.js and the `require(...)` returns the path to your image in this case (`/_next/static/images/my-image-5216de428a8e8bd01a4aa3673d2d1391.jpg`).
161 |
162 | You can use both variants directly on an image in the `src` attribute or in your CSS file inside an `url()` value.
163 |
164 | ### Query params
165 |
166 | > *If you are using flow or eslint-plugin-import and are experiencing some issues with query params, check out the [solution posted by @eleith](https://github.com/cyrilwanner/next-optimized-images/issues/23).*
167 |
168 | There are some cases where you don't want to reference a file or get a base64 data-uri but you actually want to include the raw file directly into your HTML.
169 | Especially for SVGs because you can't style them with CSS if they are in an `src` attribute on an image.
170 |
171 | So there are additional options you can specify as query params when you import the images.
172 |
173 | * [`?include`](#include): Include the raw file directly (useful for SVG icons)
174 | * [`?webp`](#webp): Convert a JPEG/PNG image to WebP on the fly
175 | * [`?inline`](#inline): Force inlining an image (data-uri)
176 | * [`?url`](#url): Force an URL for a small image (instead of data-uri)
177 | * [`?original`](#original): Use the original image and do not optimize it
178 | * [`?lqip`](#lqip): Generate a low quality image placeholder
179 | * [`?lqip-colors`](#lqip-colors): Extract the dominant colors of an image
180 | * [`?trace`](#trace): Use traced outlines as loading placeholder
181 | * [`?resize`](#resize): Resize an image
182 | * [`?sprite`](#sprite): Use SVG sprites
183 |
184 | #### ?include
185 |
186 | The image will now directly be included in your HTML without a data-uri or a reference to your file.
187 |
188 | As described above, this is useful for SVGs so you can style them with CSS.
189 |
190 | ```javascript
191 | import React from 'react';
192 |
193 | export default () => (
194 |
195 | );
196 |
197 | /**
198 | * Results in:
199 | *
200 | *
201 | *
202 | *
203 | *
204 | *
205 | *
206 | */
207 | ```
208 |
209 | The image will still get optimized, even if it is directly included in your content (but by [default only in production](#optimizeimagesindev)).
210 |
211 | #### ?webp
212 |
213 | > Requires the optional optimization package `webp-loader` (`npm install webp-loader`)
214 |
215 | WebP is an even better and smaller image format but it is still not that common yet and developers often only receive jpeg/png images.
216 |
217 | If this `?webp` query parameter is specified, `next-optimized-images` automatically converts a JPEG/PNG image to the new WebP format.
218 |
219 | For browsers that don't yet support WebP, you can also provide a fallback using the `` tag:
220 |
221 | ```javascript
222 | import React from 'react';
223 |
224 | export default () => (
225 |
226 |
227 |
228 |
229 |
230 | );
231 |
232 | /**
233 | * Results in:
234 | *
235 | *
236 | *
237 | *
238 | *
239 | */
240 | ```
241 |
242 | #### ?inline
243 |
244 | You can specify a [limit for inlining](#inlineimagelimit) images which will include it as a data-uri directly in your content instead of referencing a file if the file size is below that limit.
245 |
246 | You usually don't want to specify a too high limit but there may be cases where you still want to inline larger images.
247 |
248 | In this case, you don't have to set the global limit to a higher value but you can add an exception for a single image using the `?inline` query options.
249 |
250 | ```javascript
251 | import React from 'react';
252 |
253 | export default () => (
254 |
255 | );
256 |
257 | /**
258 | * Results in:
259 | *
260 | *
261 | *
262 | * Even if the image size is above the defined limit.
263 | */
264 | ```
265 |
266 | The inlining will only get applied to exactly this import, so if you import the image a second time without the `?inline` option, it will then get normally referenced as a file if it is above your limit.
267 |
268 | #### ?url
269 |
270 | When you have an image smaller than your defined [limit for inlining](#inlineimagelimit), it normally gets inlined automatically.
271 | If you don't want a specific small file to get inlined, you can use the `?url` query param to always get back an image URL, regardless of the inline limit.
272 |
273 | If you are using this option a lot, it could also make sense to [disable the inlining](#inlineimagelimit) completely and use the [`?inline`](#inline) param for single files.
274 |
275 | ```javascript
276 | import React from 'react';
277 |
278 | export default () => (
279 |
280 | );
281 |
282 | /**
283 | * Results in:
284 | *
285 | *
286 | *
287 | * Even if the image size is below the defined inlining limit.
288 | */
289 | ```
290 |
291 | The inlining will only get disabled for exactly this import, so if you import the image a second time without the `?url` option, it will then get inlined again if it is below your limit.
292 |
293 | #### ?original
294 |
295 | The image won't get optimized and used as it is.
296 | It makes sense to use this query param if you know an image already got optimized (e.g. during export) so it doesn't get optimized again a second time.
297 |
298 | ```javascript
299 | import React from 'react';
300 |
301 | export default () => (
302 |
303 | );
304 | ```
305 |
306 | This can also be combined with the `?url` or `?inline` resource query (e.g. `?original&inline`).
307 |
308 | #### ?lqip
309 |
310 | > Requires the optional package `lqip-loader` (`npm install lqip-loader`)
311 |
312 | When using this resource query, a very small (about 10x7 pixel) image gets created.
313 | You can then display this image as a placeholder until the real (big) image has loaded.
314 |
315 | You will normally stretch this tiny image to the same size as the real image is, like *medium.com* does.
316 | To make the stretched image look better in chrome, check out [this solution](https://github.com/zouhir/lqip-loader/issues/5) and add a blur filter to your image.
317 |
318 | ```javascript
319 | import React from 'react';
320 |
321 | export default () => (
322 |
323 | );
324 |
325 | /**
326 | * Replaces the src with a tiny image in base64.
327 | */
328 | ```
329 |
330 | #### ?lqip-colors
331 |
332 | > Requires the optional package `lqip-loader` (`npm install lqip-loader`)
333 |
334 | This resource query returns you an **array with hex values** of the dominant colors of an image.
335 | You can also use this as a placeholder until the real image has loaded (e.g. as a background) like the *Google Picture Search* does.
336 |
337 | The number of colors returned can vary and depends on how many different colors your image has.
338 |
339 | ```javascript
340 | import React from 'react';
341 |
342 | export default () => (
343 | ...
344 | );
345 |
346 | /**
347 | * require('./images/my-image.jpg?lqip-colors')
348 | *
349 | * returns for example
350 | *
351 | * ['#0e648d', '#5f94b5', '#a7bbcb', '#223240', '#a4c3dc', '#1b6c9c']
352 | */
353 | ```
354 |
355 | #### ?trace
356 |
357 | > Requires the optional package `image-trace-loader` (`npm install image-trace-loader`)
358 |
359 | With the `?trace` resource query, you can generate [SVG image outlines](https://twitter.com/mikaelainalem/status/918213244954861569) which can be used as a placeholder while loading the original image.
360 |
361 | ```javascript
362 | import React from 'react';
363 | import MyImage from './images/my-image.jpg?trace';
364 |
365 | export default () => (
366 |
367 |
{/* <-- SVG trace */}
368 |
{/* <-- Normal image which you want to lazy load */}
369 |
370 | );
371 |
372 | /**
373 | * Results in:
374 | *
375 | *
376 | *
377 | *
378 | *
379 | */
380 | ```
381 |
382 | `require('./images/my-image.jpg?trace')` returns an object containing the trace (`trace`) as an inlined SVG and the normal image (`src`) which also gets optimized.
383 |
384 | The trace will have exactly the same width and height as your normal image.
385 |
386 | Options for the loader can be set in the [plugin configuration](#imagetrace).
387 |
388 | #### ?resize
389 |
390 | > Requires the optional package `responsive-loader` (`npm install responsive-loader`)
391 | > and either `jimp` (node implementation, slower) or `sharp` (binary, faster)
392 |
393 | After the `?resize` resource query, you can add any other query of the [`responsive-loader`](https://www.npmjs.com/package/responsive-loader) which allows you to resize images and create whole source sets.
394 |
395 | ```javascript
396 | import React from 'react';
397 |
398 | const oneSize = require('./images/my-image.jpg?resize&size=300');
399 | const multipleSizes = require('./images/my-image.jpg?resize&sizes[]=300&sizes[]=600&sizes[]=1000');
400 |
401 | export default () => (
402 |
403 | {/* Single image: */}
404 |
405 |
406 | {/* Source set with multiple sizes: */}
407 |
408 |
409 | );
410 | ```
411 |
412 | If only the `size` or `sizes` param is used, the `?resize` param can also be omitted (e.g. `my-image.jpg?size=300`). But it is required for all other parameters of `responsive-loader`.
413 |
414 | You can also set global configs in the [`responsive`](#responsive) property (in the `next.config.js` file) and define, for example, default sizes which will get generated when you don't specify one for an image (e.g. only `my-image.jpg?resize`).
415 |
416 | #### ?sprite
417 |
418 | > Requires the optional optimization packages `imagemin-svgo` and `svg-sprite-loader` (`npm install imagemin-svgo svg-sprite-loader`)
419 |
420 | If you need to style or animate your SVGs [?include](#?include) might be the wrong option, because that ends up in a lot of DOM elements, especially when using the SVG in list-items etc.
421 | In that case, you can use `?sprite` which uses [svg-sprite-loader](https://github.com/kisenka/svg-sprite-loader) to render and inject an SVG sprite in the page automatically.
422 |
423 | ```javascript
424 | import React from 'react';
425 | import MyIcon from './icons/my-icon.svg?sprite';
426 |
427 | export default () => (
428 |
429 | my page..
430 |
431 |
432 | );
433 | ```
434 |
435 | All props passed to the imported sprite will get applied to the `` element, so you can add a class normally with ` `.
436 |
437 | The `svg-sprite-loader` object also gets exposed if you want to build your own component:
438 |
439 | ```javascript
440 | import React from 'react';
441 | import icon from './icons/icon.svg?sprite';
442 |
443 | export default () => (
444 |
445 | my page..
446 |
447 |
448 |
449 |
450 | );
451 | ```
452 |
453 | To also make this work for server-side rendering, you need to add these changes to your `_document.jsx` file (read [here](https://nextjs.org/docs/#custom-document) if you don't have this file yet):
454 |
455 | ```javascript
456 | // ./pages/_document.js
457 | import Document, { Head, Main, NextScript } from 'next/document';
458 | import sprite from 'svg-sprite-loader/runtime/sprite.build';
459 |
460 | export default class MyDocument extends Document {
461 | static async getInitialProps(ctx) {
462 | const initialProps = await Document.getInitialProps(ctx);
463 | const spriteContent = sprite.stringify();
464 |
465 | return {
466 | spriteContent,
467 | ...initialProps,
468 | };
469 | }
470 |
471 | render() {
472 | return (
473 |
474 | {/* your head if needed */}
475 |
476 |
477 |
478 |
479 |
480 |
481 | );
482 | }
483 | }
484 | ```
485 |
486 | ## Configuration
487 |
488 | This plugin uses [img-loader](https://www.npmjs.com/package/img-loader) under the hood which is based on [mozjpeg](https://github.com/imagemin/imagemin-mozjpeg), [optipng](https://github.com/imagemin/imagemin-optipng), [gifsicle](https://github.com/imagemin/imagemin-gifsicle) and [svgo](https://github.com/imagemin/imagemin-svgo).
489 |
490 | The default options for these optimizers should be enough in most cases, but you can overwrite every available option if you want to.
491 |
492 | #### handleImages
493 |
494 | Type: `string[]`
495 | Default: `['jpeg', 'png', 'svg', 'webp', 'gif']`
496 |
497 | `next-optimized-images` registers the webpack loader for all these file types.
498 | If you don't want one of these handled by next-optimized-images because you, for example, have another plugin or custom loader rule, simply remove it from the array.
499 |
500 | Please note that an image being handled does not mean it also gets automatically optimized. The required optimization package for that image also has to be installed. Please read the [optimization packages](#optimization-packages) section for more information.
501 |
502 | If an image gets handled but not optimized, it means that the original image will get used and copied for the build.
503 |
504 | :information_source: Since version 2.5, `ico` files are also supported but for backwards compatibility, they need to be manually enabled.
505 | By adding `'ico'` to the `handleImages` array, the plugin will also handle `ico` files.
506 |
507 | #### inlineImageLimit
508 |
509 | Type: `number`
510 | Default: `8192`
511 |
512 | Smaller files will get inlined with a data-uri by [url-loader](https://www.npmjs.com/package/url-loader).
513 | This number defines the maximum file size (in bytes) for images to get inlined.
514 | If an image is bigger, it will get copied to the static folder of next.
515 |
516 | Images will get optimized in both cases.
517 |
518 | To completely disable image inlining, set this value to `-1`. You will then always get back an image URL.
519 |
520 | #### imagesFolder
521 |
522 | Type: `string`
523 | Default: `'images'`
524 |
525 | Folder name inside `/static/` in which the images will get copied to during build.
526 |
527 | #### imagesPublicPath
528 |
529 | Type: `string`
530 | Default: ``` `/_next/static/${imagesFolder}/` ```
531 |
532 | The public path that should be used for image URLs. This can be used to serve
533 | the optimized image from a cloud storage service like S3.
534 |
535 | From version 2 on, next-optimized-images uses the [`assetPrefx` config of next.js](https://nextjs.org/docs/#cdn-support-with-asset-prefix) by default, but you can overwrite it with `imagesPublicPath` specially for images.
536 |
537 | #### imagesOutputPath
538 |
539 | Type: `string`
540 | Default: ``` `static/${imagesFolder}/` ```
541 |
542 | The output path that should be used for images. This can be used to have a custom output folder.
543 |
544 | #### imagesName
545 |
546 | Type: `string`
547 | Default: `'[name]-[hash].[ext]'`
548 |
549 | The filename of the optimized images.
550 | Make sure you keep the `[hash]` part so they receive a new filename if the content changes.
551 |
552 | #### removeOriginalExtension
553 |
554 | Type: `boolean`
555 | Default: `false`
556 |
557 | When images converted to WebP on the fly, `.webp` was append to the filename. For example, `test.png` became `test.png.webp`. If you want to have only one filename extension like `test.webp`, you can set this option to `true`.
558 |
559 | #### optimizeImagesInDev
560 |
561 | Type: `boolean`
562 | Default: `false`
563 |
564 | For faster development builds and HMR, images will not get optimized by default when running in development mode.
565 | In production, images will always get optimized, regardless of this setting.
566 |
567 | #### mozjpeg
568 |
569 | > Requires the optional optimization package `imagemin-mozjpeg` (`npm install imagemin-mozjpeg`)
570 |
571 | Type: `object`
572 | Default: `{}`
573 |
574 | [mozjpeg](https://github.com/imagemin/imagemin-mozjpeg) is used for optimizing jpeg images.
575 | You can specify the options for it here.
576 | The default options of `mozjpeg` are used if you omit this option.
577 |
578 | #### optipng
579 |
580 | > Requires the optional optimization package `imagemin-optipng` (`npm install imagemin-optipng`)
581 |
582 | Type: `object`
583 | Default: `{}`
584 |
585 | [optipng](https://github.com/imagemin/imagemin-optipng) is used for optimizing png images by default.
586 | You can specify the options for it here.
587 | The default options of `optipng` are used if you omit this option.
588 |
589 | #### pngquant
590 |
591 | > Requires the optional optimization package `imagemin-pngquant` (`npm install imagemin-pngquant`)
592 |
593 | Type: `object`
594 | Default: `{}`
595 |
596 | [pngquant](https://github.com/imagemin/imagemin-pngquant) is an alternative way for optimizing png images.
597 | The default options of `pngquant` are used if you omit this option.
598 |
599 | #### gifsicle
600 |
601 | > Requires the optional optimization package `imagemin-gifsicle` (`npm install imagemin-gifsicle`)
602 |
603 | Type: `object`
604 | Default:
605 | ```javascript
606 | {
607 | interlaced: true,
608 | optimizationLevel: 3,
609 | }
610 | ```
611 |
612 | [gifsicle](https://github.com/imagemin/imagemin-gifsicle) is used for optimizing gif images.
613 | You can specify the options for it here.
614 | The default options of `gifsicle` are used if you omit this option.
615 |
616 | #### svgo
617 |
618 | > Requires the optional optimization package `imagemin-svgo` (`npm install imagemin-svgo`)
619 |
620 | Type: `object`
621 | Default: `{}`
622 |
623 | [svgo](https://github.com/imagemin/imagemin-svgo) is used for optimizing svg images and icons.
624 | You can specify the options for it here.
625 | The default options of `svgo` are used if you omit this option.
626 |
627 | Single svgo plugins can get disabled/enabled in the plugins array:
628 | ```javascript
629 | {
630 | svgo: {
631 | plugins: [
632 | { removeComments: false }
633 | ]
634 | }
635 | }
636 | ```
637 |
638 | #### svgSpriteLoader
639 |
640 | > Requires the optional optimization packages `imagemin-svgo` and `svg-sprite-loader` (`npm install imagemin-svgo svg-sprite-loader`)
641 |
642 | Type: `object`
643 | Default:
644 | ```javascript
645 | {
646 | runtimeGenerator: require.resolve(path.resolve('node_modules', 'next-optimized-images', 'svg-runtime-generator.js')),
647 | }
648 | ```
649 |
650 | When using the [svg sprite option](#sprite), [`svg-sprite-loader`](https://github.com/kisenka/svg-sprite-loader) gets used internally.
651 | You can overwrite the configuration passed to this loader here.
652 |
653 | #### webp
654 |
655 | > Requires the optional optimization package `webp-loader` (`npm install webp-loader`)
656 |
657 | Type: `object`
658 | Default: `{}`
659 |
660 | [imagemin-webp](https://github.com/imagemin/imagemin-webp) is used for optimizing webp images and converting other formats to webp.
661 | You can specify the options for it here.
662 | The default options of `imagemin-webp` are used if you omit this option.
663 |
664 | #### imageTrace
665 |
666 | > Requires the optional package `image-trace-loader` (`npm install image-trace-loader`)
667 |
668 | Type: `object`
669 | Default: `{}`
670 |
671 | When using [`image-trace-loader`](https://github.com/EmilTholin/image-trace-loader) for the `?trace` resource query, you can define all options for the image trace loader in this object.
672 | The default options of `image-trace-loader` are used if you omit this option.
673 |
674 | #### responsive
675 |
676 | > Requires the optional optimization package `responsive-loader` (`npm install responsive-loader`)
677 |
678 | Type: `object`
679 | Default: `{}`
680 |
681 | The configuration for the [`responsive-loader`](https://github.com/herrstucki/responsive-loader) can be defined here.
682 |
683 | #### defaultImageLoader
684 |
685 | > Requires the optional optimization package `responsive-loader` (`npm install responsive-loader`)
686 |
687 | Type: `string`
688 | Default: `'img-loader'`
689 |
690 | By default, img-loader handles most of the requests.
691 | However, if you use the `responsive-loader` a lot and don't want to add the [`?resize`](#resize) query param to every require, you can set this value to `'responsive-loader'`.
692 |
693 | After that, `responsive-loader` will handle *all* JPEG and PNG images per default, even without an additional query param.
694 | Just be aware that you can't use any of the query params `next-optimized-images` provides anymore on these images because the request just gets forwarded and not modified anymore.
695 | All other formats (SVG, WEBP and GIF) still work as before with the `img-loader` and so have all query params available.
696 |
697 | #### optimizeImages
698 |
699 | Type: `boolean`
700 | Default: `true`
701 |
702 | If you don't have any optimization package installed, no image will get optimized.
703 | In this case, a warning gets written to the console during build to inform you about a possible misconfiguration.
704 | If this config is intended and you indeed don't want the images to be optimized, you can set this value to `false` and you won't get the warning anymore.
705 |
706 | ## Example
707 |
708 | The options specified here are the **default** values.
709 |
710 | So if they are good enough for your use-case, you don't have to specify them to have a shorter and cleaner `next.config.js` file.
711 |
712 | ```javascript
713 | // next.config.js
714 | const withPlugins = require('next-compose-plugins');
715 | const optimizedImages = require('next-optimized-images');
716 |
717 | module.exports = withPlugins([
718 | [optimizedImages, {
719 | // these are the default values so you don't have to provide them if they are good enough for your use-case.
720 | // but you can overwrite them here with any valid value you want.
721 | inlineImageLimit: 8192,
722 | imagesFolder: 'images',
723 | imagesName: '[name]-[hash].[ext]',
724 | handleImages: ['jpeg', 'png', 'svg', 'webp', 'gif'],
725 | removeOriginalExtension: false,
726 | optimizeImages: true,
727 | optimizeImagesInDev: false,
728 | mozjpeg: {
729 | quality: 80,
730 | },
731 | optipng: {
732 | optimizationLevel: 3,
733 | },
734 | pngquant: false,
735 | gifsicle: {
736 | interlaced: true,
737 | optimizationLevel: 3,
738 | },
739 | svgo: {
740 | // enable/disable svgo plugins here
741 | },
742 | webp: {
743 | preset: 'default',
744 | quality: 75,
745 | },
746 | }],
747 | ]);
748 | ```
749 |
750 | ## See also
751 |
752 | * [next-images](https://github.com/arefaslani/next-images) if you just want images and not optimize them
753 | * [next-compose-plugins](https://github.com/cyrilwanner/next-compose-plugins) for a cleaner plugins API when you have many plugins in your `next.config.js` file
754 | * [next-plugins](https://github.com/zeit/next-plugins) for a list of official and community made plugins
755 |
756 | ## License
757 |
758 | [MIT](https://github.com/cyrilwanner/next-optimized-images/blob/master/LICENSE) © Cyril Wanner
759 |
--------------------------------------------------------------------------------
/UPGRADING.md:
--------------------------------------------------------------------------------
1 | # Upgrading
2 |
3 | ## From version `1.x` to `2.x`
4 |
5 | From version 1 to 2, some breaking changes were introduced. Please read this document carefully and update your configuration if needed.
6 |
7 | ### Optional optimization packages
8 |
9 | In version 1, we installed all possible optimization packages (JPEG, PNG, GIF, SVG, SVG Sprites, WebP) even if you didn't need them. Some of them also caused troubles in specific environments even though they weren't used.
10 |
11 | With version 2, we also updated the underlying [`img-loader`](https://www.npmjs.com/package/img-loader) which now enables us to have all optimization packages optional.
12 |
13 | This now enables you to only install the ones you really need and in the build step you need them (e.g. only as a devDependency).
14 | **But you have to install them manually in addition to next-optimized-images.**
15 |
16 | To have the same behavior as in *v1* (all optimization packages installed), run the following command *after* upgrading to next-optimized-images:
17 | ```bash
18 | npm install imagemin-mozjpeg imagemin-optipng imagemin-gifsicle imagemin-svgo svg-sprite-loader webp-loader
19 | ```
20 |
21 | If you don't need all these optimization packages, please read the [optimization packages section in the readme](https://github.com/cyrilwanner/next-optimized-images#optimization-packages).
22 |
23 | ### Node version
24 |
25 | To keep up with the newest next.js version, we now also require **Node.js >= 8**.
26 |
27 | If you need to support older node versions, you can still use [version 1](https://github.com/cyrilwanner/next-optimized-images/tree/v1#readme) of next-optimized-images.
28 |
29 | ### Configuration changes
30 |
31 | #### `assetPrefix` and `imagesPublicPath`
32 |
33 | next-optimized-images now uses the [`assetPrefix` config of next.js](https://nextjs.org/docs/#cdn-support-with-asset-prefix) by default.
34 | So if your images are located at the same place as your other assets, you don't have to set the `imagesPublicPath` anymore.
35 |
36 | But this config still exists and if specified, overwrites the `assetPrefix` setting for images.
37 |
38 | #### Don't optimize an image type
39 |
40 | Previously, if you didn't want, for example, jpeg images to be optimized, you could set `mozjpeg` to `null` (same for `optipng`, `pngquant`, `gifsicle`, `svgo` or `webp`).
41 |
42 | From now on, simply uninstall (if you had it installed) the `imagemin-mozjpeg` package (or the optimization package for the respective image type).
43 |
44 | #### Don't handle an image type
45 |
46 | Previously, if you didn't want, for example, jpeg images to be handled by next-optimized-images because you had another plugin or a custom loader definition for it, you could set `mozjpeg` to `false` (same for `optipng`, `pngquant`, `gifsicle`, `svgo` or `webp`).
47 |
48 | Now, this setting is in a single configuration key, the [`handleImages`](https://github.com/cyrilwanner/next-optimized-images#handleimages).
49 | You can define the image types which next-optimized-images should handle within this array:
50 | ```javascript
51 | {
52 | // default value:
53 | handleImages: ['jpeg', 'png', 'svg', 'webp', 'gif'],
54 | }
55 | ```
56 |
57 | ## Having questions?
58 |
59 | Please do not hesitate to open a [new issue](https://github.com/cyrilwanner/next-optimized-images/issues/new) if you have a question or problem while updating `next-optimized-images`.
60 |
--------------------------------------------------------------------------------
/__tests__/index.test.js:
--------------------------------------------------------------------------------
1 | const withOptimizedImages = require('../lib');
2 |
3 | const getNextConfig = (options, webpackOptions = {}) => {
4 | const webpackConfig = {
5 | module: {
6 | rules: [],
7 | },
8 | };
9 |
10 | return withOptimizedImages(options).webpack(Object.assign({}, webpackConfig), Object.assign({
11 | defaultLoaders: [],
12 | dev: false,
13 | }, webpackOptions));
14 | };
15 |
16 | describe('next-optimized-images', () => {
17 | it('returns a next.js config object', () => {
18 | const config = withOptimizedImages();
19 |
20 | expect(typeof config.webpack).toBe('function');
21 | });
22 |
23 | it('handles all images by default', () => {
24 | const config = getNextConfig();
25 |
26 | expect(config.module.rules).toHaveLength(2);
27 |
28 | const rule = config.module.rules[0];
29 | const webpRule = config.module.rules[1];
30 |
31 | expect(rule.test.test('.jpg')).toEqual(true);
32 | expect(rule.test.test('.jpeg')).toEqual(true);
33 | expect(rule.test.test('.png')).toEqual(true);
34 | expect(rule.test.test('.gif')).toEqual(true);
35 | expect(rule.test.test('.svg')).toEqual(true);
36 | expect(rule.test.test('.JPG')).toEqual(true);
37 | expect(rule.test.test('.JPEG')).toEqual(true);
38 | expect(rule.test.test('.PNG')).toEqual(true);
39 | expect(rule.test.test('.GIF')).toEqual(true);
40 | expect(rule.test.test('.SVG')).toEqual(true);
41 | expect(rule.test.test('.webp')).toEqual(false);
42 | expect(rule.test.test('.WEBP')).toEqual(false);
43 | expect(webpRule.test.test('.webp')).toEqual(true);
44 | expect(webpRule.test.test('.WEBP')).toEqual(true);
45 | });
46 |
47 | it('can disable image types', () => {
48 | const config = getNextConfig({ handleImages: ['jpeg'] });
49 |
50 | expect(config.module.rules).toHaveLength(1);
51 |
52 | const rule = config.module.rules[0];
53 |
54 | expect(rule.test.test('.jpg')).toEqual(true);
55 | expect(rule.test.test('.jpeg')).toEqual(true);
56 | expect(rule.test.test('.png')).toEqual(false);
57 | expect(rule.test.test('.gif')).toEqual(false);
58 | expect(rule.test.test('.svg')).toEqual(false);
59 | expect(rule.test.test('.JPG')).toEqual(true);
60 | expect(rule.test.test('.JPEG')).toEqual(true);
61 | expect(rule.test.test('.PNG')).toEqual(false);
62 | expect(rule.test.test('.GIF')).toEqual(false);
63 | expect(rule.test.test('.SVG')).toEqual(false);
64 | expect(rule.test.test('.webp')).toEqual(false);
65 | expect(rule.test.test('.WEBP')).toEqual(false);
66 | });
67 |
68 | it('propagates and merges configuration', () => {
69 | const config = getNextConfig({
70 | webpack: (webpackConfig, webpackOptions) => {
71 | expect(webpackConfig.module.rules).toHaveLength(2);
72 | expect(webpackOptions.dev).toEqual(false);
73 | expect(webpackOptions.foo).toEqual('bar');
74 |
75 | return Object.assign({
76 | changed: true,
77 | }, webpackConfig);
78 | },
79 | }, { foo: 'bar' });
80 |
81 | expect(config.module.rules).toHaveLength(2);
82 | expect(config.changed).toEqual(true);
83 | });
84 |
85 | it('only supports next.js >= 5', () => {
86 | expect(() => {
87 | getNextConfig({}, { defaultLoaders: false });
88 | }).toThrow();
89 | });
90 | });
91 |
--------------------------------------------------------------------------------
/__tests__/loaders/file-loader.test.js:
--------------------------------------------------------------------------------
1 | const { getConfig } = require('../../lib/config');
2 | const { getFileLoaderOptions } = require('../../lib/loaders/file-loader');
3 |
4 | describe('next-optimized-images/loaders/file-loader', () => {
5 | it('uses the default config', () => {
6 | const options = getFileLoaderOptions(getConfig({}), false);
7 |
8 | expect(options.publicPath).toEqual('/_next/static/images/');
9 | expect(options.outputPath).toEqual('static/images/');
10 | expect(options.name).toEqual('[name]-[hash].[ext]');
11 | });
12 |
13 | it('uses the correct directory on the server', () => {
14 | const options = getFileLoaderOptions(getConfig({}), true);
15 |
16 | expect(options.outputPath).toEqual('../static/images/');
17 | });
18 |
19 | it('uses the assetPrefix config', () => {
20 | const options1 = getFileLoaderOptions(getConfig({ assetPrefix: 'https://cdn.com/' }), false);
21 | const options2 = getFileLoaderOptions(getConfig({ assetPrefix: 'https://cdn.com' }), false);
22 | const options3 = getFileLoaderOptions(getConfig({ assetPrefix: 'https://cdn.com/', imagesFolder: 'img-test' }), false);
23 |
24 | expect(options1.publicPath).toEqual('https://cdn.com/_next/static/images/');
25 | expect(options2.publicPath).toEqual('https://cdn.com/_next/static/images/');
26 | expect(options3.publicPath).toEqual('https://cdn.com/_next/static/img-test/');
27 | });
28 |
29 | it('overwrites assetPrefix config with imagesPublicPath', () => {
30 | const options = getFileLoaderOptions(getConfig({ assetPrefix: 'https://cdn.com/', imagesPublicPath: 'https://another-cdn.com/' }), false);
31 |
32 | expect(options.publicPath).toEqual('https://another-cdn.com/');
33 | });
34 |
35 | it('allows overwriting the output path', () => {
36 | const options = getFileLoaderOptions(getConfig({ imagesOutputPath: 'another-path/' }), true);
37 |
38 | expect(options.outputPath).toEqual('another-path/');
39 | });
40 | });
41 |
--------------------------------------------------------------------------------
/__tests__/loaders/img-loader.test.js:
--------------------------------------------------------------------------------
1 | const {
2 | getImgLoaderOptions,
3 | getHandledFilesRegex,
4 | applyImgLoader,
5 | requireImageminPlugin,
6 | } = require('../../lib/loaders/img-loader');
7 | const { getConfig } = require('../../lib/config');
8 |
9 | module.exports = () => () => ({ plugin: true });
10 |
11 | describe('next-optimized-images/loaders/img-loader', () => {
12 | it('adds the correct plugins', () => {
13 | const plugins1 = getImgLoaderOptions({}, { png: __filename }, true);
14 | const plugins2 = getImgLoaderOptions({}, { png: __filename }, false);
15 |
16 | expect(plugins1.plugins).toHaveLength(1);
17 | expect(plugins2.plugins).toHaveLength(0);
18 | });
19 |
20 | it('allows overwriting the resolve path', () => {
21 | const plugin = requireImageminPlugin('img-loader.test.js', { overwriteImageLoaderPaths: __dirname });
22 |
23 | expect(plugin()).toEqual({ plugin: true });
24 | });
25 |
26 | it('generates the correct file regex', () => {
27 | const regex1 = getHandledFilesRegex({
28 | jpeg: true,
29 | png: false,
30 | svg: false,
31 | gif: false,
32 | });
33 | const regex2 = getHandledFilesRegex({
34 | jpeg: false,
35 | png: false,
36 | svg: true,
37 | gif: true,
38 | });
39 | const regex3 = getHandledFilesRegex({
40 | jpeg: true,
41 | png: true,
42 | svg: true,
43 | gif: true,
44 | });
45 |
46 | expect(regex1.test('.jpg')).toEqual(true);
47 | expect(regex1.test('.jpeg')).toEqual(true);
48 | expect(regex1.test('.JpEg')).toEqual(true);
49 | expect(regex1.test('.png')).toEqual(false);
50 | expect(regex1.test('.svg')).toEqual(false);
51 | expect(regex1.test('.gif')).toEqual(false);
52 |
53 | expect(regex2.test('.jpg')).toEqual(false);
54 | expect(regex2.test('.png')).toEqual(false);
55 | expect(regex2.test('.svg')).toEqual(true);
56 | expect(regex2.test('.gif')).toEqual(true);
57 |
58 | expect(regex3.test('.jpg')).toEqual(true);
59 | expect(regex3.test('.png')).toEqual(true);
60 | expect(regex3.test('.svg')).toEqual(true);
61 | expect(regex3.test('.gif')).toEqual(true);
62 | });
63 |
64 | it('adds rules to the webpack config', () => {
65 | const webpackConfig = { module: { rules: [] } };
66 | applyImgLoader(webpackConfig, getConfig({}), true, false, {
67 | jpeg: '../../__tests__/loaders/img-loader.test.js',
68 | png: false,
69 | svg: '../../__tests__/loaders/img-loader.test.js',
70 | gif: false,
71 | }, {
72 | jpeg: true,
73 | png: true,
74 | svg: true,
75 | gif: false,
76 | });
77 |
78 | const rule = webpackConfig.module.rules[0];
79 |
80 | expect(rule.test).toBeInstanceOf(RegExp);
81 | expect(rule.oneOf).toHaveLength(13);
82 | });
83 | });
84 |
--------------------------------------------------------------------------------
/__tests__/loaders/index.test.js:
--------------------------------------------------------------------------------
1 | const {
2 | isModuleInstalled,
3 | detectLoaders,
4 | getHandledImageTypes,
5 | getNumOptimizationLoadersInstalled,
6 | appendLoaders,
7 | } = require('../../lib/loaders');
8 | const { getConfig } = require('../../lib/config');
9 |
10 | module.exports = () => () => ({ plugin: true });
11 |
12 | describe('next-optimized-images/loaders', () => {
13 | it('detects if a module is installed', () => {
14 | expect(isModuleInstalled('path')).toEqual(true);
15 | expect(isModuleInstalled('pathalksdfjladksfj')).toEqual(false);
16 | expect(isModuleInstalled('img-loader.test.js')).toEqual(false);
17 | expect(isModuleInstalled('img-loader.test.js', __dirname)).toEqual(true);
18 | });
19 |
20 | it('detects installed loaders', () => {
21 | expect(detectLoaders()).toEqual({
22 | jpeg: false,
23 | gif: false,
24 | svg: false,
25 | svgSprite: 'svg-sprite-loader', // is in the devDependencies
26 | webp: false,
27 | png: false,
28 | lqip: false,
29 | responsive: false,
30 | responsiveAdapter: false,
31 | });
32 | });
33 |
34 | it('returns the handled image types', () => {
35 | expect(getHandledImageTypes(getConfig({}))).toEqual({
36 | jpeg: true,
37 | png: true,
38 | svg: true,
39 | webp: true,
40 | gif: true,
41 | ico: false,
42 | });
43 |
44 | expect(getHandledImageTypes(getConfig({ handleImages: ['jpg', 'png', 'ico'] }))).toEqual({
45 | jpeg: true,
46 | png: true,
47 | svg: false,
48 | webp: false,
49 | gif: false,
50 | ico: true,
51 | });
52 |
53 | expect(getHandledImageTypes(getConfig({ handleImages: [] }))).toEqual({
54 | jpeg: false,
55 | png: false,
56 | svg: false,
57 | webp: false,
58 | gif: false,
59 | ico: false,
60 | });
61 | });
62 |
63 | it('counts the number of optimization loaders', () => {
64 | expect(getNumOptimizationLoadersInstalled({
65 | jpeg: 'imagemin-jpeg',
66 | png: 'imagemin-png',
67 | svgSprite: 'svg-sprite-loader',
68 | })).toEqual(2);
69 | });
70 |
71 | it('appends loaders to the webpack config', () => {
72 | const webpackConfig = { module: { rules: [] } };
73 |
74 | appendLoaders(webpackConfig, getConfig({}), {
75 | jpeg: __filename,
76 | webp: __filename,
77 | }, false, true);
78 |
79 | expect(webpackConfig.module.rules).toHaveLength(2);
80 | });
81 | });
82 |
--------------------------------------------------------------------------------
/__tests__/loaders/url-loader.test.js:
--------------------------------------------------------------------------------
1 | const { getConfig } = require('../../lib/config');
2 | const { getUrlLoaderOptions } = require('../../lib/loaders/url-loader');
3 | const { getFileLoaderPath } = require('../../lib/loaders/file-loader');
4 |
5 | describe('next-optimized-images/loaders/url-loader', () => {
6 | it('uses the default config', () => {
7 | const options = getUrlLoaderOptions(getConfig({}), false);
8 |
9 | expect(options.limit).toEqual(8192);
10 | expect(options.fallback).toEqual(getFileLoaderPath());
11 | expect(options.name).toEqual('[name]-[hash].[ext]');
12 | });
13 |
14 | it('allows overwriting the inlineImageLimit option', () => {
15 | const options = getUrlLoaderOptions(getConfig({ inlineImageLimit: 10 }), false);
16 |
17 | expect(options.limit).toEqual(10);
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/__tests__/loaders/webp-loader.test.js:
--------------------------------------------------------------------------------
1 | const { getConfig } = require('../../lib/config');
2 | const {
3 | getWebpLoaderOptions,
4 | applyWebpLoader,
5 | getWebpResourceQuery,
6 | } = require('../../lib/loaders/webp-loader');
7 |
8 | describe('next-optimized-images/loaders/webp-loader', () => {
9 | it('uses the default config', () => {
10 | const options = getWebpLoaderOptions(getConfig({}), false);
11 |
12 | expect(options).toEqual({});
13 | });
14 |
15 | it('allows overwriting the default options', () => {
16 | const options = getWebpLoaderOptions(getConfig({ webp: { a: 1 } }), false);
17 |
18 | expect(options).toEqual({ a: 1 });
19 | });
20 |
21 | it('adds rules to the webpack config', () => {
22 | const webpackConfig = { module: { rules: [] } };
23 | applyWebpLoader(webpackConfig, getConfig({}), true, false, {});
24 |
25 | const rule = webpackConfig.module.rules[0];
26 |
27 | expect(rule.test).toBeInstanceOf(RegExp);
28 | expect(rule.test.test('.webp')).toEqual(true);
29 | expect(rule.oneOf).toHaveLength(13);
30 | });
31 |
32 | it('generates a resource query for webp conversion', () => {
33 | const config = getWebpResourceQuery(getConfig({}), false);
34 |
35 | expect(config.resourceQuery.test('img.jpg?webp')).toEqual(true);
36 | expect(config.use).toHaveLength(2);
37 | expect(config.use[0].loader).toEqual('url-loader');
38 | expect(config.use[1].loader).toEqual('webp-loader');
39 | });
40 | });
41 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | .next
2 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | # next-optimized-images Example
2 |
3 | This example showcases a basic setup of `next-optimized-images`.
4 |
5 | ## Setup
6 |
7 | 1. Run `npm install`
8 | 2. Run either `npm run build && npm start` to start it in the production mode, or `npm run dev` to start it in the development mode
9 | 3. Open the page in your browser: http://localhost:3000
10 |
--------------------------------------------------------------------------------
/example/next.config.js:
--------------------------------------------------------------------------------
1 | const withPlugins = require('next-compose-plugins');
2 | const optimizedImages = require('next-optimized-images');
3 |
4 | module.exports = withPlugins([
5 | optimizedImages,
6 | ]);
7 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "next-optimized-images-example",
3 | "scripts": {
4 | "dev": "next",
5 | "build": "next build",
6 | "start": "next start"
7 | },
8 | "dependencies": {
9 | "imagemin-mozjpeg": "^8.0.0",
10 | "imagemin-optipng": "^6.0.0",
11 | "next": "8.1.0",
12 | "next-compose-plugins": "^2.1.1",
13 | "next-optimized-images": "^2.4.1",
14 | "react": "16.8.6",
15 | "react-dom": "16.8.6",
16 | "webp-loader": "^0.4.0"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/example/pages/images/ben-den-engelsen-unsplash.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cyrilwanner/next-optimized-images/fe31e5c77008a88ce9ffe76d504229562ad8a549/example/pages/images/ben-den-engelsen-unsplash.jpg
--------------------------------------------------------------------------------
/example/pages/images/spencer-davis-unsplash.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cyrilwanner/next-optimized-images/fe31e5c77008a88ce9ffe76d504229562ad8a549/example/pages/images/spencer-davis-unsplash.jpg
--------------------------------------------------------------------------------
/example/pages/images/stage-7-photography-unsplash.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cyrilwanner/next-optimized-images/fe31e5c77008a88ce9ffe76d504229562ad8a549/example/pages/images/stage-7-photography-unsplash.jpg
--------------------------------------------------------------------------------
/example/pages/images/victor-rodriguez-unsplash.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cyrilwanner/next-optimized-images/fe31e5c77008a88ce9ffe76d504229562ad8a549/example/pages/images/victor-rodriguez-unsplash.jpg
--------------------------------------------------------------------------------
/example/pages/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default () => (
4 |
5 |
6 |
Including an image
7 |
8 |
Big images get normally referenced in the src
attribute
9 | {/* Include an image with the normal require function, import does also work */}
10 |
11 |
12 |
Smaller images get automatically inlined for better performance (can be disabled )
13 | {/* This is a small image, so it will automatically get inlined */}
14 |
15 |
16 |
17 |
18 |
Resource query params
19 |
20 |
Disable optimization for a single image
21 | {/* Do not optimize this image and use the original one */}
22 |
23 |
24 |
Convert an image to WebP
25 | {/* Convert this image to WebP */}
26 |
27 |
28 |
To also support browsers without WebP, you can use a fallback in the picture
tag.
29 |
30 |
More query params
31 |
Check the documentation for a full list of all available query params .
32 |
33 |
34 |
53 |
54 | );
55 |
--------------------------------------------------------------------------------
/lib/config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Enriches the next.js configuration object with default config values for
3 | * next-optimized-iamges and returns it
4 | *
5 | * @param {object} nextConfig - next.js configuration object
6 | * @returns {object} enriched config
7 | */
8 | const getConfig = nextConfig => ({
9 | optimizeImages: true,
10 | optimizeImagesInDev: false,
11 | handleImages: ['jpeg', 'png', 'svg', 'webp', 'gif'],
12 | imagesFolder: 'images',
13 | imagesName: '[name]-[hash].[ext]',
14 | removeOriginalExtension: false,
15 | inlineImageLimit: 8192,
16 | defaultImageLoader: 'img-loader',
17 | mozjpeg: {},
18 | optipng: {},
19 | pngquant: {},
20 | gifsicle: {
21 | interlaced: true,
22 | optimizationLevel: 3,
23 | },
24 | svgo: {},
25 | svgSpriteLoader: {
26 | symbolId: '[name]-[hash:8]',
27 | },
28 | webp: {},
29 | ...nextConfig,
30 | });
31 |
32 | module.exports = {
33 | getConfig,
34 | };
35 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | const { getConfig } = require('./config');
2 | const { detectLoaders, getNumOptimizationLoadersInstalled, appendLoaders } = require('./loaders');
3 | const { showWarning } = require('./migrater');
4 |
5 | /**
6 | * Configure webpack and next.js to handle and optimize images with this plugin.
7 | *
8 | * @param {object} nextConfig - configuration, see the readme for possible values
9 | * @param {object} nextComposePlugins - additional information when loaded with next-compose-plugins
10 | * @returns {object}
11 | */
12 | const withOptimizedImages = (nextConfig = {}, nextComposePlugins = {}) => {
13 | const { optimizeImages, optimizeImagesInDev, overwriteImageLoaderPaths } = getConfig(nextConfig);
14 |
15 | return Object.assign({}, nextConfig, {
16 | webpack(config, options) {
17 | if (!options.defaultLoaders) {
18 | throw new Error(
19 | 'This plugin is not compatible with Next.js versions below 5.0.0 https://err.sh/next-plugins/upgrade',
20 | );
21 | }
22 |
23 | const { dev, isServer } = options;
24 | let enrichedConfig = config;
25 |
26 | // detect all installed loaders
27 | const detectedLoaders = detectLoaders(overwriteImageLoaderPaths);
28 |
29 | // check if it should optimize images in the current step
30 | const optimizeInCurrentStep = nextComposePlugins && typeof nextComposePlugins.phase === 'string'
31 | ? (
32 | (nextComposePlugins.phase === 'phase-production-build' && optimizeImages)
33 | || (nextComposePlugins.phase === 'phase-export' && optimizeImages)
34 | || (nextComposePlugins.phase === 'phase-development-server' && optimizeImagesInDev)
35 | )
36 | : ((!dev && optimizeImages) || (dev && optimizeImagesInDev));
37 |
38 | // show a warning if images should get optimized but no loader is installed
39 | if (optimizeImages && getNumOptimizationLoadersInstalled(detectedLoaders) === 0 && isServer) {
40 | showWarning();
41 | }
42 |
43 | // remove (unoptimized) builtin image processing introduced in next.js 9.2
44 | if (enrichedConfig.module.rules) {
45 | enrichedConfig.module.rules.forEach((rule) => {
46 | if (rule.oneOf) {
47 | rule.oneOf.forEach((subRule) => {
48 | if (
49 | subRule.issuer && !subRule.test && !subRule.include && subRule.exclude
50 | && subRule.use && subRule.use.options && subRule.use.options.name
51 | ) {
52 | if ((String(subRule.issuer.test) === '/\\.(css|scss|sass)$/' || String(subRule.issuer) === '/\\.(css|scss|sass)$/') && subRule.use.options.name.startsWith('static/media/')) {
53 | subRule.exclude.push(/\.(jpg|jpeg|png|svg|webp|gif|ico)$/);
54 | }
55 | }
56 | });
57 | }
58 | });
59 | }
60 |
61 | // append loaders
62 | enrichedConfig = appendLoaders(enrichedConfig, getConfig(nextConfig), detectedLoaders,
63 | isServer, optimizeInCurrentStep);
64 |
65 | if (typeof nextConfig.webpack === 'function') {
66 | return nextConfig.webpack(enrichedConfig, options);
67 | }
68 |
69 | return enrichedConfig;
70 | },
71 | });
72 | };
73 |
74 | module.exports = withOptimizedImages;
75 |
--------------------------------------------------------------------------------
/lib/loaders/file-loader.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs');
3 |
4 | /**
5 | * Build options for the webpack file loader
6 | *
7 | * @param {object} nextConfig - next.js configuration
8 | * @param {boolean} isServer - if the build is for the server
9 | * @returns {object}
10 | */
11 | const getFileLoaderOptions = ({
12 | assetPrefix,
13 | imagesPublicPath,
14 | imagesOutputPath,
15 | imagesFolder,
16 | imagesName,
17 | }, isServer) => {
18 | let publicPath = `/_next/static/${imagesFolder}/`;
19 |
20 | if (imagesPublicPath) {
21 | publicPath = imagesPublicPath;
22 | } else if (assetPrefix) {
23 | publicPath = `${assetPrefix}${assetPrefix.endsWith('/') ? '' : '/'}_next/static/${imagesFolder}/`;
24 | }
25 |
26 | return {
27 | publicPath,
28 | outputPath: imagesOutputPath || `${isServer ? '../' : ''}static/${imagesFolder}/`,
29 | name: imagesName,
30 | };
31 | };
32 |
33 | /**
34 | * Get the file-loader path
35 | *
36 | * @returns {string}
37 | */
38 | const getFileLoaderPath = () => {
39 | const absolutePath = path.resolve(__dirname, '..', '..', 'node_modules', 'file-loader', 'dist', 'cjs.js');
40 |
41 | if (fs.existsSync(absolutePath)) {
42 | return absolutePath;
43 | }
44 |
45 | return 'file-loader';
46 | };
47 |
48 | /**
49 | * Apply the file loader to the webpack configuration
50 | *
51 | * @param {object} webpackConfig - webpack configuration
52 | * @param {object} nextConfig - next.js configuration
53 | * @param {boolean} isServer - if the build is for the server
54 | * @param {RegExp} fileRegex - regex for files to handle
55 | * @returns {object}
56 | */
57 | const applyFileLoader = (webpackConfig, nextConfig, isServer, fileRegex) => {
58 | webpackConfig.module.rules.push({
59 | test: fileRegex,
60 | oneOf: [
61 | {
62 | use: {
63 | loader: getFileLoaderPath(),
64 | options: getFileLoaderOptions(nextConfig, isServer),
65 | },
66 | },
67 | ],
68 | });
69 |
70 | return webpackConfig;
71 | };
72 |
73 | module.exports = {
74 | getFileLoaderOptions,
75 | getFileLoaderPath,
76 | applyFileLoader,
77 | };
78 |
--------------------------------------------------------------------------------
/lib/loaders/image-trace-loader.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Build options for the webpack image trace loader
3 | *
4 | * @param {object} nextConfig - next.js configuration
5 | * @returns {object}
6 | */
7 | const getImageTraceLoaderOptions = ({
8 | imageTrace,
9 | }) => ({
10 | ...(imageTrace || {}),
11 | });
12 |
13 | module.exports = {
14 | getImageTraceLoaderOptions,
15 | };
16 |
--------------------------------------------------------------------------------
/lib/loaders/img-loader.js:
--------------------------------------------------------------------------------
1 | const { getResourceQueries } = require('../resource-queries');
2 | const { getWebpResourceQuery } = require('./webp-loader');
3 | const { getUrlLoaderOptions } = require('./url-loader');
4 | const { getSvgSpriteLoaderResourceQuery } = require('./svg-sprite-loader');
5 |
6 | /**
7 | * Requires an imagemin plugin and configures it
8 | *
9 | * @param {string} plugin - plugin name
10 | * @param {*} nextConfig - next.js configuration
11 | * @return {function}
12 | */
13 | const requireImageminPlugin = (plugin, nextConfig) => {
14 | let moduleName = plugin;
15 |
16 | if (nextConfig.overwriteImageLoaderPaths) {
17 | moduleName = require.resolve(plugin, { paths: [nextConfig.overwriteImageLoaderPaths] });
18 | }
19 |
20 | /* eslint global-require: "off", import/no-dynamic-require: "off" */
21 | return require(moduleName)(nextConfig[plugin.replace('imagemin-', '')] || {});
22 | };
23 |
24 | /**
25 | * Build options for the img loader
26 | *
27 | * @param {object} nextConfig - next.js configuration
28 | * @param {object} detectedLoaders - detected loaders
29 | * @param {boolean} optimize - if images should get optimized
30 | * @return {object}
31 | */
32 | const getImgLoaderOptions = (nextConfig, detectedLoaders, optimize) => {
33 | if (!optimize) {
34 | return {
35 | plugins: [],
36 | };
37 | }
38 |
39 | return {
40 | plugins: [
41 | detectedLoaders.jpeg
42 | ? requireImageminPlugin(detectedLoaders.jpeg, nextConfig)
43 | : undefined,
44 |
45 | detectedLoaders.png
46 | ? requireImageminPlugin(detectedLoaders.png, nextConfig)
47 | : undefined,
48 |
49 | detectedLoaders.svg
50 | ? requireImageminPlugin(detectedLoaders.svg, nextConfig)
51 | : undefined,
52 |
53 | detectedLoaders.gif
54 | ? requireImageminPlugin(detectedLoaders.gif, nextConfig)
55 | : undefined,
56 | ].filter(Boolean),
57 | };
58 | };
59 |
60 | /**
61 | * Build the regex for all handled image types
62 | *
63 | * @param {object} handledImageTypes - handled image types
64 | * @return {RegExp}
65 | */
66 | const getHandledFilesRegex = (handledImageTypes) => {
67 | const handledFiles = [
68 | handledImageTypes.jpeg ? 'jpe?g' : null,
69 | handledImageTypes.png ? 'png' : null,
70 | handledImageTypes.svg ? 'svg' : null,
71 | handledImageTypes.gif ? 'gif' : null,
72 | ];
73 |
74 | return new RegExp(`\\.(${handledFiles.filter(Boolean).join('|')})$`, 'i');
75 | };
76 |
77 | /**
78 | * Apply the img loader to the webpack configuration
79 | *
80 | * @param {object} webpackConfig - webpack configuration
81 | * @param {object} nextConfig - next.js configuration
82 | * @param {boolean} optimize - if images should get optimized
83 | * @param {boolean} isServer - if the build is for the server
84 | * @param {object} detectedLoaders - detected loaders
85 | * @param {object} handledImageTypes - detected image types
86 | * @returns {object}
87 | */
88 | const applyImgLoader = (
89 | webpackConfig,
90 | nextConfig,
91 | optimize,
92 | isServer,
93 | detectedLoaders,
94 | handledImageTypes,
95 | ) => {
96 | const imgLoaderOptions = getImgLoaderOptions(nextConfig, detectedLoaders, optimize);
97 |
98 | webpackConfig.module.rules.push({
99 | test: getHandledFilesRegex(handledImageTypes),
100 | oneOf: [
101 | // add all resource queries
102 | ...getResourceQueries(nextConfig, isServer, optimize ? 'img-loader' : null, imgLoaderOptions, detectedLoaders),
103 |
104 | // ?webp: convert an image to webp
105 | handledImageTypes.webp
106 | ? getWebpResourceQuery(nextConfig, isServer)
107 | : undefined,
108 |
109 | // ?sprite: add icon to sprite
110 | detectedLoaders.svgSprite
111 | ? getSvgSpriteLoaderResourceQuery(nextConfig, detectedLoaders, imgLoaderOptions, optimize)
112 | : undefined,
113 |
114 | // default behavior: inline if below the definied limit, external file if above
115 | {
116 | use: [
117 | {
118 | loader: 'url-loader',
119 | options: getUrlLoaderOptions(nextConfig, isServer),
120 | },
121 | {
122 | loader: 'img-loader',
123 | options: imgLoaderOptions,
124 | },
125 | ],
126 | },
127 | ].filter(Boolean),
128 | });
129 |
130 | return webpackConfig;
131 | };
132 |
133 | module.exports = {
134 | requireImageminPlugin,
135 | getImgLoaderOptions,
136 | getHandledFilesRegex,
137 | applyImgLoader,
138 | };
139 |
--------------------------------------------------------------------------------
/lib/loaders/index.js:
--------------------------------------------------------------------------------
1 | const { applyImgLoader } = require('./img-loader');
2 | const { applyWebpLoader } = require('./webp-loader');
3 | const { applyResponsiveLoader } = require('./responsive-loader');
4 | const { applyFileLoader } = require('./file-loader');
5 |
6 | /**
7 | * Checks if a node module is installed in the current context
8 | *
9 | * @param {string} name - module name
10 | * @param {string} resolvePath - optional resolve path
11 | * @returns {boolean}
12 | */
13 | const isModuleInstalled = (name, resolvePath) => {
14 | try {
15 | require.resolve(name, resolvePath ? { paths: [resolvePath] } : undefined);
16 |
17 | return true;
18 | } catch (e) {
19 | return false;
20 | }
21 | };
22 |
23 | /**
24 | * Detects all currently installed image optimization loaders
25 | *
26 | * @param {string} resolvePath - optional resolve path
27 | * @returns {object}
28 | */
29 | const detectLoaders = (resolvePath) => {
30 | const jpeg = isModuleInstalled('imagemin-mozjpeg', resolvePath) ? 'imagemin-mozjpeg' : false;
31 | const gif = isModuleInstalled('imagemin-gifsicle', resolvePath) ? 'imagemin-gifsicle' : false;
32 | const svg = isModuleInstalled('imagemin-svgo', resolvePath) ? 'imagemin-svgo' : false;
33 | const svgSprite = isModuleInstalled('svg-sprite-loader', resolvePath) ? 'svg-sprite-loader' : false;
34 | const webp = isModuleInstalled('webp-loader', resolvePath) ? 'webp-loader' : false;
35 | const lqip = isModuleInstalled('lqip-loader', resolvePath) ? 'lqip-loader' : false;
36 |
37 | let png = false;
38 | let responsive = false;
39 | let responsiveAdapter = false;
40 |
41 | if (isModuleInstalled('imagemin-optipng', resolvePath)) {
42 | png = 'imagemin-optipng';
43 | } else if (isModuleInstalled('imagemin-pngquant', resolvePath)) {
44 | png = 'imagemin-pngquant';
45 | }
46 |
47 | if (isModuleInstalled('responsive-loader', resolvePath)) {
48 | responsive = require.resolve('responsive-loader', resolvePath ? { paths: [resolvePath] } : undefined).replace(/(\/|\\)lib(\/|\\)index.js$/g, '');
49 |
50 | if (isModuleInstalled('sharp', resolvePath)) {
51 | responsiveAdapter = 'sharp';
52 | } else if (isModuleInstalled('jimp', resolvePath)) {
53 | responsiveAdapter = 'jimp';
54 | }
55 | }
56 |
57 | return {
58 | jpeg,
59 | gif,
60 | svg,
61 | svgSprite,
62 | webp,
63 | png,
64 | lqip,
65 | responsive,
66 | responsiveAdapter,
67 | };
68 | };
69 |
70 | /**
71 | * Checks which image types should by handled by this plugin
72 | *
73 | * @param {object} nextConfig - next.js configuration object
74 | * @returns {object}
75 | */
76 | const getHandledImageTypes = (nextConfig) => {
77 | const { handleImages } = nextConfig;
78 |
79 | return {
80 | jpeg: handleImages.indexOf('jpeg') >= 0 || handleImages.indexOf('jpg') >= 0,
81 | png: handleImages.indexOf('png') >= 0,
82 | svg: handleImages.indexOf('svg') >= 0,
83 | webp: handleImages.indexOf('webp') >= 0,
84 | gif: handleImages.indexOf('gif') >= 0,
85 | ico: handleImages.indexOf('ico') >= 0,
86 | };
87 | };
88 |
89 | /**
90 | * Returns the number of image optimization loaders installed
91 | *
92 | * @param {object} loaders - detected loaders
93 | * @returns {number}
94 | */
95 | const getNumOptimizationLoadersInstalled = loaders => Object.values(loaders)
96 | .filter(loader => loader && (
97 | loader.startsWith('imagemin-')
98 | || loader.startsWith('webp-')
99 | || loader.startsWith('lqip-')
100 | )).length;
101 |
102 | /**
103 | * Appends all loaders to the webpack configuration
104 | *
105 | * @param {object} webpackConfig - webpack configuration
106 | * @param {object} nextConfig - next.js configuration
107 | * @param {object} detectedLoaders - detected loaders
108 | * @param {boolean} isServer - if the build is for the server
109 | * @param {boolean} optimize - if images should get optimized or just copied
110 | * @returns {object}
111 | */
112 | const appendLoaders = (
113 | webpackConfig,
114 | nextConfig,
115 | detectedLoaders,
116 | isServer,
117 | optimize,
118 | ) => {
119 | let config = webpackConfig;
120 | const handledImageTypes = getHandledImageTypes(nextConfig);
121 | let imgLoaderHandledTypes = handledImageTypes;
122 |
123 | // check if responsive-loader should be the default loader and apply it if so
124 | if (nextConfig.defaultImageLoader && nextConfig.defaultImageLoader === 'responsive-loader') {
125 | // img-loader no longer has to handle jpeg and png images
126 | imgLoaderHandledTypes = { ...imgLoaderHandledTypes, jpeg: false, png: false };
127 |
128 | config = applyResponsiveLoader(webpackConfig, nextConfig, isServer, detectLoaders);
129 | }
130 |
131 | // apply img loader
132 | const shouldApplyImgLoader = imgLoaderHandledTypes.jpeg || imgLoaderHandledTypes.png
133 | || imgLoaderHandledTypes.gif || imgLoaderHandledTypes.svg;
134 |
135 | if ((detectedLoaders.jpeg || detectedLoaders.png || detectedLoaders.gif || detectedLoaders.svg)
136 | && shouldApplyImgLoader) {
137 | config = applyImgLoader(webpackConfig, nextConfig, optimize, isServer,
138 | detectedLoaders, imgLoaderHandledTypes);
139 | } else if (shouldApplyImgLoader) {
140 | config = applyImgLoader(webpackConfig, nextConfig, false, isServer,
141 | detectedLoaders, imgLoaderHandledTypes);
142 | }
143 |
144 | // apply webp loader
145 | if (detectedLoaders.webp && handledImageTypes.webp) {
146 | config = applyWebpLoader(webpackConfig, nextConfig, optimize, isServer, detectLoaders);
147 | } else if (handledImageTypes.webp) {
148 | config = applyWebpLoader(webpackConfig, nextConfig, false, isServer, detectLoaders);
149 | }
150 |
151 | // apply file loader for non optimizable image types
152 | if (handledImageTypes.ico) {
153 | config = applyFileLoader(webpackConfig, nextConfig, isServer, /\.(ico)$/i);
154 | }
155 |
156 | return config;
157 | };
158 |
159 | module.exports = {
160 | isModuleInstalled,
161 | detectLoaders,
162 | getHandledImageTypes,
163 | getNumOptimizationLoadersInstalled,
164 | appendLoaders,
165 | };
166 |
--------------------------------------------------------------------------------
/lib/loaders/lqip-loader/colors-export-loader.js:
--------------------------------------------------------------------------------
1 | module.exports = (content) => { // eslint-disable-line arrow-body-style
2 | return `${content.toString('utf-8').replace('module.exports', 'var lqip')} module.exports = lqip.palette; module.exports = Object.assign(module.exports, lqip);`;
3 | };
4 |
--------------------------------------------------------------------------------
/lib/loaders/lqip-loader/index.js:
--------------------------------------------------------------------------------
1 | const { getFileLoaderOptions } = require('../file-loader');
2 |
3 | /**
4 | * Build options for the webpack lqip loader
5 | *
6 | * @param {object} nextConfig - next.js configuration
7 | * @param {boolean} isServer - if the build is for the server
8 | * @returns {object}
9 | */
10 | const getLqipLoaderOptions = (nextConfig, isServer) => ({
11 | ...getFileLoaderOptions(nextConfig, isServer),
12 | ...(nextConfig.lqip || {}),
13 | });
14 |
15 | module.exports = {
16 | getLqipLoaderOptions,
17 | };
18 |
--------------------------------------------------------------------------------
/lib/loaders/lqip-loader/picture-export-loader.js:
--------------------------------------------------------------------------------
1 | module.exports = (content) => { // eslint-disable-line arrow-body-style
2 | return `${content.toString('utf-8').replace('module.exports', 'var lqip')} module.exports = lqip.preSrc; module.exports = Object.assign(module.exports, lqip);`;
3 | };
4 |
--------------------------------------------------------------------------------
/lib/loaders/raw-loader/export-loader.js:
--------------------------------------------------------------------------------
1 | module.exports = (content) => { // eslint-disable-line arrow-body-style
2 | return `${content.toString('utf-8').replace('export default', 'var raw =')}; module.exports = raw; exports.default = raw;`;
3 | };
4 |
--------------------------------------------------------------------------------
/lib/loaders/responsive-loader.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const { getFileLoaderOptions } = require('./file-loader');
3 |
4 | /**
5 | * Build options for the webpack responsive loader
6 | *
7 | * @param {object} nextConfig - next.js configuration
8 | * @param {object} detectedLoaders - all detected and installed loaders
9 | * @returns {object}
10 | */
11 | const getResponsiveLoaderOptions = ({
12 | responsive,
13 | ...nextConfig
14 | }, isServer, detectedLoaders) => {
15 | let adapter = responsive ? responsive.adapter : undefined;
16 |
17 | if (!adapter && detectedLoaders.responsiveAdapter === 'sharp') {
18 | adapter = require(`${detectedLoaders.responsive}${path.sep}sharp`); // eslint-disable-line
19 | }
20 |
21 | return {
22 | ...getFileLoaderOptions(nextConfig, isServer),
23 | name: '[name]-[width]-[hash].[ext]',
24 | ...(responsive || {}),
25 | adapter,
26 | };
27 | };
28 |
29 | /**
30 | * Apply the responsive loader to the webpack configuration
31 | *
32 | * @param {object} webpackConfig - webpack configuration
33 | * @param {object} nextConfig - next.js configuration
34 | * @param {boolean} isServer - if the build is for the server
35 | * @param {object} detectedLoaders - all detected and installed loaders
36 | * @returns {object}
37 | */
38 | const applyResponsiveLoader = (webpackConfig, nextConfig, isServer, detectedLoaders) => {
39 | webpackConfig.module.rules.push({
40 | test: /\.(jpe?g|png)$/i,
41 | oneOf: [
42 | {
43 | use: {
44 | loader: 'responsive-loader',
45 | options: getResponsiveLoaderOptions(nextConfig, isServer, detectedLoaders),
46 | },
47 | },
48 | ],
49 | });
50 |
51 | return webpackConfig;
52 | };
53 |
54 | module.exports = {
55 | getResponsiveLoaderOptions,
56 | applyResponsiveLoader,
57 | };
58 |
--------------------------------------------------------------------------------
/lib/loaders/svg-sprite-loader/component.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | var React = require('react');
3 | var SpriteSymbol = require('$$symbolRequest$$');
4 | var sprite = require('$$spriteRequest$$');
5 |
6 | var symbol = new SpriteSymbol($$stringifiedSymbol$$);
7 | sprite.add(symbol);
8 |
9 | var SvgSpriteIcon = function SvgSpriteIcon(props) {
10 | return React.createElement(
11 | 'svg',
12 | Object.assign({
13 | viewBox: symbol.viewBox
14 | }, props),
15 | React.createElement(
16 | 'use',
17 | {
18 | xlinkHref: '#' + symbol.id
19 | }
20 | )
21 | );
22 | };
23 |
24 | SvgSpriteIcon.viewBox = symbol.viewBox;
25 | SvgSpriteIcon.id = symbol.id;
26 | SvgSpriteIcon.content = symbol.content;
27 | SvgSpriteIcon.url = symbol.url;
28 | SvgSpriteIcon.toString = symbol.toString;
29 |
30 | module.exports = SvgSpriteIcon;
31 | module.exports.default = SvgSpriteIcon;
32 |
--------------------------------------------------------------------------------
/lib/loaders/svg-sprite-loader/index.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | /**
4 | * Returns the resource query definition for an svg sprite image
5 | *
6 | * @param {object} nextConfig - next.js configuration
7 | * @param {object} detectedLoaders - detected loaders
8 | * @param {object} imgLoaderOptions - img loader options
9 | * @param {boolean} optimize - if the svg image should get optimized
10 | * @returns {object}
11 | */
12 | const getSvgSpriteLoaderResourceQuery = (
13 | nextConfig,
14 | detectedLoaders,
15 | imgLoaderOptions,
16 | optimize,
17 | ) => ({
18 | resourceQuery: /sprite/,
19 | use: [
20 | {
21 | loader: 'svg-sprite-loader',
22 | options: {
23 | runtimeGenerator: require.resolve(path.resolve(__dirname, 'svg-runtime-generator.js')),
24 | ...(nextConfig.svgSpriteLoader || {}),
25 | },
26 | },
27 | ].concat(detectedLoaders.svg && optimize ? [
28 | {
29 | loader: 'img-loader',
30 | options: imgLoaderOptions,
31 | },
32 | ] : []),
33 | });
34 |
35 | module.exports = {
36 | getSvgSpriteLoaderResourceQuery,
37 | };
38 |
--------------------------------------------------------------------------------
/lib/loaders/svg-sprite-loader/svg-runtime-generator.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const { stringifyRequest } = require('loader-utils');
4 | const { stringifySymbol } = require('svg-sprite-loader/lib/utils');
5 |
6 | /**
7 | * Defines the runtime generator for the svg sprite loader
8 | *
9 | * @param {object} config - runtime generator context
10 | * @returns {string}
11 | */
12 | const runtimeGenerator = ({ symbol, config, context }) => {
13 | const { spriteModule, symbolModule } = config;
14 |
15 | const spriteRequest = stringifyRequest({ context }, spriteModule);
16 | const symbolRequest = stringifyRequest({ context }, symbolModule);
17 |
18 | const component = fs.readFileSync(path.resolve(__dirname, 'component.js')).toString();
19 |
20 | return component
21 | .replace('\'$$symbolRequest$$\'', symbolRequest)
22 | .replace('\'$$spriteRequest$$\'', spriteRequest)
23 | .replace('$$stringifiedSymbol$$', stringifySymbol(symbol));
24 | };
25 |
26 | module.exports = runtimeGenerator;
27 |
--------------------------------------------------------------------------------
/lib/loaders/url-loader.js:
--------------------------------------------------------------------------------
1 | const { getFileLoaderOptions, getFileLoaderPath } = require('./file-loader');
2 |
3 | /**
4 | * Build options for the webpack url loader
5 | *
6 | * @param {object} nextConfig - next.js configuration
7 | * @param {boolean} isServer - if the build is for the server
8 | * @returns {object}
9 | */
10 | const getUrlLoaderOptions = ({
11 | inlineImageLimit,
12 | ...config
13 | }, isServer) => ({
14 | ...getFileLoaderOptions(config, isServer),
15 | limit: inlineImageLimit,
16 | fallback: getFileLoaderPath(),
17 | });
18 |
19 | module.exports = {
20 | getUrlLoaderOptions,
21 | };
22 |
--------------------------------------------------------------------------------
/lib/loaders/webp-loader.js:
--------------------------------------------------------------------------------
1 | const { getUrlLoaderOptions } = require('./url-loader');
2 | const { getResourceQueries } = require('../resource-queries');
3 |
4 | /**
5 | * Build options for the webp loader
6 | *
7 | * @param {object} nextConfig - next.js configuration
8 | * @returns {object}
9 | */
10 | const getWebpLoaderOptions = ({ webp }) => webp || {};
11 |
12 | /**
13 | * Apply the webp loader to the webpack configuration
14 | *
15 | * @param {object} webpackConfig - webpack configuration
16 | * @param {object} nextConfig - next.js configuration
17 | * @param {boolean} optimize - if images should get optimized
18 | * @param {boolean} isServer - if the build is for the server
19 | * @param {object} detectedLoaders - all detected and installed loaders
20 | * @returns {object}
21 | */
22 | const applyWebpLoader = (webpackConfig, nextConfig, optimize, isServer, detectLoaders) => {
23 | const webpLoaders = [
24 | {
25 | loader: 'url-loader',
26 | options: getUrlLoaderOptions(nextConfig, isServer),
27 | },
28 | ];
29 |
30 | if (optimize) {
31 | webpLoaders.push({
32 | loader: 'webp-loader',
33 | options: getWebpLoaderOptions(nextConfig),
34 | });
35 | }
36 |
37 | webpackConfig.module.rules.push({
38 | test: /\.webp$/i,
39 | oneOf: [
40 | // add all resource queries
41 | ...getResourceQueries(nextConfig, isServer, !optimize ? null : 'webp-loader', getWebpLoaderOptions(nextConfig), detectLoaders),
42 |
43 | // default behavior: inline if below the definied limit, external file if above
44 | {
45 | use: webpLoaders,
46 | },
47 | ],
48 | });
49 |
50 | return webpackConfig;
51 | };
52 |
53 | /**
54 | * Returns the resource query definition for converting a jpeg/png image to webp
55 | *
56 | * @param {object} nextConfig - next.js configuration
57 | * @param {boolean} isServer - if the build is for the server
58 | * @returns {object}
59 | */
60 | const getWebpResourceQuery = (nextConfig, isServer) => {
61 | const urlLoaderOptions = getUrlLoaderOptions(nextConfig, isServer);
62 | const imageName = urlLoaderOptions.name.indexOf('[ext]') >= 0
63 | ? urlLoaderOptions.name.replace('[ext]', nextConfig.removeOriginalExtension ? 'webp' : '[ext].webp')
64 | : `${urlLoaderOptions.name}.webp`;
65 |
66 | return {
67 | resourceQuery: /webp/,
68 | use: [
69 | {
70 | loader: 'url-loader',
71 | options: Object.assign(
72 | {},
73 | urlLoaderOptions,
74 | {
75 | name: imageName,
76 | mimetype: 'image/webp',
77 | },
78 | ),
79 | },
80 | {
81 | loader: 'webp-loader',
82 | options: getWebpLoaderOptions(nextConfig),
83 | },
84 | ],
85 | };
86 | };
87 |
88 | module.exports = {
89 | getWebpLoaderOptions,
90 | applyWebpLoader,
91 | getWebpResourceQuery,
92 | };
93 |
--------------------------------------------------------------------------------
/lib/migrater.js:
--------------------------------------------------------------------------------
1 | const chalk = require('chalk');
2 | const figures = require('figures');
3 |
4 | const prefix = `${chalk.gray('next-optimized-images')} ${chalk.red(figures.pointer)}`;
5 |
6 | /**
7 | * Output a warning when images should get optimized (prod build) but no optimization
8 | * package is installed.
9 | */
10 | const showWarning = () => console.log( // eslint-disable-line no-console
11 | `${prefix} ${chalk.red('WARNING!')}
12 | ${prefix} ${chalk.red('No package found which can optimize images.')}
13 | ${prefix} Starting from version ${chalk.cyan('2')} of ${chalk.cyan('next-optimized-images')}, all optimization is optional and you can choose which ones you want to use.
14 | ${prefix} For help during the setup and installation, please read ${chalk.underline('https://github.com/cyrilwanner/next-optimized-images#optimization-packages')}
15 |
16 | ${prefix} If you recently ${chalk.cyan('updated from v1 to v2')}, please read ${chalk.underline('https://github.com/cyrilwanner/next-optimized-images/blob/master/UPGRADING.md')}
17 | ${prefix} If this is on purpose and you don't want this plugin to optimize the images, set the option ${chalk.cyan('`optimizeImages: false`')} to hide this warning.
18 | `,
19 | );
20 |
21 | module.exports = {
22 | showWarning,
23 | };
24 |
--------------------------------------------------------------------------------
/lib/resource-queries.js:
--------------------------------------------------------------------------------
1 | const { getUrlLoaderOptions } = require('./loaders/url-loader');
2 | const { getFileLoaderOptions, getFileLoaderPath } = require('./loaders/file-loader');
3 | const { getLqipLoaderOptions } = require('./loaders/lqip-loader');
4 | const { getResponsiveLoaderOptions } = require('./loaders/responsive-loader');
5 | const { getImageTraceLoaderOptions } = require('./loaders/image-trace-loader');
6 |
7 | /**
8 | * Configure the common resource queries
9 | */
10 | const queries = [
11 | // ?url: force a file url/reference, never use inlining
12 | {
13 | test: 'url',
14 | loaders: [getFileLoaderPath()],
15 | optimize: true,
16 | combinations: ['original'],
17 | },
18 |
19 | // ?inline: force inlining an image regardless of the defined limit
20 | {
21 | test: 'inline',
22 | loaders: ['url-loader'],
23 | options: [{
24 | limit: undefined,
25 | }],
26 | optimize: true,
27 | combinations: ['original'],
28 | },
29 |
30 | // ?include: include the image directly, no data uri or external file
31 | {
32 | test: 'include',
33 | loaders: [
34 | require.resolve('./loaders/raw-loader/export-loader.js'),
35 | 'raw-loader',
36 | ],
37 | optimize: true,
38 | combinations: ['original'],
39 | },
40 |
41 | // ?original: use the original image and don't optimize it
42 | {
43 | test: 'original',
44 | loaders: ['url-loader'],
45 | optimize: false,
46 | },
47 |
48 | // ?lqip: low quality image placeholder
49 | {
50 | test: 'lqip(&|$)',
51 | loaders: [
52 | require.resolve('./loaders/lqip-loader/picture-export-loader.js'),
53 | 'lqip-loader',
54 | 'url-loader',
55 | ],
56 | optimize: false,
57 | },
58 |
59 | // ?lqip: low quality image placeholder
60 | {
61 | test: 'lqip-colors',
62 | loaders: [
63 | require.resolve('./loaders/lqip-loader/colors-export-loader.js'),
64 | 'lqip-loader',
65 | 'url-loader',
66 | ],
67 | options: [{}, {
68 | base64: false,
69 | palette: true,
70 | }],
71 | optimize: false,
72 | },
73 |
74 | // ?resize: resize images
75 | {
76 | test: 'size',
77 | loaders: [
78 | 'responsive-loader',
79 | ],
80 | optimize: false,
81 | },
82 |
83 | // ?trace: generate svg image traces for placeholders
84 | {
85 | test: 'trace',
86 | loaders: [
87 | 'image-trace-loader',
88 | 'url-loader',
89 | ],
90 | optimize: true,
91 | combinations: ['original'],
92 | },
93 | ];
94 |
95 | /**
96 | * Add combinations
97 | */
98 | [].concat(queries).forEach((queryConfig) => {
99 | if (queryConfig.combinations) {
100 | queryConfig.combinations.forEach((combination) => {
101 | if (combination === 'original') {
102 | queries.unshift({
103 | ...queryConfig,
104 | test: `(${queryConfig.test}.*original|original.*${queryConfig.test})`,
105 | optimize: false,
106 | });
107 | }
108 | });
109 | }
110 | });
111 |
112 | /**
113 | * Returns all common resource queries for the given optimization loader
114 | *
115 | * @param {object} nextConfig - next.js configuration object
116 | * @param {boolean} isServer - if the current build is for a server
117 | * @param {string} optimizerLoaderName - name of the loader used to optimize the images
118 | * @param {object} optimizerLoaderOptions - config for the optimization loader
119 | * @returns {array}
120 | */
121 | const getResourceQueries = (
122 | nextConfig,
123 | isServer,
124 | optimizerLoaderName,
125 | optimizerLoaderOptions,
126 | detectLoaders,
127 | ) => {
128 | const loaderOptions = {
129 | 'url-loader': getUrlLoaderOptions(nextConfig, isServer),
130 | 'file-loader': getFileLoaderOptions(nextConfig, isServer),
131 | [getFileLoaderPath()]: getFileLoaderOptions(nextConfig, isServer),
132 | 'lqip-loader': getLqipLoaderOptions(nextConfig, isServer),
133 | 'responsive-loader': getResponsiveLoaderOptions(nextConfig, isServer, detectLoaders),
134 | 'image-trace-loader': getImageTraceLoaderOptions(nextConfig),
135 | };
136 |
137 | return queries.map((queryConfig) => {
138 | const loaders = [];
139 |
140 | queryConfig.loaders.forEach((loader, index) => {
141 | const loaderConfig = {
142 | loader,
143 | };
144 |
145 | if (loaderOptions[loader]) {
146 | loaderConfig.options = loaderOptions[loader];
147 | }
148 |
149 | if (queryConfig.options) {
150 | loaderConfig.options = {
151 | ...(loaderConfig.options || {}),
152 | ...(queryConfig.options[index] || {}),
153 | };
154 | }
155 |
156 | loaders.push(loaderConfig);
157 | });
158 |
159 | return {
160 | resourceQuery: new RegExp(queryConfig.test),
161 | use: loaders.concat(queryConfig.optimize && optimizerLoaderName !== null ? [
162 | {
163 | loader: optimizerLoaderName,
164 | options: optimizerLoaderOptions,
165 | },
166 | ] : []),
167 | };
168 | });
169 | };
170 |
171 | module.exports = {
172 | getResourceQueries,
173 | };
174 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "next-optimized-images",
3 | "version": "2.6.2",
4 | "description": "Automatically optimize images used in next.js projects (jpeg, png, gif, svg).",
5 | "main": "lib/index.js",
6 | "files": [
7 | "lib"
8 | ],
9 | "scripts": {
10 | "lint": "eslint lib",
11 | "lint:fix": "eslint --fix lib",
12 | "test": "jest --coverage",
13 | "test:watch": "jest --watch"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/cyrilwanner/next-optimized-images.git"
18 | },
19 | "keywords": [
20 | "nextjs",
21 | "next",
22 | "react",
23 | "plugin",
24 | "images",
25 | "optimize",
26 | "optimized-images"
27 | ],
28 | "author": "Cyril Wanner ",
29 | "license": "MIT",
30 | "bugs": {
31 | "url": "https://github.com/cyrilwanner/next-optimized-images/issues"
32 | },
33 | "homepage": "https://github.com/cyrilwanner/next-optimized-images#readme",
34 | "dependencies": {
35 | "chalk": "^2.4.2",
36 | "figures": "^3.0.0",
37 | "file-loader": "^3.0.1",
38 | "imagemin": "^6.1.0",
39 | "img-loader": "^3.0.1",
40 | "raw-loader": "^2.0.0",
41 | "url-loader": "^1.1.2"
42 | },
43 | "devDependencies": {
44 | "eslint": "^5.16.0",
45 | "eslint-config-airbnb-base": "^13.1.0",
46 | "eslint-plugin-import": "^2.17.2",
47 | "eslint-plugin-jest": "^22.5.1",
48 | "jest": "^24.7.1",
49 | "loader-utils": "^1.2.3",
50 | "npm-autoinstaller": "^1.3.1",
51 | "svg-sprite-loader": "^4.1.3",
52 | "webpack": "^4.30.0"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------