Skip to content

Commit

Permalink
Handle and optionally optimize webp images (#8)
Browse files Browse the repository at this point in the history
* Add support for handling and optimizing webp images

* Fix disabling of optimization of webp images

* Add tests for webp

* Update readme to handle webp images
  • Loading branch information
cyrilwanner authored Mar 19, 2018
1 parent 472a809 commit 11874ad
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 42 deletions.
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -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:

Expand All @@ -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

Expand Down Expand Up @@ -314,10 +314,14 @@ If you want svg images and icons to be handled but _not_ optimized, you can set
Type: `object`<br>
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.
Expand Down Expand Up @@ -351,6 +355,10 @@ module.exports = withPlugins([
svgo: {
// enable/disable svgo plugins here
},
webp: {
preset: 'default',
quality: 75,
},
}],
]);
```
Expand Down
125 changes: 90 additions & 35 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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
{
Expand All @@ -153,8 +177,8 @@ const withOptimizedImages = (nextConfig) => {
),
},
{
loader: `webp-loader`,
options: getOptimizerConfig(webp),
loader: 'webp-loader',
options: webpLoaderOptions || {},
},
],
},
Expand All @@ -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);
}
Expand Down
31 changes: 28 additions & 3 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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);
});
});

/**
Expand Down Expand Up @@ -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');

Expand All @@ -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);
});

Expand Down

0 comments on commit 11874ad

Please sign in to comment.