Repository: dazuaz/responsive-loader Branch: master Commit: ef2c806fcd36 Files: 31 Total size: 91.3 KB Directory structure: gitextract_hh3w3uug/ ├── .babelrc ├── .eslintignore ├── .eslintrc.js ├── .github/ │ └── workflows/ │ └── codeql-analysis.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── jimp.js ├── package.json ├── sharp.js ├── src/ │ ├── adapters/ │ │ ├── jimp.ts │ │ └── sharp.ts │ ├── cache.ts │ ├── cjs.js │ ├── index.ts │ ├── loader-utils.d.ts │ ├── parseQuery.ts │ ├── schema.json │ ├── types.d.ts │ └── utils.ts ├── test/ │ ├── cjs.test.js │ ├── jimp/ │ │ ├── build/ │ │ │ └── __snapshots__/ │ │ │ └── test.js.snap │ │ ├── index.js │ │ └── webpack.config.js │ ├── sharp/ │ │ ├── build/ │ │ │ └── __snapshots__/ │ │ │ └── test.js.snap │ │ ├── index.js │ │ └── webpack.config.js │ └── utils.test.js └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "presets": [ [ "@babel/preset-env", { "targets": { "node": "12.22.1" } } ] ] } ================================================ FILE: .eslintignore ================================================ # don't ever lint node_modules node_modules # don't lint build output lib # don't lint test test sharp.js jimp.js src/cjs.js .eslintrc.js ================================================ FILE: .eslintrc.js ================================================ module.exports = { root: true, parser: "@typescript-eslint/parser", plugins: ["@typescript-eslint"], extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"], ignorePatterns: ["examples/*"], rules: { "@typescript-eslint/ban-ts-comment": "off", }, } ================================================ FILE: .github/workflows/codeql-analysis.yml ================================================ # For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL" on: push: branches: [ master ] pull_request: # The branches below must be a subset of the branches above branches: [ master ] schedule: - cron: '20 4 * * 3' jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ 'javascript' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Learn more about CodeQL language support at https://git.io/codeql-language-support steps: - name: Checkout repository uses: actions/checkout@v2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v1 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v1 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines # and modify them (or add more) to build your code if your project # uses a compiled language #- run: | # make bootstrap # make release - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v1 ================================================ FILE: .gitignore ================================================ node_modules *.log test/build test/**/build/*.js test/**/build/**/*.png test/**/build/**/*.jpg test/**/build/**/*.avif test/**/build/**/*.jpeg test/**/build/**/*.webp .node-version lib .vscode .envrc .tool-versions ================================================ FILE: .travis.yml ================================================ os: osx language: node_js node_js: - '12' ================================================ FILE: CHANGELOG.md ================================================ # Change Log ## v1.1.0 - Added `min` and `max` options to automatically generate a number of images, and `steps` option to say how many images ([#31](https://github.com/herrstucki/responsive-loader/pull/31)). ## v1.0.0 ### New - 🚀 Added support for [sharp](https://github.com/lovell/sharp) ([#19](https://github.com/herrstucki/responsive-loader/pull/29)) ### Breaking #### Webpack 2 support Removed support for webpack 1! Please upgrade to webpack >= 2. The syntax to import images has changed. The query part now comes _after_ the resource (the image) instead of the loader. ```diff - require('responsive-loader?size=100!some-image.jpg') + require('responsive-loader!some-image.jpg?size=100') ``` That means if `responsive-loader` is configured in your webpack-config, it's possible to specify image-specific options without having to add the loader part to the import path. For example: ```js // webpack.config.js module.exports = { // ... module: { rules: [ { test: /\.jpg$/, loader: 'responsive-loader', options: { size: 1000 //... } } ] }, } // some-file.js const image1000 = require('some-image.jpg') // will have size 1000 from the config const image500 = require('some-image.jpg?size=500') ``` #### Other breaking changes - The `ext` option was removed, in favor of `format=jpg|png`. `[ext]` is now part of the `name` option like in other loaders (fixes [#13](https://github.com/herrstucki/responsive-loader/issues/13)) - Changed default JPEG `quality` to `85` - The `pass` option is now called `disable` ## v0.7.0 - Add `placeholder` option ([#16](https://github.com/herrstucki/responsive-loader/pull/16)) - Add `width` and `height` attributes to output ([#19](https://github.com/herrstucki/responsive-loader/pull/19)) ## v0.6.1 - Declare default `name`, `context`, `quality`, and `background` through webpack options when they're not specified in the loader query ([#12](https://github.com/herrstucki/responsive-loader/pull/12)). ## v0.6.0 - Add linting ([#7](https://github.com/herrstucki/responsive-loader/pull/7)) - Breaking (maybe): Require node >= v4 ## v0.5.3 - Fix wrong callback being called on file load error ([#6](https://github.com/herrstucki/responsive-loader/pull/6)) ## v0.5.2 - Added tests! - Update `queue-async` to `d3-queue` ## v0.5.1 - Optimization: skip resizing images of the same size ([#5](https://github.com/herrstucki/responsive-loader/pull/5)) ## v0.5.0 Using the `size` option for getting only one resized image no longer just returns a string but the same object structure as when using `sizes`. The difference is, that when `toString()` is called on that object, it will return the path of the first resized image. Also, for pure convenience, the returned object also contains a `src` property, so it can be spread onto a React component (e.g. ``). ### Before This worked: ```js import resized from 'responsive?sizes[]=100,sizes[]=200'; ``` ```css .foo { background-image: url('responsive?size=100'); } ``` But this didn't :sob:: ```js import resized from 'responsive?size=100'; // Whoops, error because `resized` ist just a string ``` ```css /* Whoops, `url('[object Object]')` */ .foo { background-image: url('responsive?sizes[]=100'); } ``` ### After All these work :v: ```js import resized from 'responsive?sizes[]=100,sizes[]=200'; ``` ```css .foo { background-image: url('responsive?sizes[]=100,sizes[]=200'); } .foo { background-image: url('responsive?sizes[]=100'); } .foo { background-image: url('responsive?size=100'); } ``` ================================================ FILE: LICENSE ================================================ Copyright (c) 2016, Jeremy Stucki All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of responsive-loader nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: README.md ================================================ # responsive-loader [![build][travis]][travis-url] [![node][node]][node-url] A webpack loader for responsive images. Creates multiple images from one source image, and returns a `srcset`. For more information on how to use `srcset`, read [Responsive Images](https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images). Browser support is [pretty good](http://caniuse.com/#search=srcset). ## Install ### With sharp ``` npm install responsive-loader sharp --save-dev ``` For [super-charged performance](http://sharp.dimens.io/en/stable/performance/) and webp and avif formats support, responsive-loader works with [sharp](https://github.com/lovell/sharp). It's recommended to use sharp if you have lots of images to transform. ```js module.exports = { // ... module: { rules: [ { test: /\.(png|jpe?g)$/, use: [ { loader: 'responsive-loader', options: { // Set options for all transforms }, }, ], type: 'javascript/auto', }, ], }, } ``` ### With jimp ``` npm install responsive-loader jimp --save-dev ``` Responsive-loader can be use with [jimp](https://github.com/oliver-moran/jimp) to transform images. which needs to be installed alongside responsive-loader. Because jimp is written entirely in JavaScript and doesn't have any native dependencies it will work anywhere. The main drawback is that it's pretty slow. If you want to use jimp, you need to configure responsive-loader to use its adapter: ```diff module.exports = { // ... module: { rules: [ { test: /\.(png|jpe?g)$/, use: [ { loader: 'responsive-loader', options: { + adapter: require('responsive-loader/jimp') }, }, ], type: 'javascript/auto', } ] }, } ``` ### Typescript ```typescript //declare a module to your type definitions files *.d.ts interface ResponsiveImageOutput { src: string srcSet: string placeholder: string | undefined images: { path: string; width: number; height: number }[] width: number height: number toString: () => string } declare module '*!rl' { const src: ResponsiveImageOutput export default src } ``` ``` import responsiveImage from 'img/myImage.jpg?sizes[]=300,sizes[]=600,sizes[]=1024,sizes[]=2048!rl'; import responsiveImageWebp from 'img/myImage.jpg?sizes[]=300,sizes[]=600,sizes[]=1024,sizes[]=2048&format=webp!rl'; ... ``` --- Then import images in your JavaScript files: ```js import responsiveImage from 'img/myImage.jpg?sizes[]=300,sizes[]=600,sizes[]=1024,sizes[]=2048'; import responsiveImageWebp from 'img/myImage.jpg?sizes[]=300,sizes[]=600,sizes[]=1024,sizes[]=2048&format=webp'; // Outputs // responsiveImage.srcSet => '2fefae46cb857bc750fa5e5eed4a0cde-300.jpg 300w,2fefae46cb857bc750fa5e5eed4a0cde-600.jpg 600w,2fefae46cb857bc750fa5e5eed4a0cde-600.jpg 600w ...' // responsiveImage.images => [{height: 150, path: '2fefae46cb857bc750fa5e5eed4a0cde-300.jpg', width: 300}, {height: 300, path: '2fefae46cb857bc750fa5e5eed4a0cde-600.jpg', width: 600} ...] // responsiveImage.src => '2fefae46cb857bc750fa5e5eed4a0cde-2048.jpg' // responsiveImage.toString() => '2fefae46cb857bc750fa5e5eed4a0cde-2048.jpg' ... ... ``` Notes: - `width` and `height` are intrinsic and are used to avoid layout shift, other techniques involve the use of aspect ratio and padding. - `sizes`, without sizes, the browser assumes the image is always 100vw for any viewport. - A helpful tool to determine proper sizes https://ausi.github.io/respimagelint/ - `loading` do not add loading lazy if the image is part of the initial rendering of the page or close to it. - `srcset` Modern browsers will choose the closest best image depending on the pixel density of your screen. - in the example above is your pixel density is `>1x` for a screen `>1024px` it will display the 2048 image. Or use it in CSS (only the first resized image will be used, if you use multiple `sizes`): ```css .myImage { background: url('myImage.jpg?size=1140'); } @media (max-width: 480px) { .myImage { background: url('myImage.jpg?size=480'); } } ``` ```js // Outputs placeholder image as a data URI, and three images with 100, 200, and 300px widths const responsiveImage = require('myImage.jpg?placeholder=true&sizes[]=100,sizes[]=200,sizes[]=300') // responsiveImage.placeholder => 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAIBAQE…' ReactDOM.render(
, el ) ``` You can also use [JSON5](https://json5.org/) notation: ``` ``` ### Options | Option | Type | Default | Description | | --------------------------------------- | --------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | `string` | `[hash]-[width].[ext]` | Filename template for output files. | | `outputPath` | `string \| Function` | `undefined` | Configure a custom output path for your file | | `publicPath` | `string \| Function` | `undefined` | Configure a custom public path for your file. | | `context` | `string` | `this.options.context` | Custom file context, defaults to webpack.config.js [context](https://webpack.js.org/configuration/entry-context/#context) | | `sizes` | `array` | _original size_ | Specify all widths you want to use; if a specified size exceeds the original image's width, the latter will be used (i.e. images won't be scaled up). You may also declare a default `sizes` array in the loader options in your `webpack.config.js`. | | `size` | `integer` | _original size_ | Specify one width you want to use; if the specified size exceeds the original image's width, the latter will be used (i.e. images won't be scaled up) | | `min` | `integer` | | As an alternative to manually specifying `sizes`, you can specify `min`, `max` and `steps`, and the sizes will be generated for you. | | `max` | `integer` | | See `min` above | | `steps` | `integer` | `4` | Configure the number of images generated between `min` and `max` (inclusive) | | `quality` | `integer` | `85` | JPEG and WEBP compression quality | | `format` | `string` | _original format_ | Either `png` or `jpg`; use to convert to another format. `webp` and `avif` is also supported, but only by the sharp adapter | | `placeholder` | `boolean` | `false` | A true or false value to specify wether to output a placeholder image as a data URI | | `placeholderSize` | `integer` | `40` | A number value specifying the width of the placeholder image, if enabled with the option above | | `adapter` | `Adapter` | JIMP | Specify which adapter to use. Can only be specified in the loader options. | | `disable` | `boolean` | `false` | Disable processing of images by this loader (useful in development). `srcSet` and other attributes will still be generated but only for the original size. Note that the `width` and `height` attributes will both be set to `100` but the image will retain its original dimensions. | | **[`esModule`](#esmodule)** | `boolean` | `false` | Use ES modules syntax. | | `emitFile` | `boolean` | `true` | If `true`, emits a file (writes a file to the filesystem). If `false`, the loader will still return a object with the public URI but will not emit the file. It is often useful to disable this option for server-side packages. | | **[`cacheDirectory`](#cachedirectory)** | `string` or `boolean` | `false` | Experimental: If `true`, this will cache the result object but not the image files. The images are only produced once, when they are not found in the results object cache, or when the options change (cache key). For Development you can set query parameter to `?cacheDirectory=false`. | #### Adapter-specific options ##### jimp - `background: number` — Background fill when converting transparent to opaque images. Make sure this is a valid hex number, e.g. `0xFFFFFFFF`) ##### sharp - `background: string` — Background fill when converting transparent to opaque images. E.g. `#FFFFFF` or `%23FFFFFF` for webpack > 5 - `format: webp` — Conversion to the `image/webp` format. Recognizes the `quality` option. - `format: avif` — Conversion to the `image/avif` format. Recognizes the `quality` option. - `progressive: boolean` - Use progressive (interlace) scan for `image/jpeg` format. - `rotate: number` - Rotates image [more here](https://sharp.pixelplumbing.com/api-operation#rotate) ### Examples Set a default `sizes` array, so you don't have to declare them with each `require`. ```js module.exports = { entry: {...}, output: {...}, module: { rules: [ { test: /\.(jpe?g|png|webp)$/i, use: [ { loader: "responsive-loader", options: { adapter: require('responsive-loader/sharp'), sizes: [320, 640, 960, 1200, 1800, 2400], placeholder: true, placeholderSize: 20 }, }, ], } ] }, } ``` ### `cacheDirectory` Type: `Boolean` or `string` Default: `false` Experimental: If `true`, this will cache the result object but not the image files. The images are only produced once, when they are not found in the results object cache, or when the options change (cache key). For Development you can set query parameter to individual images by using `?cacheDirectory=false`. Default cache directory might be `.node_modules/.cache/responsive-loader` ```js module.exports = { module: { rules: [ { test: /\.(jpe?g|png)$/i, use: [ { loader: 'responsive-loader', options: { esModule: true, cacheDirectory: true, publicPath: '/_next', name: 'static/media/[name]-[hash:7]-[width].[ext]', }, }, ], }, ], }, } ``` ### `esModule` Type: `Boolean` Default: `false` By default, `responsive-loader` generates JS modules that use the CommonJS syntax. There are some cases in which using ES modules is beneficial, like in the case of [module concatenation](https://webpack.js.org/plugins/module-concatenation-plugin/) and [tree shaking](https://webpack.js.org/guides/tree-shaking/). You can enable a ES module syntax using: **webpack.config.js** ```js module.exports = { module: { rules: [ { test: /\.(jpe?g|png)$/i, use: [ { loader: 'responsive-loader', options: { esModule: true, }, }, ], }, ], }, } ``` ### Writing Your Own Adapter Maybe you want to use another image processing library or you want to change an existing one's behavior. You can write your own adapter with the following signature: ```js type Adapter = (imagePath: string) => { metadata: () => Promise<{width: number, height: number}> resize: (config: {width: number, mime: string, options: Object}) => Promise<{data: Buffer, width: number, height: number}> } ``` The `resize` method takes a single argument which has a `width`, `mime` and `options` property (which receives all loader options) In your webpack config, require your adapter ```js { test: /\.(jpe?g|png)$/i, loader: 'responsive-loader', options: { adapter: require('./my-adapter') foo: 'bar' // will get passed to adapter.resize({width, mime, options: {foo: 'bar}}) } } ``` ## Notes - Doesn't support `1x`, `2x` sizes, but you probably don't need it. ## Usage Examples ### Next.js - https://github.com/dazuaz/responsive-loader-example ### Pug - [How to use responsive-loader with Pug](https://webdiscus.github.io/pug-plugin/responsive-image/). Thanks to the awesome [pug-loader](https://webdiscus.github.io/pug-plugin/hello-world/). Please submit your own example to add here [node]: https://img.shields.io/node/v/responsive-loader.svg [node-url]: https://nodejs.org [travis]: https://travis-ci.com/dazuaz/responsive-loader.svg?branch=master [travis-url]: https://travis-ci.com/dazuaz/responsive-loader ================================================ FILE: jimp.js ================================================ module.exports = require('./lib/adapters/jimp'); ================================================ FILE: package.json ================================================ { "name": "responsive-loader", "version": "3.1.2", "description": "A webpack loader for responsive images", "main": "lib/cjs.js", "engines": { "node": ">= 12.22.1" }, "scripts": { "build": "tsc", "lint": "eslint", "test:clean": "find test/**/build/ -name '*.jpg' -o -name '*.png' -o -name '*.avif' -o -name '*.webp' -o -name '*.jpeg' -o -name '*.js' | xargs rm -f", "test": "npm run build && npm run test:clean && webpack --config=./test/jimp/webpack.config.js && webpack --config=./test/sharp/webpack.config.js && jest" }, "np": { "yarn": false, "contents": "lib" }, "files": [ "lib", "jimp.js", "sharp.js" ], "repository": { "type": "git", "url": "git+https://github.com/dazuaz/responsive-loader.git" }, "keywords": [ "webpack", "responsive", "loader", "srcset" ], "author": "Jeremy Stucki ", "contributors": [ "Daniel Zuloaga (https://staticprops.com/)" ], "license": "BSD-3-Clause", "bugs": { "url": "https://github.com/dazuaz/responsive-loader/issues" }, "homepage": "https://github.com/dazuaz/responsive-loader", "peerDependencies": { "webpack": "^5.73.0" }, "peerDependenciesMeta": { "jimp": { "optional": true }, "sharp": { "optional": true } }, "dependencies": { "@types/node": "^18.11.9", "find-cache-dir": "^3.3.2", "json5": "^2.2.1", "loader-utils": "^3.2.1", "make-dir": "^3.1.0", "schema-utils": "^4.0.0" }, "devDependencies": { "@babel/core": "^7.20.2", "@babel/preset-env": "^7.20.2", "@types/find-cache-dir": "^3.2.1", "@types/jest": "^29.2.3", "@types/json-schema": "^7.0.11", "@types/sharp": "^0.31.0", "@types/webpack": "^5.28.0", "@typescript-eslint/eslint-plugin": "^5.43.0", "@typescript-eslint/parser": "^5.43.0", "babel-jest": "^29.3.1", "eslint": "^8.27.0", "jest": "^29.3.1", "jimp": "^0.16.2", "prettier": "^2.7.1", "prettier-eslint": "^15.0.1", "sharp": "^0.31.2", "typescript": "^4.9.3", "webpack": "^5.75.0", "webpack-cli": "^5.0.0" }, "jest": { "testEnvironment": "node" } } ================================================ FILE: sharp.js ================================================ module.exports = require('./lib/adapters/sharp'); ================================================ FILE: src/adapters/jimp.ts ================================================ import * as jimp from "jimp" type ResizeProps = { width: number mime: "image/jpeg" | "image/png" | "image/webp" | "image/avif" options: { background?: string rotate: number quality: number progressive?: boolean } } class JimpAdapter { readImage: Promise constructor(imagePath: string) { this.readImage = jimp.read(imagePath) } metadata(): Promise<{ height: number; width: number }> { return this.readImage.then((image) => ({ width: image.bitmap.width, height: image.bitmap.height, })) } resize({ width, mime, options, }: ResizeProps): Promise<{ width: number height: number data: Buffer }> { return new Promise((resolve, reject) => { this.readImage.then((image) => { image .clone() .resize(width, jimp.AUTO) .quality(options.quality) .background(parseInt(options.background + "", 16) || 0xffffffff) .getBuffer(mime, function (err, data) { // eslint-disable-line func-names if (err) { reject(err) } else { resolve({ data, width, height: this.bitmap.height, }) } }) }) }) } } module.exports = (imagePath: string): JimpAdapter => { return new JimpAdapter(imagePath) } ================================================ FILE: src/adapters/sharp.ts ================================================ import * as sharp from 'sharp' type ResizeProps = { width: number mime: 'image/jpeg' | 'image/png' | 'image/webp' | 'image/avif' options: { background?: string rotate: number quality: number progressive?: boolean } } class SharpAdapter { image: sharp.Sharp constructor(imagePath: string) { this.image = sharp(imagePath) } metadata(): Promise { return this.image.metadata() } resize({ width, mime, options }: ResizeProps): Promise<{ data: Buffer; width: number; height: number }> { return new Promise((resolve, reject) => { let resized = this.image.clone().resize(width, null) if (!options.rotate) { // .toBuffer() strips EXIF metadata like orientation, so portrait // images will become landscape. This updates the image to reflect // the EXIF metadata (if an EXIF orientation is set; otherwise unchanged). resized.rotate() } if (options.background) { resized = resized.flatten({ background: options.background, }) } if (mime === 'image/jpeg') { resized = resized.jpeg({ quality: options.quality, progressive: options.progressive, }) } if (mime === 'image/png') { resized = resized.png({ quality: options.quality, progressive: options.progressive, }) } if (mime === 'image/webp') { resized = resized.webp({ quality: options.quality, }) } if (mime === 'image/avif') { // @ts-ignore resized = resized.avif({ quality: options.quality, }) } // rotate if (options.rotate && options.rotate !== 0) { resized = resized.rotate(options.rotate) } resized.toBuffer((err, data, { height }) => { if (err) { reject(err) } else { resolve({ data, width, height, }) } }) }) } } // export default SharpAdapter module.exports = (imagePath: string): SharpAdapter => { return new SharpAdapter(imagePath) } ================================================ FILE: src/cache.ts ================================================ /** * Filesystem Cache * * Given a file and a transform function, cache the result into files * or retrieve the previously cached files if the given file is already known. * * @see https://github.com/babel/babel-loader/ */ import * as fs from 'fs' import * as os from 'os' import * as path from 'path' import * as zlib from 'zlib' import * as crypto from 'crypto' import * as findCacheDir from 'find-cache-dir' import * as makeDir from 'make-dir' import { promisify } from 'util' import { CacheOptions, TransformParams } from './types' import { transform } from '.' // Lazily instantiated when needed let defaultCacheDirectory: string | null = null const readFile = promisify(fs.readFile) const writeFile = promisify(fs.writeFile) const gunzip = promisify(zlib.gunzip) const gzip = promisify(zlib.gzip) /** * Read the contents from the compressed file. * * @async * @params {String} filename * @params {Boolean} compress */ const read = async function (filename: string, compress: boolean) { const data = await readFile(filename + (compress ? '.gz' : '')) const content = compress ? await gunzip(data) : data return JSON.parse(content.toString()) } /** * Write contents into a compressed file. * * @async * @params {String} filename * @params {Boolean} compress * @params {String} result */ const write = async function (filename: string, compress: boolean, result: string) { const content = JSON.stringify(result) const data = compress ? await gzip(content) : content return await writeFile(filename + (compress ? '.gz' : ''), data) } /** * Build the filename for the cached file * * @params {String} source File source code * @params {Object} options Options used * * @return {String} */ const filename = function (source: string, identifier: string) { const hash = crypto.createHash('md4') const contents = JSON.stringify({ source, identifier }) hash.update(contents) return hash.digest('hex') + '.json' } /** * Handle the cache * * @params {String} directory * @params {Object} params */ const handleCache = async function ( directory: string, cacheOptions: CacheOptions, params: TransformParams ): Promise { const { cacheIdentifier, cacheDirectory, cacheCompression } = cacheOptions const file = path.join(directory, filename(params.resourcePath, cacheIdentifier)) try { // No errors mean that the file was previously cached // we just need to return it return await read(file, cacheCompression) } catch (err) { // continue regardless of error } const fallback = typeof cacheDirectory !== 'string' && directory !== os.tmpdir() // Make sure the directory exists. try { await makeDir(directory) } catch (err) { if (fallback) { return handleCache(os.tmpdir(), cacheOptions, params) } throw err } // Otherwise just transform the file // return it to the user asap and write it in cache const result = await transform(params) try { await write(file, cacheCompression, result) } catch (err) { if (fallback) { // Fallback to tmpdir if node_modules folder not writable return handleCache(os.tmpdir(), cacheOptions, params) } throw err } return result } /** * Retrieve file from cache, or create a new one for future reads * * @async * @param {CacheOptions} cacheOptions * @param {TransformParams} transformParams Options to be given to the transform fn * */ export async function cache(cacheOptions: CacheOptions, transformParams: TransformParams): Promise { let directory if (typeof cacheOptions.cacheDirectory === 'string') { directory = cacheOptions.cacheDirectory } else { if (defaultCacheDirectory === null) { defaultCacheDirectory = findCacheDir({ name: 'responsive-loader' }) || os.tmpdir() } directory = defaultCacheDirectory } return await handleCache(directory, cacheOptions, transformParams) } ================================================ FILE: src/cjs.js ================================================ const loader = require('./index') module.exports = loader.default module.exports.raw = loader.raw ================================================ FILE: src/index.ts ================================================ import * as schema from './schema.json' import { validate } from 'schema-utils' import { JSONSchema7 } from 'schema-utils/declarations/ValidationError' import { parseOptions, getOutputAndPublicPath, createPlaceholder } from './utils' import { cache } from './cache' import type { LoaderContext } from 'webpack' import { interpolateName } from 'loader-utils' import { parseQuery } from './parseQuery' import type { Adapter, Options, CacheOptions, AdapterImplementation, MimeType, AdapterResizeResponse, TransformParams, } from './types' const DEFAULTS = { quality: 85, placeholder: false, placeholderSize: 40, name: '[hash]-[width].[ext]', steps: 4, esModule: false, emitFile: true, rotate: 0, cacheDirectory: false, cacheCompression: true, cacheIdentifier: '', } /** * **Responsive Loader** * * Creates multiple images from one source image, and returns a srcset * [Responsive Loader](https://github.com/dazuaz/responsive-loader) * * @param {Buffer} content Source * * @return {loaderCallback} loaderCallback Result */ export default function loader(this: LoaderContext, content: string): void { const loaderCallback = this.async() if (typeof loaderCallback == 'undefined') { new Error('Responsive loader callback error') return } // Parsers the query string and options const parsedResourceQuery = this.resourceQuery ? parseQuery(this.resourceQuery) : {} // Combines defaults, webpack options and query options, const options = { ...DEFAULTS, ...this.getOptions(), ...parsedResourceQuery } validate(schema as JSONSchema7, options, { name: 'Responsive Loader' }) const outputContext = options.context || this.rootContext const { mime, ext, name, sizes, outputPlaceholder, placeholderSize, imageOptions, cacheOptions } = parseOptions( this.resourcePath, options ) if (!mime) { loaderCallback(new Error('No mime type for file with extension ' + ext + ' supported')) return } const createFile = ({ data, width, height }: AdapterResizeResponse) => { const fileName = interpolateName(this, name, { context: outputContext, content: data.toString(), }) .replace(/\[width\]/gi, width + '') .replace(/\[height\]/gi, height + '') const { outputPath, publicPath } = getOutputAndPublicPath(fileName, { outputPath: options.outputPath, publicPath: options.publicPath, }) if (options.emitFile) { this.emitFile(outputPath, data) } return { src: publicPath + `+${JSON.stringify(` ${width}w`)}`, path: publicPath, width: width, height: height, } } /** * Disable processing of images by this loader (useful in development) */ if (options.disable) { const { path } = createFile({ data: content, width: 100, height: 100 }) loaderCallback( null, `${options.esModule ? 'export default' : 'module.exports ='} { srcSet: ${path}, images: [{path:${path},width:100,height:100}], src: ${path}, toString: function(){return ${path}} }` ) return } // The full config is passed to the adapter, later sources' properties overwrite earlier ones. const adapterOptions = Object.assign({}, options, imageOptions) const transformParams = { adapterModule: options.adapter, resourcePath: this.resourcePath, adapterOptions, createFile, outputPlaceholder, placeholderSize, mime, sizes, } orchestrate({ cacheOptions, transformParams }) .then((result) => loaderCallback(null, result)) .catch((err) => loaderCallback(err)) } interface OrchestrateParams { cacheOptions: CacheOptions transformParams: TransformParams } async function orchestrate(params: OrchestrateParams) { // use cached, or create new image. let result const { transformParams, cacheOptions } = params if (cacheOptions.cacheDirectory) { result = await cache(cacheOptions, transformParams) } else { result = await transform(transformParams) } return result } // Transform based on the parameters export async function transform({ adapterModule, resourcePath, createFile, sizes, mime, outputPlaceholder, placeholderSize, adapterOptions, }: TransformParams): Promise { const adapter: Adapter = adapterModule || require('./adapters/sharp') const img = adapter(resourcePath) const results = await transformations({ img, sizes, mime, outputPlaceholder, placeholderSize, adapterOptions }) let placeholder let files if (outputPlaceholder) { files = results.slice(0, -1).map(createFile) placeholder = createPlaceholder(results[results.length - 1], mime) } else { files = results.map(createFile) } const srcset = files.map((f) => f.src).join('+","+') const images = files.map((f) => `{path: ${f.path},width: ${f.width},height: ${f.height}}`).join(',') // default to the biggest image const defaultImage = files[files.length - 1] return `${adapterOptions.esModule ? 'export default' : 'module.exports ='} { srcSet: ${srcset}, images: [${images}], src: ${defaultImage.path}, toString: function(){return ${defaultImage.path}}, ${placeholder ? 'placeholder: ' + placeholder + ',' : ''} width: ${defaultImage.width}, height: ${defaultImage.height} }` } interface TransformationParams { img: AdapterImplementation sizes: number[] mime: MimeType outputPlaceholder: boolean placeholderSize: number adapterOptions: Options } /** * **Run Transformations** * * For each size defined in the parameters, resize an image via the adapter * */ async function transformations({ img, sizes, mime, outputPlaceholder, placeholderSize, adapterOptions, }: TransformationParams): Promise { const metadata = await img.metadata() const promises = [] const widthsToGenerate = new Set() sizes.forEach((size) => { const width = Math.min(metadata.width, size) // Only resize images if they aren't an exact copy of one already being resized... if (!widthsToGenerate.has(width)) { widthsToGenerate.add(width) promises.push( img.resize({ width, mime, options: adapterOptions, }) ) } }) if (outputPlaceholder) { promises.push( img.resize({ width: placeholderSize, options: adapterOptions, mime, }) ) } return Promise.all(promises) } export const raw = true ================================================ FILE: src/loader-utils.d.ts ================================================ declare module 'loader-utils' { export function interpolateName(LoaderContext: any, name: string, options: any): any } ================================================ FILE: src/parseQuery.ts ================================================ import * as JSON5 from 'json5' interface LooseObject { [key: string]: any } const specialValues: LooseObject = { null: null, true: true, false: false, } function parseQuery(query: string): LooseObject { if (query.slice(0, 1) !== '?') { throw new Error("A valid query string passed to parseQuery should begin with '?'") } query = query.slice(1) if (!query) { return {} } if (query.slice(0, 1) === '{' && query.slice(-1) === '}') { query = decodeURIComponent(query) return JSON5.parse(query) } const queryArgs = query.split(/[,&]/g) const result: LooseObject = {} queryArgs.forEach((arg) => { const idx = arg.indexOf('=') if (idx >= 0) { let name = arg.slice(0, idx) let value = decodeURIComponent(arg.slice(idx + 1)) // const specialValues: LooseObject = {} // eslint-disable-next-line no-prototype-builtins if (specialValues.hasOwnProperty(value)) { value = specialValues[value] } if (name.slice(-2) === '[]') { name = decodeURIComponent(name.slice(0, name.length - 2)) if (!Array.isArray(result[name])) { result[name] = [] } result[name].push(value) } else { name = decodeURIComponent(name) result[name] = value } } else { if (arg.slice(0, 1) === '-') { result[decodeURIComponent(arg.slice(1))] = false } else if (arg.slice(0, 1) === '+') { result[decodeURIComponent(arg.slice(1))] = true } else { result[decodeURIComponent(arg)] = true } } }) return result } export { parseQuery } ================================================ FILE: src/schema.json ================================================ { "title": "Responsive Loader options", "type": "object", "properties": { "size": { "anyOf": [{ "type": "string" }, { "type": "number" }] }, "sizes": { "type": "array", "items": { "description": "Array of strings or numbers", "anyOf": [ { "type": "string" }, { "type": "number" } ] } }, "min": { "anyOf": [{ "type": "string" }, { "type": "number" }] }, "max": { "anyOf": [{ "type": "string" }, { "type": "number" }] }, "steps": { "anyOf": [{ "type": "string" }, { "type": "number" }] }, "name": { "type": "string" }, "outputPath": { "anyOf": [{ "type": "string" }, { "instanceof": "Function" }] }, "publicPath": { "anyOf": [{ "type": "string" }, { "instanceof": "Function" }] }, "context": { "type": "string" }, "placeholderSize": { "anyOf": [{ "type": "string" }, { "type": "number" }] }, "quality": { "anyOf": [{ "type": "string" }, { "type": "number" }] }, "background": { "anyOf": [{ "type": "string" }, { "type": "number" }] }, "rotate": { "anyOf": [{ "type": "string" }, { "type": "number" }] }, "progressive": { "type": "boolean" }, "placeholder": { "type": ["string", "boolean"] }, "adapter": { "instanceof": "Function" }, "format": { "type": "string", "enum": ["png", "jpg", "jpeg", "webp", "avif"] }, "disable": { "type": "boolean" }, "esModule": { "description": "By default, responsive-loader generates JS modules that don't use the ES modules syntax.", "type": "boolean" }, "emitFile": { "description": "Enables/Disables emit files.", "type": "boolean" }, "cacheDirectory": { "anyOf": [{ "type": "string" }, { "type": "boolean" }] }, "cacheIdentifier": { "type": "string" }, "cacheCompression": { "type": "boolean" } }, "additionalProperties": true } ================================================ FILE: src/types.d.ts ================================================ export type Options = { size?: string | number sizes?: [string | number] min?: string | number max?: string | number steps: string | number name: string outputPath?: ((...args: Array) => string) | string publicPath?: ((...args: Array) => string) | string context?: string placeholder: string | boolean placeholderSize: string | number quality: string | number background?: string | number progressive?: boolean rotate: string | number adapter?: Adapter format?: Format disable?: boolean | null esModule: boolean emitFile: boolean cacheDirectory: string | boolean cacheIdentifier: string cacheCompression: boolean } export type Format = 'png' | 'jpg' | 'jpeg' | 'webp' | 'avif' export type FileExt = 'jpg' | 'png' | 'webp' | 'avif' export type MimeType = 'image/jpeg' | 'image/png' | 'image/webp' | 'image/avif' export interface CacheOptions { cacheDirectory: string | boolean cacheIdentifier: string cacheCompression: boolean } export type Adapter = (imagePath: string) => AdapterImplementation export interface ImageOptions { quality: number background?: string | number progressive: boolean rotate?: number } export interface AdapterImplementation { metadata: () => Promise<{ width: number; height: number }> resize: (config: { width: number; mime: string; options: Options }) => Promise } export type AdapterResizeResponse = { data: string | Buffer; width: number; height: number } export interface TransformParams { adapterModule: Adapter | undefined resourcePath: string createFile: ({ data, width, height }: AdapterResizeResponse) => { src: string path: string width: number height: number } outputPlaceholder: boolean placeholderSize: number mime: MimeType sizes: number[] adapterOptions: Options & ImageOptions } ================================================ FILE: src/utils.ts ================================================ import * as path from 'path' import type { Options, MimeType, ImageOptions, CacheOptions } from './types' const version = '3' enum MIMES { jpg = 'image/jpeg', jpeg = 'image/jpeg', png = 'image/png', webp = 'image/webp', avif = 'image/avif', } enum EXTS { 'image/jpeg' = 'jpg', 'image/png' = 'png', 'image/webp' = 'webp', 'image/avif' = 'avif', } type ParsedOptions = { outputPlaceholder: boolean placeholderSize: number name: string mime: MimeType | undefined ext: string sizes: number[] imageOptions: ImageOptions cacheOptions: CacheOptions } function parseOptions(resourcePath: string, options: Options): ParsedOptions { const outputPlaceholder = Boolean(options.placeholder) const placeholderSize: number = parseInt(options.placeholderSize + '', 10) // Adapter compression options const imageOptions: ImageOptions = { quality: parseInt(options.quality + '', 10), rotate: parseInt(options.rotate + '', 10), background: options.background, progressive: Boolean(options.progressive), } // let mime: MimeType | undefined // let ext: FileExt | string let mime let ext if (options.format) { mime = MIMES[options.format] ext = EXTS[mime] } else { ext = path.extname(resourcePath).replace(/\./, '') switch (ext) { case 'jpg': case 'jpeg': case 'png': case 'webp': case 'avif': mime = MIMES[ext] break default: mime = undefined break } } const name = options.name.replace(/\[ext\]/gi, ext) const min: number | void = options.min !== undefined ? parseInt(options.min + '', 10) : undefined const max: number | void = options.max !== undefined ? parseInt(options.max + '', 10) : undefined const steps: number = parseInt(options.steps + '', 10) let generatedSizes if (typeof min === 'number' && max) { generatedSizes = [] for (let step = 0; step < steps; step++) { const size = min + ((max - min) / (steps - 1)) * step generatedSizes.push(Math.ceil(size)) } } const size = parseInt(options.size + '', 10) const sizes = size ? [size] : options.sizes?.map((size) => parseInt(size + '', 10)) || generatedSizes || [Number.MAX_SAFE_INTEGER] // Cache options const cacheOptions: CacheOptions = { cacheDirectory: options.cacheDirectory, cacheIdentifier: JSON.stringify({ options, 'responsive-loader': version, }), cacheCompression: Boolean(options.cacheCompression), } return { ext, mime, name, sizes, outputPlaceholder, placeholderSize, cacheOptions, imageOptions, } } const createPlaceholder = ({ data }: { data: any }, mime: string): string => { return `"data:${mime};base64,${data.toString('base64')}"` } // return `"data:${mime};base64,${data.toString("base64")}"` interface GetOutputAndPublicPath { ( fileName: string, { outputPath, publicPath, }: { outputPath?: ((...args: Array) => string) | string publicPath?: ((...args: Array) => string) | string } ): { outputPath: string publicPath: string } } /** * **Responsive Loader Paths** * * Returns the output and public path * * @method getOutputAndPublicPath * * @param {string} fileName * @param {Config} outputPath * @param {Config} publicPath * * @return {Config} Paths Result */ const getOutputAndPublicPath: GetOutputAndPublicPath = ( fileName: string, { outputPath: configOutputPath, publicPath: configPublicPath } ) => { let outputPath = fileName if (configOutputPath) { if (typeof configOutputPath === 'function') { outputPath = configOutputPath(fileName) } else { outputPath = path.posix.join(configOutputPath, fileName) } } let publicPath = `__webpack_public_path__ + ${JSON.stringify(outputPath)}` if (configPublicPath) { if (typeof configPublicPath === 'function') { publicPath = configPublicPath(fileName) } else { // publicPath can be a url or local path // check if it's a valid url if (isValidUrl(configPublicPath)) { const url = new URL(configPublicPath) url.pathname = path.posix.join(url.pathname, fileName) publicPath = url.toString() } else { publicPath = path.posix.join(configPublicPath, fileName) } } publicPath = JSON.stringify(publicPath) } return { outputPath, publicPath, } } const isValidUrl = (urlString: string) => { try { return Boolean(new URL(urlString)) } catch (e) { return false } } export { parseOptions, getOutputAndPublicPath, createPlaceholder } ================================================ FILE: test/cjs.test.js ================================================ import src from '../lib' import cjs from '../lib/cjs' describe('CJS', () => { it('should export loader', () => { expect(cjs).toEqual(src) }) it('should export "raw" flag', () => { expect(cjs.raw).toEqual(true) }) }) ================================================ FILE: test/jimp/build/__snapshots__/test.js.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`disable 1`] = ` { "default": { "images": [ { "height": 100, "path": "foobar/82e6d197b5ce433a-100.jpg", "width": 100, }, ], "src": "foobar/82e6d197b5ce433a-100.jpg", "srcSet": "foobar/82e6d197b5ce433a-100.jpg", "toString": [Function], }, } `; exports[`doesn't emit file 1`] = ` { "default": { "height": 225, "images": [ { "height": 225, "path": "foobar/445db0be601e84d6-250.jpg", "width": 250, }, ], "src": "foobar/445db0be601e84d6-250.jpg", "srcSet": "foobar/445db0be601e84d6-250.jpg 250w", "toString": [Function], "width": 250, }, } `; exports[`multiple sizes 1`] = ` { "default": { "height": 900, "images": [ { "height": 450, "path": "foobar/c869fe04ebafd01d-500.jpg", "width": 500, }, { "height": 900, "path": "foobar/076d6ca8665b1d94-1000.jpg", "width": 1000, }, ], "src": "foobar/076d6ca8665b1d94-1000.jpg", "srcSet": "foobar/c869fe04ebafd01d-500.jpg 500w,foobar/076d6ca8665b1d94-1000.jpg 1000w", "toString": [Function], "width": 1000, }, } `; exports[`output first resized image height & width 1`] = ` { "default": { "height": 450, "images": [ { "height": 450, "path": "foobar/c869fe04ebafd01d-500.jpg", "width": 500, }, ], "src": "foobar/c869fe04ebafd01d-500.jpg", "srcSet": "foobar/c869fe04ebafd01d-500.jpg 500w", "toString": [Function], "width": 500, }, } `; exports[`output should be in outputPath dir 1`] = ` { "default": { "height": 900, "images": [ { "height": 450, "path": "foobar/img/c869fe04ebafd01d-500.jpg", "width": 500, }, { "height": 675, "path": "foobar/img/76a7b8dd076af418-750.jpg", "width": 750, }, { "height": 900, "path": "foobar/img/076d6ca8665b1d94-1000.jpg", "width": 1000, }, ], "src": "foobar/img/076d6ca8665b1d94-1000.jpg", "srcSet": "foobar/img/c869fe04ebafd01d-500.jpg 500w,foobar/img/76a7b8dd076af418-750.jpg 750w,foobar/img/076d6ca8665b1d94-1000.jpg 1000w", "toString": [Function], "width": 1000, }, } `; exports[`output should be relative to context 1`] = ` { "default": { "height": 900, "images": [ { "height": 450, "path": "foobar/test/c869fe04ebafd01d-500x450.jpg", "width": 500, }, { "height": 675, "path": "foobar/test/76a7b8dd076af418-750x675.jpg", "width": 750, }, { "height": 900, "path": "foobar/test/076d6ca8665b1d94-1000x900.jpg", "width": 1000, }, ], "src": "foobar/test/076d6ca8665b1d94-1000x900.jpg", "srcSet": "foobar/test/c869fe04ebafd01d-500x450.jpg 500w,foobar/test/76a7b8dd076af418-750x675.jpg 750w,foobar/test/076d6ca8665b1d94-1000x900.jpg 1000w", "toString": [Function], "width": 1000, }, } `; exports[`override min and max with size 1`] = ` { "default": { "height": 90, "images": [ { "height": 90, "path": "foobar/f7443232972a8934-100.jpg", "width": 100, }, ], "src": "foobar/f7443232972a8934-100.jpg", "srcSet": "foobar/f7443232972a8934-100.jpg 100w", "toString": [Function], "width": 100, }, } `; exports[`override min and max with sizes 1`] = ` { "default": { "height": 180, "images": [ { "height": 90, "path": "foobar/f7443232972a8934-100.jpg", "width": 100, }, { "height": 180, "path": "foobar/974831fb3cc31ef7-200.jpg", "width": 200, }, ], "src": "foobar/974831fb3cc31ef7-200.jpg", "srcSet": "foobar/f7443232972a8934-100.jpg 100w,foobar/974831fb3cc31ef7-200.jpg 200w", "toString": [Function], "width": 200, }, } `; exports[`parses json notation 1`] = ` { "default": { "height": 180, "images": [ { "height": 45, "path": "foobar/843b57924a32e9ff-50.jpg", "width": 50, }, { "height": 90, "path": "foobar/f7443232972a8934-100.jpg", "width": 100, }, { "height": 180, "path": "foobar/974831fb3cc31ef7-200.jpg", "width": 200, }, ], "src": "foobar/974831fb3cc31ef7-200.jpg", "srcSet": "foobar/843b57924a32e9ff-50.jpg 50w,foobar/f7443232972a8934-100.jpg 100w,foobar/974831fb3cc31ef7-200.jpg 200w", "toString": [Function], "width": 200, }, } `; exports[`png 1`] = ` { "default": { "height": 595, "images": [ { "height": 580, "path": "foobar/f64253666cd2fe13-500.png", "width": 500, }, { "height": 595, "path": "foobar/a44dff6b028c41f7-513.png", "width": 513, }, ], "src": "foobar/a44dff6b028c41f7-513.png", "srcSet": "foobar/f64253666cd2fe13-500.png 500w,foobar/a44dff6b028c41f7-513.png 513w", "toString": [Function], "width": 513, }, } `; exports[`png to jpeg with background color 1`] = ` { "default": { "height": 595, "images": [ { "height": 580, "path": "foobar/940321b5b2ba9532-500.jpg", "width": 500, }, { "height": 595, "path": "foobar/07deb4eb4e131586-513.jpg", "width": 513, }, ], "src": "foobar/07deb4eb4e131586-513.jpg", "srcSet": "foobar/940321b5b2ba9532-500.jpg 500w,foobar/07deb4eb4e131586-513.jpg 513w", "toString": [Function], "width": 513, }, } `; exports[`png to jpeg with background color 2`] = ` { "default": { "height": 595, "images": [ { "height": 580, "path": "foobar/940321b5b2ba9532-500.jpg", "width": 500, }, { "height": 595, "path": "foobar/07deb4eb4e131586-513.jpg", "width": 513, }, ], "src": "foobar/07deb4eb4e131586-513.jpg", "srcSet": "foobar/940321b5b2ba9532-500.jpg 500w,foobar/07deb4eb4e131586-513.jpg 513w", "toString": [Function], "width": 513, }, } `; exports[`public path should replace global publicPath 1`] = ` { "default": { "height": 900, "images": [ { "height": 450, "path": "public/c869fe04ebafd01d-500.jpg", "width": 500, }, { "height": 675, "path": "public/76a7b8dd076af418-750.jpg", "width": 750, }, { "height": 900, "path": "public/076d6ca8665b1d94-1000.jpg", "width": 1000, }, ], "src": "public/076d6ca8665b1d94-1000.jpg", "srcSet": "public/c869fe04ebafd01d-500.jpg 500w,public/76a7b8dd076af418-750.jpg 750w,public/076d6ca8665b1d94-1000.jpg 1000w", "toString": [Function], "width": 1000, }, } `; exports[`public path should replace global publicPath absolute 1`] = ` { "default": { "height": 900, "images": [ { "height": 450, "path": "/public/c869fe04ebafd01d-500.jpg", "width": 500, }, { "height": 675, "path": "/public/76a7b8dd076af418-750.jpg", "width": 750, }, { "height": 900, "path": "/public/076d6ca8665b1d94-1000.jpg", "width": 1000, }, ], "src": "/public/076d6ca8665b1d94-1000.jpg", "srcSet": "/public/c869fe04ebafd01d-500.jpg 500w,/public/76a7b8dd076af418-750.jpg 750w,/public/076d6ca8665b1d94-1000.jpg 1000w", "toString": [Function], "width": 1000, }, } `; exports[`single size 1`] = ` { "default": { "height": 450, "images": [ { "height": 450, "path": "foobar/c869fe04ebafd01d-500.jpg", "width": 500, }, ], "src": "foobar/c869fe04ebafd01d-500.jpg", "srcSet": "foobar/c869fe04ebafd01d-500.jpg 500w", "toString": [Function], "width": 500, }, } `; exports[`with min and max sizes 1`] = ` { "default": { "height": 900, "images": [ { "height": 450, "path": "foobar/c869fe04ebafd01d-500.jpg", "width": 500, }, { "height": 675, "path": "foobar/76a7b8dd076af418-750.jpg", "width": 750, }, { "height": 900, "path": "foobar/076d6ca8665b1d94-1000.jpg", "width": 1000, }, ], "src": "foobar/076d6ca8665b1d94-1000.jpg", "srcSet": "foobar/c869fe04ebafd01d-500.jpg 500w,foobar/76a7b8dd076af418-750.jpg 750w,foobar/076d6ca8665b1d94-1000.jpg 1000w", "toString": [Function], "width": 1000, }, } `; exports[`with min and max sizes options 1`] = ` { "default": { "height": 270, "images": [ { "height": 90, "path": "foobar/f7443232972a8934-100.jpg", "width": 100, }, { "height": 150, "path": "foobar/fb90951ca5da54c2-167.jpg", "width": 167, }, { "height": 211, "path": "foobar/fe6debad717249e9-234.jpg", "width": 234, }, { "height": 270, "path": "foobar/a990e2f8f763852a-300.jpg", "width": 300, }, ], "src": "foobar/a990e2f8f763852a-300.jpg", "srcSet": "foobar/f7443232972a8934-100.jpg 100w,foobar/fb90951ca5da54c2-167.jpg 167w,foobar/fe6debad717249e9-234.jpg 234w,foobar/a990e2f8f763852a-300.jpg 300w", "toString": [Function], "width": 300, }, } `; exports[`with min and max sizes, and default steps 1`] = ` { "default": { "height": 900, "images": [ { "height": 450, "path": "foobar/c869fe04ebafd01d-500.jpg", "width": 500, }, { "height": 675, "path": "foobar/76a7b8dd076af418-750.jpg", "width": 750, }, { "height": 900, "path": "foobar/076d6ca8665b1d94-1000.jpg", "width": 1000, }, ], "src": "foobar/076d6ca8665b1d94-1000.jpg", "srcSet": "foobar/c869fe04ebafd01d-500.jpg 500w,foobar/76a7b8dd076af418-750.jpg 750w,foobar/076d6ca8665b1d94-1000.jpg 1000w", "toString": [Function], "width": 1000, }, } `; exports[`with placeholder image 1`] = ` { "default": { "height": 900, "images": [ { "height": 450, "path": "foobar/c869fe04ebafd01d-500.jpg", "width": 500, }, { "height": 675, "path": "foobar/76a7b8dd076af418-750.jpg", "width": 750, }, { "height": 900, "path": "foobar/076d6ca8665b1d94-1000.jpg", "width": 1000, }, ], "placeholder": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAUDBAQEAwUEBAQFBQUGBwwIBwcHBw8LCwkMEQ8SEhEPERETFhwXExQaFRERGCEYGh0dHx8fExciJCIeJBweHx4BBQUFBwYHDggIDh4UERQeHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHv/AABEIACQAKAMBEQACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AMnW/wBnaO4v4hpWo2tjZFv3reWxkRfRVzhvqSMe9bukuhnzO1mUm/ZomXVIZF8Uxy2YbMiyWZD47AYYg/pU+yGpF3XP2aNIuYi+na7cWlwe5hDRn/gORj86p0kHMzl7j9nPxZo9za32karY6lJGcyxMDCf+A5JB49cVlUoOUWjajW9nNS7HfeDPg9ObqK+8VyQsiHcLKE5DEf327j2HX1rCjgrO8zvxGZuS5aa+Z7dPNBa20tzcyRwwRKXkkfhVUDJJ9K9A8g8i8TftB+D9MuXg0u1utXZODKn7mIn2LfMf++cVm6iQ1FkvgT48eG/EWsRaXe2E+kTXB2xSPKJIi3oTgEZ9cYoVVdQcWeqQ3thcY+z3dtNuO0bJFbJ9OD1rS6ESFAeOfoaAPFPFvw98dalod43iv4hNNYW8DyPHH+7jbaCfnwACOOpzWMozfUtOK6HzWbIu/wC7IlQAH5PmyOnPpWG6L2Z33w1+Geta217cBlsJ9PmEWy5Uq27GfTjginKN0JSsz1v4S/DS78N+NINakuIfJSNmuQX3GSYhh8vA4AYHPrmqpwfMmwlJWPbAVfkZA7iuoyM+70+0u7SW2u4FninUpJDJ8yOD1BB4NTuB5/rHwltZ/F9j4h0pbKxNsQJIBbDy3VfukKMYYcfp6VDp63Q0+5sa5pOssrxxqmbniS4jjw+4Dgkg5HAxntUzTYIs/C7Tdfg8O+X4ilea4S5kEczABpI88EgdO/4VVNO2o5W6HXeQVyQcitCSVVGc5NMBr5DcE1LASJiwYtzzTQErcKBng0wAdM9z1oA//9k=", "src": "foobar/076d6ca8665b1d94-1000.jpg", "srcSet": "foobar/c869fe04ebafd01d-500.jpg 500w,foobar/76a7b8dd076af418-750.jpg 750w,foobar/076d6ca8665b1d94-1000.jpg 1000w", "toString": [Function], "width": 1000, }, } `; exports[`with size defined in webpack.config.js 1`] = ` { "default": { "height": 900, "images": [ { "height": 450, "path": "foobar/c869fe04ebafd01d-500.jpg", "width": 500, }, { "height": 675, "path": "foobar/76a7b8dd076af418-750.jpg", "width": 750, }, { "height": 900, "path": "foobar/076d6ca8665b1d94-1000.jpg", "width": 1000, }, ], "src": "foobar/076d6ca8665b1d94-1000.jpg", "srcSet": "foobar/c869fe04ebafd01d-500.jpg 500w,foobar/76a7b8dd076af418-750.jpg 750w,foobar/076d6ca8665b1d94-1000.jpg 1000w", "toString": [Function], "width": 1000, }, } `; ================================================ FILE: test/jimp/index.js ================================================ test('multiple sizes', () => { const multi = require('../cat-1000.jpg?sizes[]=500&sizes[]=2000') expect(multi).toMatchSnapshot() expect(multi.default.toString()).toBe(multi.default.src) }) test('parses json notation', () => { const multi = require('../cat-1000.jpg?{sizes:[50,100,200]}') expect(multi).toMatchSnapshot() }) test('single size', () => { const single = require('../cat-1000.jpg?size=500') expect(single).toMatchSnapshot() }) test('with size defined in webpack.config.js', () => { const multi = require('../cat-1000.jpg') expect(multi).toMatchSnapshot() }) test('disable', () => { const multi = require('../cat-1000.jpg?disable') expect(multi).toMatchSnapshot() }) test('output should be relative to context', () => { const multi = require('../cat-1000.jpg?name=[path][hash]-[width]x[height].[ext]&context=./') expect(multi).toMatchSnapshot() }) test('output should be in outputPath dir', () => { const multi = require('../cat-1000.jpg?outputPath=img/') expect(multi).toMatchSnapshot() }) test('public path should replace global publicPath', () => { const multi = require('../cat-1000.jpg?outputPath=img/&publicPath=public/') expect(multi).toMatchSnapshot() }) test('public path should replace global publicPath absolute', () => { const multi = require('../cat-1000.jpg?outputPath=/img2/&publicPath=/public/') expect(multi).toMatchSnapshot() }) test('with placeholder image', () => { const output = require('../cat-1000.jpg?placeholder=true') expect(output).toMatchSnapshot() }) test('output first resized image height & width', () => { const output = require('../cat-1000.jpg?size=500') expect(output).toMatchSnapshot() }) test('png', () => { const output = require('../cat-transparent.png') expect(output).toMatchSnapshot() }) test('png to jpeg with background color', () => { const output = require('../cat-transparent.png?background=0xFF0000FF&format=jpg') expect(output).toMatchSnapshot() }) test('png to jpeg with background color', () => { const output = require('../cat-transparent.png?background=0xFF0000FF&format=jpg') expect(output).toMatchSnapshot() }) test('with min and max sizes', () => { const output = require('../cat-1000.jpg?min=600&max=800&steps=3') expect(output).toMatchSnapshot() }) test('with min and max sizes, and default steps', () => { const output = require('../cat-1000.jpg?min=500&max=1000') expect(output).toMatchSnapshot() }) test('with min and max sizes options', () => { const output = require('../cat-1000.jpg?minmax') expect(output).toMatchSnapshot() }) test('override min and max with sizes', () => { const output = require('../cat-1000.jpg?minmax&sizes[]=100&sizes[]=200') expect(output).toMatchSnapshot() }) test('override min and max with size', () => { const output = require('../cat-1000.jpg?minmax&size=100') expect(output).toMatchSnapshot() }) test("doesn't emit file", () => { const multi = require('../cat-1000.jpg?emitFile=false&sizes[]=250') expect(multi).toMatchSnapshot() }) ================================================ FILE: test/jimp/webpack.config.js ================================================ const path = require('path') module.exports = { mode: 'development', entry: path.resolve(__dirname, 'index'), module: { rules: [ // This rule will be matched when the resourceQuery contains `minmax`, e.g. `cat-1000.jpg?minmax` { test: /\.(png|jpg)$/, resourceQuery: /minmax/, loader: require.resolve('../../lib/index'), options: { min: 100, max: 300, esModule: true, adapter: require('../../jimp'), }, type: 'javascript/auto', }, { test: /\.(png|jpg)$/, loader: require.resolve('../../lib/index'), options: { sizes: [500, 750, 1000], esModule: true, adapter: require('../../jimp'), }, type: 'javascript/auto', }, ], }, output: { path: path.resolve(__dirname, 'build'), publicPath: 'foobar/', filename: 'test.js', }, target: 'node', } ================================================ FILE: test/sharp/build/__snapshots__/test.js.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Works with the cache 1`] = ` { "height": 451, "images": [ { "height": 451, "path": "foobar/ab790bc248982116-501.jpg", "width": 501, }, ], "src": "foobar/ab790bc248982116-501.jpg", "srcSet": "foobar/ab790bc248982116-501.jpg 501w", "toString": [Function], "width": 501, } `; exports[`creates jpeg extension 1`] = ` { "height": 395, "images": [ { "height": 395, "path": "foobar/f93d9af4a72634bc-439.jpeg", "width": 439, }, ], "src": "foobar/f93d9af4a72634bc-439.jpeg", "srcSet": "foobar/f93d9af4a72634bc-439.jpeg 439w", "toString": [Function], "width": 439, } `; exports[`disable 1`] = ` { "images": [ { "height": 100, "path": "foobar/82e6d197b5ce433a-100.jpg", "width": 100, }, ], "src": "foobar/82e6d197b5ce433a-100.jpg", "srcSet": "foobar/82e6d197b5ce433a-100.jpg", "toString": [Function], } `; exports[`doesn't emit file 1`] = ` { "height": 225, "images": [ { "height": 225, "path": "foobar/0a229ed85b2f5618-250.jpg", "width": 250, }, ], "src": "foobar/0a229ed85b2f5618-250.jpg", "srcSet": "foobar/0a229ed85b2f5618-250.jpg 250w", "toString": [Function], "width": 250, } `; exports[`hash lenght output should be relative to context 1`] = ` { "height": 900, "images": [ { "height": 450, "path": "foobar/test/282d046d-500.jpg", "width": 500, }, { "height": 675, "path": "foobar/test/b8e60128-750.jpg", "width": 750, }, { "height": 900, "path": "foobar/test/3cf3c375-1000.jpg", "width": 1000, }, ], "src": "foobar/test/3cf3c375-1000.jpg", "srcSet": "foobar/test/282d046d-500.jpg 500w,foobar/test/b8e60128-750.jpg 750w,foobar/test/3cf3c375-1000.jpg 1000w", "toString": [Function], "width": 1000, } `; exports[`jpg to webp 1`] = ` { "height": 900, "images": [ { "height": 450, "path": "foobar/8593c1f9da542c50-500.webp", "width": 500, }, { "height": 675, "path": "foobar/72befca36bd004a7-750.webp", "width": 750, }, { "height": 900, "path": "foobar/c66204d304cfe4fd-1000.webp", "width": 1000, }, ], "src": "foobar/c66204d304cfe4fd-1000.webp", "srcSet": "foobar/8593c1f9da542c50-500.webp 500w,foobar/72befca36bd004a7-750.webp 750w,foobar/c66204d304cfe4fd-1000.webp 1000w", "toString": [Function], "width": 1000, } `; exports[`multiple sizes 1`] = ` { "height": 900, "images": [ { "height": 450, "path": "foobar/282d046d550fa19c-500.jpg", "width": 500, }, { "height": 900, "path": "foobar/3cf3c37514578847-1000.jpg", "width": 1000, }, ], "src": "foobar/3cf3c37514578847-1000.jpg", "srcSet": "foobar/282d046d550fa19c-500.jpg 500w,foobar/3cf3c37514578847-1000.jpg 1000w", "toString": [Function], "width": 1000, } `; exports[`output first resized image height & width 1`] = ` { "height": 450, "images": [ { "height": 450, "path": "foobar/282d046d550fa19c-500.jpg", "width": 500, }, ], "src": "foobar/282d046d550fa19c-500.jpg", "srcSet": "foobar/282d046d550fa19c-500.jpg 500w", "toString": [Function], "width": 500, } `; exports[`output should be in outputPath dir 1`] = ` { "height": 900, "images": [ { "height": 450, "path": "foobar/img/282d046d550fa19c-500.jpg", "width": 500, }, { "height": 675, "path": "foobar/img/b8e6012830c98919-750.jpg", "width": 750, }, { "height": 900, "path": "foobar/img/3cf3c37514578847-1000.jpg", "width": 1000, }, ], "src": "foobar/img/3cf3c37514578847-1000.jpg", "srcSet": "foobar/img/282d046d550fa19c-500.jpg 500w,foobar/img/b8e6012830c98919-750.jpg 750w,foobar/img/3cf3c37514578847-1000.jpg 1000w", "toString": [Function], "width": 1000, } `; exports[`output should be relative to context 1`] = ` { "height": 900, "images": [ { "height": 450, "path": "foobar/test/282d046d550fa19c-500x450.jpg", "width": 500, }, { "height": 675, "path": "foobar/test/b8e6012830c98919-750x675.jpg", "width": 750, }, { "height": 900, "path": "foobar/test/3cf3c37514578847-1000x900.jpg", "width": 1000, }, ], "src": "foobar/test/3cf3c37514578847-1000x900.jpg", "srcSet": "foobar/test/282d046d550fa19c-500x450.jpg 500w,foobar/test/b8e6012830c98919-750x675.jpg 750w,foobar/test/3cf3c37514578847-1000x900.jpg 1000w", "toString": [Function], "width": 1000, } `; exports[`override min and max with size 1`] = ` { "height": 90, "images": [ { "height": 90, "path": "foobar/661fbbbdbe7372a2-100.jpg", "width": 100, }, ], "src": "foobar/661fbbbdbe7372a2-100.jpg", "srcSet": "foobar/661fbbbdbe7372a2-100.jpg 100w", "toString": [Function], "width": 100, } `; exports[`override min and max with sizes 1`] = ` { "height": 180, "images": [ { "height": 90, "path": "foobar/661fbbbdbe7372a2-100.jpg", "width": 100, }, { "height": 180, "path": "foobar/daae2e5f7f65b269-200.jpg", "width": 200, }, ], "src": "foobar/daae2e5f7f65b269-200.jpg", "srcSet": "foobar/661fbbbdbe7372a2-100.jpg 100w,foobar/daae2e5f7f65b269-200.jpg 200w", "toString": [Function], "width": 200, } `; exports[`parses json notation 1`] = ` { "height": 180, "images": [ { "height": 45, "path": "foobar/b58d8935f3e689e3-50.webp", "width": 50, }, { "height": 90, "path": "foobar/fffbb70a2c269cf1-100.webp", "width": 100, }, { "height": 180, "path": "foobar/701bf549670b5294-200.webp", "width": 200, }, ], "src": "foobar/701bf549670b5294-200.webp", "srcSet": "foobar/b58d8935f3e689e3-50.webp 50w,foobar/fffbb70a2c269cf1-100.webp 100w,foobar/701bf549670b5294-200.webp 200w", "toString": [Function], "width": 200, } `; exports[`png 1`] = ` { "height": 595, "images": [ { "height": 580, "path": "foobar/ed6170e9b0b0edf0-500.png", "width": 500, }, { "height": 595, "path": "foobar/418c98c2d061efc6-513.png", "width": 513, }, ], "src": "foobar/418c98c2d061efc6-513.png", "srcSet": "foobar/ed6170e9b0b0edf0-500.png 500w,foobar/418c98c2d061efc6-513.png 513w", "toString": [Function], "width": 513, } `; exports[`png to avif 1`] = ` { "height": 595, "images": [ { "height": 595, "path": "foobar/54f4e72270566ed5-513.avif", "width": 513, }, ], "src": "foobar/54f4e72270566ed5-513.avif", "srcSet": "foobar/54f4e72270566ed5-513.avif 513w", "toString": [Function], "width": 513, } `; exports[`png to avif 2`] = ` { "height": 900, "images": [ { "height": 450, "path": "foobar/99b6b84c23e8a9b4-500.avif", "width": 500, }, { "height": 675, "path": "foobar/d0b22cf68e1f393a-750.avif", "width": 750, }, { "height": 900, "path": "foobar/6f9cba284621e494-1000.avif", "width": 1000, }, ], "src": "foobar/6f9cba284621e494-1000.avif", "srcSet": "foobar/99b6b84c23e8a9b4-500.avif 500w,foobar/d0b22cf68e1f393a-750.avif 750w,foobar/6f9cba284621e494-1000.avif 1000w", "toString": [Function], "width": 1000, } `; exports[`png to jpeg with background color 1`] = ` { "height": 595, "images": [ { "height": 580, "path": "foobar/0be420cbedde023f-500.jpg", "width": 500, }, { "height": 595, "path": "foobar/9e08ba9531804694-513.jpg", "width": 513, }, ], "src": "foobar/9e08ba9531804694-513.jpg", "srcSet": "foobar/0be420cbedde023f-500.jpg 500w,foobar/9e08ba9531804694-513.jpg 513w", "toString": [Function], "width": 513, } `; exports[`png to webp with transparent background 1`] = ` { "height": 595, "images": [ { "height": 580, "path": "foobar/548d85cfef6a6c88-500.webp", "width": 500, }, { "height": 595, "path": "foobar/924e13d10c98fb28-513.webp", "width": 513, }, ], "src": "foobar/924e13d10c98fb28-513.webp", "srcSet": "foobar/548d85cfef6a6c88-500.webp 500w,foobar/924e13d10c98fb28-513.webp 513w", "toString": [Function], "width": 513, } `; exports[`preserves rotation 1`] = ` { "height": 554, "images": [ { "height": 554, "path": "foobar/eb9b881857272a9d-499.jpg", "width": 499, }, ], "src": "foobar/eb9b881857272a9d-499.jpg", "srcSet": "foobar/eb9b881857272a9d-499.jpg 499w", "toString": [Function], "width": 499, } `; exports[`progressive image 1`] = ` { "height": 864, "images": [ { "height": 684, "path": "foobar/e971e26ec2eea1f1-760.jpg", "width": 760, }, { "height": 864, "path": "foobar/e5e8c73f5f931a48-960.jpg", "width": 960, }, ], "src": "foobar/e5e8c73f5f931a48-960.jpg", "srcSet": "foobar/e971e26ec2eea1f1-760.jpg 760w,foobar/e5e8c73f5f931a48-960.jpg 960w", "toString": [Function], "width": 960, } `; exports[`public path should replace global publicPath 1`] = ` { "height": 900, "images": [ { "height": 450, "path": "public/282d046d550fa19c-500.jpg", "width": 500, }, { "height": 675, "path": "public/b8e6012830c98919-750.jpg", "width": 750, }, { "height": 900, "path": "public/3cf3c37514578847-1000.jpg", "width": 1000, }, ], "src": "public/3cf3c37514578847-1000.jpg", "srcSet": "public/282d046d550fa19c-500.jpg 500w,public/b8e6012830c98919-750.jpg 750w,public/3cf3c37514578847-1000.jpg 1000w", "toString": [Function], "width": 1000, } `; exports[`rotates 90 1`] = ` { "height": 666, "images": [ { "height": 666, "path": "foobar/aa309fc022524f4b-599.jpg", "width": 599, }, ], "src": "foobar/aa309fc022524f4b-599.jpg", "srcSet": "foobar/aa309fc022524f4b-599.jpg 599w", "toString": [Function], "width": 599, } `; exports[`single size 1`] = ` { "height": 450, "images": [ { "height": 450, "path": "foobar/282d046d550fa19c-500.jpg", "width": 500, }, ], "src": "foobar/282d046d550fa19c-500.jpg", "srcSet": "foobar/282d046d550fa19c-500.jpg 500w", "toString": [Function], "width": 500, } `; exports[`with min and max sizes 1`] = ` { "height": 900, "images": [ { "height": 450, "path": "foobar/282d046d550fa19c-500.jpg", "width": 500, }, { "height": 675, "path": "foobar/b8e6012830c98919-750.jpg", "width": 750, }, { "height": 900, "path": "foobar/3cf3c37514578847-1000.jpg", "width": 1000, }, ], "src": "foobar/3cf3c37514578847-1000.jpg", "srcSet": "foobar/282d046d550fa19c-500.jpg 500w,foobar/b8e6012830c98919-750.jpg 750w,foobar/3cf3c37514578847-1000.jpg 1000w", "toString": [Function], "width": 1000, } `; exports[`with min and max sizes options 1`] = ` { "height": 270, "images": [ { "height": 90, "path": "foobar/661fbbbdbe7372a2-100.jpg", "width": 100, }, { "height": 150, "path": "foobar/87ddc3296017abd7-167.jpg", "width": 167, }, { "height": 211, "path": "foobar/c792e1e8f501c416-234.jpg", "width": 234, }, { "height": 270, "path": "foobar/5de63dd31e93ce82-300.jpg", "width": 300, }, ], "src": "foobar/5de63dd31e93ce82-300.jpg", "srcSet": "foobar/661fbbbdbe7372a2-100.jpg 100w,foobar/87ddc3296017abd7-167.jpg 167w,foobar/c792e1e8f501c416-234.jpg 234w,foobar/5de63dd31e93ce82-300.jpg 300w", "toString": [Function], "width": 300, } `; exports[`with min and max sizes, and default steps 1`] = ` { "height": 900, "images": [ { "height": 450, "path": "foobar/282d046d550fa19c-500.jpg", "width": 500, }, { "height": 675, "path": "foobar/b8e6012830c98919-750.jpg", "width": 750, }, { "height": 900, "path": "foobar/3cf3c37514578847-1000.jpg", "width": 1000, }, ], "src": "foobar/3cf3c37514578847-1000.jpg", "srcSet": "foobar/282d046d550fa19c-500.jpg 500w,foobar/b8e6012830c98919-750.jpg 750w,foobar/3cf3c37514578847-1000.jpg 1000w", "toString": [Function], "width": 1000, } `; exports[`with placeholder image 1`] = ` { "height": 900, "images": [ { "height": 450, "path": "foobar/282d046d550fa19c-500.jpg", "width": 500, }, { "height": 675, "path": "foobar/b8e6012830c98919-750.jpg", "width": 750, }, { "height": 900, "path": "foobar/3cf3c37514578847-1000.jpg", "width": 1000, }, ], "placeholder": "data:image/jpeg;base64,/9j/2wBDAAUDBAQEAwUEBAQFBQUGBwwIBwcHBw8LCwkMEQ8SEhEPERETFhwXExQaFRERGCEYGh0dHx8fExciJCIeJBweHx7/2wBDAQUFBQcGBw4ICA4eFBEUHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh7/wAARCAAkACgDASIAAhEBAxEB/8QAHAAAAgEFAQAAAAAAAAAAAAAAAAcGAQIDBAgF/8QALBAAAQMDAwIGAgIDAAAAAAAAAQIDBAAFEQYSIQcxCBMiQVFhFDJxgSNSgv/EABgBAAMBAQAAAAAAAAAAAAAAAAABAwIE/8QAHBEAAwEBAAMBAAAAAAAAAAAAAAECEQMEEjEi/9oADAMBAAIRAxEAPwDx734dA/cGU2i4Q4EIq/yqKFlxCfhKckKP2SMVpK8M8tN1YWjVMd6EFEuhyGpLmPYABRB++RXSrzrEaO7JkONtMMpK3HFcJQkDJJPsBSk1N4g9FWuQti2MTL0pPHms4aaJ+lK5P8gVdzK+k02RK+eGW1yGiu2agfiyOf2jhTZ/oHI/o1F3/DprKzSok+0XK3XVxs5eaJLB/wCSrIPHzim1oTrvpfUl4ZtUuDKs8iSdrLjriXGlK9klQA25+SMU02pMN7HkyY7u47RscSrJ+OD3ocRSNTdS00JHRnR2S5KZm6tWx5SCFfgsK3biP919sfQ7/NFPEoBGO/0aKUcOcLEivXyevWtpiT1b0/6h3WxyxqzqGXbfHjrcW00A02vakn14CQRx3Oa5qMMuHc2pLiAAco9QI7c/Fd6zLdDmQnYsxhMhmQkodZc9SFg9wR2NL689JIz+sIOorSiBAMdSQ6x+MPKcSn9SEpwAocd+OB8Vmo34SVYIjpn01vd8XMlNlMF+3Ppa8uQClW7bnGPbgim30i6aTdNa1YvLjzCY6W1LlDzN298hQASMD0jcDn+am19tV6UFoQhvdL4clNN4XuA9JJByOBjJ7Vs9LLfqBnTezUbi3ZKZLnlvqSlKnW8+klI7e4rKj9D9nhLwUr5HA9xiiqeQU5IORRXRpMyoGVE5NWuZSeCaKKGBayoqCyrBrMrhIGe9FFCAByM+570UUUwP/9k=", "src": "foobar/3cf3c37514578847-1000.jpg", "srcSet": "foobar/282d046d550fa19c-500.jpg 500w,foobar/b8e6012830c98919-750.jpg 750w,foobar/3cf3c37514578847-1000.jpg 1000w", "toString": [Function], "width": 1000, } `; exports[`with placeholder image on image with size 1`] = ` { "height": 360, "images": [ { "height": 360, "path": "foobar/c985fe28cb56f01d-400.jpg", "width": 400, }, ], "placeholder": "data:image/jpeg;base64,/9j/2wBDAAUDBAQEAwUEBAQFBQUGBwwIBwcHBw8LCwkMEQ8SEhEPERETFhwXExQaFRERGCEYGh0dHx8fExciJCIeJBweHx7/2wBDAQUFBQcGBw4ICA4eFBEUHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh7/wAARCAAkACgDASIAAhEBAxEB/8QAHAAAAgEFAQAAAAAAAAAAAAAAAAcGAQIDBAgF/8QALBAAAQMDAwIGAgIDAAAAAAAAAQIDBAAFEQYSIQcxCBMiQVFhFDJxgSNSgv/EABgBAAMBAQAAAAAAAAAAAAAAAAABAwIE/8QAHBEAAwEBAAMBAAAAAAAAAAAAAAECEQMEEjEi/9oADAMBAAIRAxEAPwDx734dA/cGU2i4Q4EIq/yqKFlxCfhKckKP2SMVpK8M8tN1YWjVMd6EFEuhyGpLmPYABRB++RXSrzrEaO7JkONtMMpK3HFcJQkDJJPsBSk1N4g9FWuQti2MTL0pPHms4aaJ+lK5P8gVdzK+k02RK+eGW1yGiu2agfiyOf2jhTZ/oHI/o1F3/DprKzSok+0XK3XVxs5eaJLB/wCSrIPHzim1oTrvpfUl4ZtUuDKs8iSdrLjriXGlK9klQA25+SMU02pMN7HkyY7u47RscSrJ+OD3ocRSNTdS00JHRnR2S5KZm6tWx5SCFfgsK3biP919sfQ7/NFPEoBGO/0aKUcOcLEivXyevWtpiT1b0/6h3WxyxqzqGXbfHjrcW00A02vakn14CQRx3Oa5qMMuHc2pLiAAco9QI7c/Fd6zLdDmQnYsxhMhmQkodZc9SFg9wR2NL689JIz+sIOorSiBAMdSQ6x+MPKcSn9SEpwAocd+OB8Vmo34SVYIjpn01vd8XMlNlMF+3Ppa8uQClW7bnGPbgim30i6aTdNa1YvLjzCY6W1LlDzN298hQASMD0jcDn+am19tV6UFoQhvdL4clNN4XuA9JJByOBjJ7Vs9LLfqBnTezUbi3ZKZLnlvqSlKnW8+klI7e4rKj9D9nhLwUr5HA9xiiqeQU5IORRXRpMyoGVE5NWuZSeCaKKGBayoqCyrBrMrhIGe9FFCAByM+570UUUwP/9k=", "src": "foobar/c985fe28cb56f01d-400.jpg", "srcSet": "foobar/c985fe28cb56f01d-400.jpg 400w", "toString": [Function], "width": 400, } `; exports[`with size defined in webpack.config.js 1`] = ` { "height": 900, "images": [ { "height": 450, "path": "foobar/282d046d550fa19c-500.jpg", "width": 500, }, { "height": 675, "path": "foobar/b8e6012830c98919-750.jpg", "width": 750, }, { "height": 900, "path": "foobar/3cf3c37514578847-1000.jpg", "width": 1000, }, ], "src": "foobar/3cf3c37514578847-1000.jpg", "srcSet": "foobar/282d046d550fa19c-500.jpg 500w,foobar/b8e6012830c98919-750.jpg 750w,foobar/3cf3c37514578847-1000.jpg 1000w", "toString": [Function], "width": 1000, } `; ================================================ FILE: test/sharp/index.js ================================================ test('Works with the cache', () => { const output = require('../cat-1000.jpg?size=501,cacheDirectory') expect(output).toMatchSnapshot() }) test('creates jpeg extension', () => { const output = require('../cat-1000copy.jpeg?size=439') expect(output).toMatchSnapshot() }) test('png to avif', () => { const output = require('../cat-transparent.png?format=avif&size=777') expect(output).toMatchSnapshot() }) test('png to avif', () => { const output = require('../cat-1000.jpg?format=avif') expect(output).toMatchSnapshot() }) test('preserves rotation', () => { const single = require('../cat-rotated-1000.jpg?size=499') expect(single).toMatchSnapshot() }) test('rotates 90', () => { const single = require('../cat-1000.jpg?size=599&rotate=90') expect(single).toMatchSnapshot() }) test('progressive image', () => { const multi = require('../cat-1000.jpg?sizes[]=760&sizes[]=960&progressive=true') expect(multi).toMatchSnapshot() }) test('multiple sizes', () => { const multi = require('../cat-1000.jpg?sizes[]=500&sizes[]=2000') expect(multi).toMatchSnapshot() expect(multi.toString()).toBe(multi.src) }) test('parses json notation', () => { const multi = require('../cat-1000.jpg?{sizes:[50,100,200], format: "webp"}') expect(multi).toMatchSnapshot() }) test('single size', () => { const single = require('../cat-1000.jpg?size=500') expect(single).toMatchSnapshot() }) test('with size defined in webpack.config.js', () => { const multi = require('../cat-1000.jpg') expect(multi).toMatchSnapshot() }) test('disable', () => { const multi = require('../cat-1000.jpg?disable') expect(multi).toMatchSnapshot() }) test('output should be relative to context', () => { const multi = require('../cat-1000.jpg?name=[path][hash]-[width]x[height].[ext]&context=./') expect(multi).toMatchSnapshot() }) test('hash lenght output should be relative to context', () => { const multi = require('../cat-1000.jpg?name=[path][contenthash:8]-[width].[ext]&context=./') expect(multi).toMatchSnapshot() }) test('output should be in outputPath dir', () => { const multi = require('../cat-1000.jpg?outputPath=img/') expect(multi).toMatchSnapshot() }) test('public path should replace global publicPath', () => { const multi = require('../cat-1000.jpg?outputPath=img/&publicPath=public/') expect(multi).toMatchSnapshot() }) test('with placeholder image', () => { const output = require('../cat-1000.jpg?placeholder=true') expect(output).toMatchSnapshot() }) test('with placeholder image on image with size', () => { const output = require('../cat-1000.jpg?size=400&placeholder=true') expect(output).toMatchSnapshot() }) test('output first resized image height & width', () => { const output = require('../cat-1000.jpg?size=500') expect(output).toMatchSnapshot() }) test('png', () => { const output = require('../cat-transparent.png') expect(output).toMatchSnapshot() }) test('png to jpeg with background color', () => { const output = require('../cat-transparent.png?background=%23FF0000&format=jpg') expect(output).toMatchSnapshot() }) test('with min and max sizes', () => { const output = require('../cat-1000.jpg?min=600&max=800&steps=3') expect(output).toMatchSnapshot() }) test('with min and max sizes, and default steps', () => { const output = require('../cat-1000.jpg?min=500&max=1000') expect(output).toMatchSnapshot() }) test('with min and max sizes options', () => { const output = require('../cat-1000.jpg?minmax') expect(output).toMatchSnapshot() }) test('override min and max with sizes', () => { const output = require('../cat-1000.jpg?minmax&sizes[]=100&sizes[]=200') expect(output).toMatchSnapshot() }) test('override min and max with size', () => { const output = require('../cat-1000.jpg?minmax&size=100') expect(output).toMatchSnapshot() }) test('jpg to webp', () => { const output = require('../cat-1000.jpg?format=webp') expect(output).toMatchSnapshot() }) test('png to webp with transparent background', () => { const output = require('../cat-transparent.png?format=webp') expect(output).toMatchSnapshot() }) test("doesn't emit file", () => { const multi = require('../cat-1000.jpg?emitFile=false&sizes[]=250') expect(multi).toMatchSnapshot() }) ================================================ FILE: test/sharp/webpack.config.js ================================================ const path = require('path') module.exports = { mode: 'development', entry: path.resolve(__dirname, 'index'), module: { rules: [ // This rule will be matched when the resourceQuery contains `minmax`, e.g. `cat-1000.jpg?minmax` { test: /\.(png|jpe?g)$/, resourceQuery: /minmax/, use: [ { loader: require.resolve('../../lib/index'), options: { min: 100, max: 300, }, }, ], type: 'javascript/auto', }, { test: /\.(png|jpe?g)$/, use: [ { loader: require.resolve('../../lib/index'), options: { sizes: [500, 750, 1000], }, }, ], type: 'javascript/auto', }, ], }, output: { path: path.resolve(__dirname, 'build'), publicPath: 'foobar/', filename: 'test.js', }, target: 'node', } ================================================ FILE: test/utils.test.js ================================================ import { getOutputAndPublicPath } from '../lib/utils' describe('Utils package', () => { it('should create both paths respecting absolutes', () => { const { outputPath, publicPath } = getOutputAndPublicPath('file.png', { outputPath: '/dist/img/', publicPath: '/public', }) expect(outputPath).toBe('/dist/img/file.png') expect(publicPath).toBe('"/public/file.png"') }) it('should create both paths ', () => { const { outputPath, publicPath } = getOutputAndPublicPath('file.png', { outputPath: 'dist/img/', publicPath: 'public/', }) expect(outputPath).toBe('dist/img/file.png') expect(publicPath).toBe('"public/file.png"') }) it('https:// slashes are kept on public path', () => { const { outputPath, publicPath } = getOutputAndPublicPath('file.png', { outputPath: 'dist/img/', publicPath: 'https://example.com/public/', }) expect(publicPath).toBe('"https://example.com/public/file.png"') }) }) ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { /* Visit https://aka.ms/tsconfig.json to read more about this file */ // node 10 https://github.com/microsoft/TypeScript/wiki/Node-Target-Mapping "target": "es2018" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, "lib": ["es2018"], "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, "outDir": "lib", "allowJs": true, /* Strict Type-Checking Options */ "strict": true /* Enable all strict type-checking options. */, "moduleResolution": "node", "noUnusedLocals": true, "noUnusedParameters": true, /* Additional Checks */ "resolveJsonModule": true }, "include": ["src/**/*"], }