diff --git a/README.md b/README.md index 53d4d67..d2f5aff 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 🌅 next-optimized-images [![npm version](https://img.shields.io/npm/v/next-optimized-images.svg)](https://www.npmjs.com/package/next-optimized-images) [![license](https://img.shields.io/github/license/cyrilwanner/next-optimized-images.svg)](https://github.com/cyrilwanner/next-optimized-images/blob/master/LICENSE) [![dependencies](https://david-dm.org/cyrilwanner/next-optimized-images/status.svg)](https://david-dm.org/cyrilwanner/next-optimized-images) -Automatically optimize images used in [next.js](https://github.com/zeit/next.js) projects (`jpeg`, `png`, `svg` and `gif`). +Automatically optimize images used in [next.js](https://github.com/zeit/next.js) projects (`jpeg`, `png`, `svg`, `webp` and `gif`). Image sizes can often get reduced between 20-60%, but this is not the only thing `next-optimized-images` does: @@ -9,9 +9,9 @@ Image sizes can often get reduced between 20-60%, but this is not the only thing * Inlines small images to save HTTP requests and additional roundtrips * Adds a content hash to the file name so images can get cached on CDN level and in the browser for a long time * Same image urls over multiple builds for long time caching -* `jpeg`, `png`, `svg` and `gif` images are supported and enabled by default but can be particularly disabled +* `jpeg`, `png`, `svg`, `webp` and `gif` images are supported and enabled by default but can be particularly disabled * Provides [options](#query-params) to force inlining a single file or include the raw optimized image directly in your html (e.g. for svgs) -* Converts images to [webp if wanted](#webp) for an even smaller size +* Converts jpeg/png images to [`webp` if wanted](#webp) for an even smaller size ## Table of contents @@ -314,10 +314,14 @@ If you want svg images and icons to be handled but _not_ optimized, you can set Type: `object`
Default: `{}` -[imagemin-webp](https://github.com/imagemin/imagemin-webp) is used for converting images to webp. +[imagemin-webp](https://github.com/imagemin/imagemin-webp) is used for optimizing webp images and converting other formats to webp. You can specify the options for it here. The default options of `imagemin-webp` are used if you omit this option. +If you don't want next-optimized-images to handle webp images (because you have another one), you can set this value to `false`. + +If you want webp images to be handled but _not_ optimized, you can set this value to `null`. + ## Example The options specified here are the **default** values. @@ -351,6 +355,10 @@ module.exports = withPlugins([ svgo: { // enable/disable svgo plugins here }, + webp: { + preset: 'default', + quality: 75, + }, }], ]); ``` diff --git a/index.js b/index.js index 79b8748..fe03823 100644 --- a/index.js +++ b/index.js @@ -42,6 +42,58 @@ const getHandledFilesRegex = (mozjpeg, optipng, pngquant, gifsicle, svgo) => { return new RegExp(`\.(${handledFiles.join('|')})$`, 'i'); }; +/** + * Build loaders for resource queries. + * + * @param {string} imgLoaderName - name of the loader to use for images + * @param {object} imgLoaderOptions - options to pass to the image loader + * @param {object} urlLoaderOptions - options to pass to url-loader + * @returns {array} loaders for resource queries + */ +const getResourceQueryLoaders = (imgLoaderName, imgLoaderOptions, urlLoaderOptions) => { + const addImgLoader = (loaders) => { + if (imgLoaderOptions === false) { + return loaders; + } + + return loaders.concat([ + { + loader: imgLoaderName, + options: imgLoaderOptions, + }, + ]); + }; + + return [ + // ?include: include the image directly, no data uri or external file + { + resourceQuery: /include/, + use: addImgLoader([ + { + loader: 'raw-loader', + }, + ]), + }, + + // ?inline: force inlining an image regardless of the defined limit + { + resourceQuery: /inline/, + use: addImgLoader([ + { + loader: 'url-loader', + options: Object.assign( + {}, + urlLoaderOptions, + { + limit: undefined, + }, + ), + }, + ]), + }, + ]; +}; + /** * Configure webpack and next.js to handle and optimize images with this plugin. * @@ -98,44 +150,16 @@ const withOptimizedImages = (nextConfig) => { svgo: getOptimizerConfig(svgo), }; - // push the loaders to the webpack configuration of next.js + // build options for webp-loader + const webpLoaderOptions = getOptimizerConfig(webp); + + // push the loaders for jpg, png, svg and gif to the webpack configuration of next.js config.module.rules.push({ test: getHandledFilesRegex(mozjpeg, optipng, pngquant, gifsicle, svgo), oneOf: [ // ?include: include the image directly, no data uri or external file - { - resourceQuery: /include/, - use: [ - { - loader: 'raw-loader', - }, - { - loader: 'img-loader', - options: imgLoaderOptions, - }, - ], - }, - // ?inline: force inlining an image regardless of the defined limit - { - resourceQuery: /inline/, - use: [ - { - loader: 'url-loader', - options: Object.assign( - {}, - urlLoaderOptions, - { - limit: undefined, - }, - ), - }, - { - loader: 'img-loader', - options: imgLoaderOptions, - }, - ], - }, + ...getResourceQueryLoaders('img-loader', imgLoaderOptions, urlLoaderOptions), // ?webp: convert an image to webp { @@ -153,8 +177,8 @@ const withOptimizedImages = (nextConfig) => { ), }, { - loader: `webp-loader`, - options: getOptimizerConfig(webp), + loader: 'webp-loader', + options: webpLoaderOptions || {}, }, ], }, @@ -175,6 +199,37 @@ const withOptimizedImages = (nextConfig) => { ], }); + // push the loaders for webp to the webpack configuration of next.js + if (webp !== false) { + const webpLoaders = [ + { + loader: 'url-loader', + options: urlLoaderOptions, + }, + ]; + + if (webp !== null) { + webpLoaders.push({ + loader: 'webp-loader', + options: webpLoaderOptions, + }); + } + + config.module.rules.push({ + test: /\.webp$/i, + oneOf: [ + // ?include: include the image directly, no data uri or external file + // ?inline: force inlining an image regardless of the defined limit + ...getResourceQueryLoaders('webp-loader', webpLoaderOptions, urlLoaderOptions), + + // default behavior: inline if below the definied limit, external file if above + { + use: webpLoaders, + }, + ], + }); + } + if (typeof nextConfig.webpack === 'function') { return nextConfig.webpack(config, options); } diff --git a/test/index.test.js b/test/index.test.js index 5d215b7..7b71589 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -21,10 +21,12 @@ describe('next-optimized-images', () => { test('handles all images by default', () => { const config = getNextConfig(); - expect(config.module.rules).toHaveLength(1); + expect(config.module.rules).toHaveLength(2); const rule = config.module.rules[0]; + const webpRule = config.module.rules[1]; const imgLoaderOptions = rule.oneOf[rule.oneOf.length - 1].use[1].options; + const webpLoaderOptions = webpRule.oneOf[webpRule.oneOf.length - 1].use[1].options; expect(rule.test.test('.jpg')).toEqual(true); expect(rule.test.test('.jpeg')).toEqual(true); @@ -36,11 +38,16 @@ describe('next-optimized-images', () => { expect(rule.test.test('.PNG')).toEqual(true); expect(rule.test.test('.GIF')).toEqual(true); expect(rule.test.test('.SVG')).toEqual(true); + expect(rule.test.test('.webp')).toEqual(false); + expect(rule.test.test('.WEBP')).toEqual(false); + expect(webpRule.test.test('.webp')).toEqual(true); + expect(webpRule.test.test('.WEBP')).toEqual(true); expect(imgLoaderOptions.mozjpeg).not.toBeFalsy(); expect(imgLoaderOptions.optipng).not.toBeFalsy(); expect(imgLoaderOptions.pngquant).toEqual(false); expect(imgLoaderOptions.gifsicle).not.toBeFalsy(); expect(imgLoaderOptions.svgo).not.toBeFalsy(); + expect(webpLoaderOptions).not.toBeFalsy(); }); test('jpg can get disabled', () => { @@ -201,6 +208,24 @@ describe('next-optimized-images', () => { expect(imgLoaderOptions.gifsicle).not.toBeFalsy(); expect(imgLoaderOptions.svgo).not.toBeFalsy(); }); + + test('webp can get disabled', () => { + const config = getNextConfig({ webp: false }); + const rule = config.module.rules[0]; + + expect(config.module.rules).toHaveLength(1); + expect(rule.test.test('.webp')).toEqual(false); + expect(rule.test.test('.WEBP')).toEqual(false); + }); + + test('webp optimization can get disabled', () => { + const config = getNextConfig({ webp: null }); + const rule = config.module.rules[1]; + + expect(rule.oneOf[rule.oneOf.length - 1].use).toHaveLength(1); + expect(rule.test.test('.webp')).toEqual(true); + expect(rule.test.test('.WEBP')).toEqual(true); + }); }); /** @@ -280,7 +305,7 @@ describe('next-optimized-images', () => { test('propagate and merge configuration', () => { const config = getNextConfig({ webpack: (webpackConfig, webpackOptions) => { - expect(webpackConfig.module.rules).toHaveLength(1); + expect(webpackConfig.module.rules).toHaveLength(2); expect(webpackOptions.dev).toEqual(false); expect(webpackOptions.foo).toEqual('bar'); @@ -290,7 +315,7 @@ describe('next-optimized-images', () => { }, }, { foo: 'bar' }); - expect(config.module.rules).toHaveLength(1); + expect(config.module.rules).toHaveLength(2); expect(config.changed).toEqual(true); });