diff --git a/package.json b/package.json index fe0c9dfa5033..82d4dc77ce79 100644 --- a/package.json +++ b/package.json @@ -79,16 +79,13 @@ "lint-staged": { "*.{js,jsx}": [ "yarn lint --fix", - "yarn prettier", - "git add" + "yarn prettier" ], "*.{ts,tsx}": [ - "yarn prettier", - "git add" + "yarn prettier" ], "*.md": [ - "yarn prettier-docs", - "git add" + "yarn prettier-docs" ] }, "husky": { diff --git a/packages/docusaurus-plugin-ideal-image/package.json b/packages/docusaurus-plugin-ideal-image/package.json index bb78aaf99211..34079dfa1fea 100644 --- a/packages/docusaurus-plugin-ideal-image/package.json +++ b/packages/docusaurus-plugin-ideal-image/package.json @@ -15,11 +15,11 @@ "fs-extra": "^9.0.0" }, "dependencies": { - "@endiliey/lqip-loader": "^3.0.2", + "@docusaurus/lqip-loader": "^2.0.0-alpha.50", "@endiliey/react-ideal-image": "^0.0.11", "@endiliey/responsive-loader": "^1.3.2", "react-waypoint": "^9.0.2", - "sharp": "^0.22.1" + "sharp": "^0.25.2" }, "peerDependencies": { "@docusaurus/core": "^2.0.0", diff --git a/packages/docusaurus-plugin-ideal-image/src/index.ts b/packages/docusaurus-plugin-ideal-image/src/index.ts index 3d67eb66f0ba..ecc5ecba2328 100644 --- a/packages/docusaurus-plugin-ideal-image/src/index.ts +++ b/packages/docusaurus-plugin-ideal-image/src/index.ts @@ -27,7 +27,7 @@ export default function (_context: LoadContext, options: PluginOptions) { { test: /\.(png|jpe?g|gif)$/i, use: [ - '@endiliey/lqip-loader', + '@docusaurus/lqip-loader', { loader: '@endiliey/responsive-loader', options: { diff --git a/packages/lqip-loader/README.md b/packages/lqip-loader/README.md new file mode 100644 index 000000000000..6369809b19b5 --- /dev/null +++ b/packages/lqip-loader/README.md @@ -0,0 +1,89 @@ +## lqip-loader: low quality images placeholders for webpack + +### Installation + +``` +npm install --save-dev @docusaurus/lqip-loader +``` + +### Example + +Generating Base64 & dominant colours palette for a jpeg image imported in your JS bundle: + +> The large image file will be emitted & only 400byte of Base64 (if set to true in the loader options) will be bundled. + +`webpack.config.js` + +```json +{ + /** + * OPTION A: + * default file-loader fallback + **/ + test: /\.jpe?g$/, + loaders: [ + { + loader: '@docusaurus/lqip-loader', + options: { + path: '/media', // your image going to be in media folder in the output dir + name: '[name].[ext]', // you can use [hash].[ext] too if you wish, + base64: true, // default: true, gives the base64 encoded image + palette: true // default: false, gives the dominant colours palette + } + } + ] + + /** + * OPTION B: + * Chained with your own url-loader or file-loader + **/ + test: /\.(png|jpe?g)$/, + loaders: [ + { + loader: '@docusaurus/lqip-loader', + options: { + base64: true, + palette: false + } + }, + { + loader: 'url-loader', + options: { + limit: 8000 + } + } + ] +} +``` + +`your-app-module.js` + +```js +import banner from './images/banner.jpg'; + +console.log(banner.preSrc); +// outputs: "data:image/jpeg;base64,/9j/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhY.... + +// the object will have palette property, array will be sorted from most dominant colour to the least +console.log(banner.palette); // [ '#628792', '#bed4d5', '#5d4340', '#ba454d', '#c5dce4', '#551f24' ] + +console.log(banner.src); // that's the original image URL to load later! +``` + +### Important note + +To save memory and improve GPU performance, browsers (including Chrome started from 61.0.3163.38) will now render a slightly more crisp or pixelated Base64 encoded images. If you want the blur to be very intense (smooth), here's a fix! + +```css +img { + filter: blur(25px); +} +``` + +More history about the issue can be [found here](https://bugs.chromium.org/p/chromium/issues/detail?id=771110#c3) and [here](https://groups.google.com/a/chromium.org/forum/#!topic/blink-dev/6L_3ZZeuA0M). + +Alternatively, you can fill the container with a really cheap colour or gradient from the amazing palette we provide. + +### Credits + +This package has been imported from [`@endiliey/lqip-loader`](https://github.com/endiliey/lqip-loader) which was a fork of original [`lqip-loader`](https://github.com/zouhir/lqip-loader) created exclusively for Docusaurus. diff --git a/packages/lqip-loader/package.json b/packages/lqip-loader/package.json new file mode 100644 index 000000000000..ac5189dde1cd --- /dev/null +++ b/packages/lqip-loader/package.json @@ -0,0 +1,22 @@ +{ + "name": "@docusaurus/lqip-loader", + "version": "2.0.0-alpha.50", + "description": "Low Quality Image Placeholders (LQIP) loader for webpack", + "main": "src/index.js", + "publishConfig": { + "access": "public" + }, + "license": "MIT", + "dependencies": { + "loader-utils": "^1.2.3", + "lodash.sortby": "^4.7.0", + "node-vibrant": "^3.1.5" + }, + "peerDependencies": { + "file-loader": "*", + "sharp": "*" + }, + "engines": { + "node": ">=10.9.0" + } +} diff --git a/packages/lqip-loader/src/index.js b/packages/lqip-loader/src/index.js new file mode 100644 index 000000000000..f1fd4f5025ba --- /dev/null +++ b/packages/lqip-loader/src/index.js @@ -0,0 +1,77 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +const loaderUtils = require('loader-utils'); +const lqip = require('./lqip'); + +module.exports = function (contentBuffer) { + if (this.cacheable) { + this.cacheable(); + } + const callback = this.async(); + const imgPath = this.resourcePath; + + const config = loaderUtils.getOptions(this) || {}; + config.base64 = 'base64' in config ? config.base64 : true; + config.palette = 'palette' in config ? config.palette : false; + + let content = contentBuffer.toString('utf8'); + const contentIsUrlExport = /^module.exports = "data:(.*)base64,(.*)/.test( + content, + ); + const contentIsFileExport = /^module.exports = (.*)/.test(content); + + let source = ''; + const SOURCE_CHUNK = 1; + + if (contentIsUrlExport) { + source = content.match(/^module.exports = (.*)/)[SOURCE_CHUNK]; + } else { + if (!contentIsFileExport) { + // eslint-disable-next-line global-require + const fileLoader = require('file-loader'); + content = fileLoader.call(this, contentBuffer); + } + source = content.match(/^module.exports = (.*);/)[SOURCE_CHUNK]; + } + + const outputPromises = []; + + if (config.base64 === true) { + outputPromises.push(lqip.base64(imgPath)); + } else { + outputPromises.push(null); + } + + // color palette generation is set to false by default + // since it is little bit slower than base64 generation + + if (config.palette === true) { + outputPromises.push(lqip.palette(imgPath)); + } else { + outputPromises.push(null); + } + + Promise.all(outputPromises) + .then((data) => { + if (data) { + const [preSrc, palette] = data; + const param1 = preSrc ? `, "preSrc": ${JSON.stringify(preSrc)}` : ''; + const param2 = palette ? `, "palette": ${JSON.stringify(palette)}` : ''; + const result = `module.exports = {"src":${source}${param1}${param2}};`; + callback(null, result); + } else { + callback('ERROR', null); + } + }) + .catch((error) => { + console.error(error); + callback(error, null); + }); +}; + +module.exports.raw = true; diff --git a/packages/lqip-loader/src/lqip.js b/packages/lqip-loader/src/lqip.js new file mode 100644 index 000000000000..7c32e0c3d18e --- /dev/null +++ b/packages/lqip-loader/src/lqip.js @@ -0,0 +1,75 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +const Vibrant = require('node-vibrant'); +const path = require('path'); +const sharp = require('sharp'); + +const {version} = require('../package.json'); +const {toPalette, toBase64} = require('./utils'); + +const ERROR_EXT = `Error: Input file is missing or uses unsupported image format, lqip v${version}`; + +const SUPPORTED_MIMES = { + jpeg: 'image/jpeg', + jpg: 'image/jpeg', + png: 'image/png', +}; + +const base64 = (file) => { + return new Promise((resolve, reject) => { + let extension = path.extname(file) || ''; + extension = extension.split('.').pop(); + + if (!SUPPORTED_MIMES[extension]) { + return reject(ERROR_EXT); + } + + return sharp(file) + .resize(10) + .toBuffer() + .then((data) => { + if (data) { + return resolve(toBase64(SUPPORTED_MIMES[extension], data)); + } + return reject( + new Error('Unhandled promise rejection in base64 promise'), + ); + }) + .catch((err) => { + return reject(err); + }); + }); +}; + +const palette = (file) => { + return new Promise((resolve, reject) => { + const vibrant = new Vibrant(file, {}); + vibrant + .getPalette() + .then((pal) => { + if (pal) { + return resolve(toPalette(pal)); + } + return reject( + new Error('Unhandled promise rejection in colorPalette', pal), + ); + }) + .catch((err) => { + return reject(err); + }); + }); +}; + +process.on('unhandledRejection', (up) => { + throw up; +}); + +module.exports = { + base64, + palette, +}; diff --git a/packages/lqip-loader/src/utils.js b/packages/lqip-loader/src/utils.js new file mode 100644 index 000000000000..d622e8e67dd2 --- /dev/null +++ b/packages/lqip-loader/src/utils.js @@ -0,0 +1,49 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +const sortBy = require('lodash.sortby'); + +/** + * toBase64 + * @description it returns a Base64 image string with required formatting + * to work on the web ( or in CSS url('..')) + * + * @param extension: image file extension + * @param data: base64 string + * @returns {string} + */ +const toBase64 = (extMimeType, data) => { + return `data:${extMimeType};base64,${data.toString('base64')}`; +}; + +/** + * toPalette + * @description takes a color swatch object, converts it to an array & returns + * only hex color + * + * @param swatch + * @returns {{palette: Array}} + */ +const toPalette = (swatch) => { + let palette = Object.keys(swatch).reduce((result, key) => { + if (swatch[key] !== null) { + result.push({ + popularity: swatch[key].getPopulation(), + hex: swatch[key].getHex(), + }); + } + return result; + }, []); + palette = sortBy(palette, ['popularity']); + palette = palette.map((color) => color.hex).reverse(); + return palette; +}; + +module.exports = { + toBase64, + toPalette, +}; diff --git a/yarn.lock b/yarn.lock index 36a02a0f3b10..74e2338a7cd3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1057,16 +1057,6 @@ resolved "https://registry.yarnpkg.com/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7" integrity sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw== -"@endiliey/lqip-loader@^3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@endiliey/lqip-loader/-/lqip-loader-3.0.2.tgz#00f4aebe7d4205b741f913644dee831a689f4fcc" - integrity sha512-Kx8te/ZrXR1EqNxBn4hfBHlVCCovm8Fu1fTpYjLSIvcGSEC2+OYFgT7dwPzvh7HyADhMl3lizOgtWbDhtM5djA== - dependencies: - loader-utils "^1.2.3" - lodash.sortby "^4.7.0" - node-vibrant "^3.1.4" - sharp "^0.22.1" - "@endiliey/react-ideal-image@^0.0.11": version "0.0.11" resolved "https://registry.yarnpkg.com/@endiliey/react-ideal-image/-/react-ideal-image-0.0.11.tgz#dc3803d04e1409cf88efa4bba0f67667807bdf27" @@ -4865,7 +4855,7 @@ color-string@^1.5.2: color-name "^1.0.0" simple-swizzle "^0.2.2" -color@^3.0.0, color@^3.1.1, color@^3.1.2: +color@^3.0.0, color@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/color/-/color-3.1.2.tgz#68148e7f85d41ad7649c5fa8c8106f098d229e10" integrity sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg== @@ -7431,11 +7421,6 @@ fs-constants@^1.0.0: resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== -fs-copy-file-sync@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/fs-copy-file-sync/-/fs-copy-file-sync-1.1.1.tgz#11bf32c096c10d126e5f6b36d06eece776062918" - integrity sha512-2QY5eeqVv4m2PfyMiEuy9adxNP+ajf+8AR05cEi+OAzPcOj90hvFImeZhTmKLBgSd9EvG33jsD7ZRxsx9dThkQ== - fs-extra@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" @@ -11430,7 +11415,7 @@ mz@^2.4.0, mz@^2.5.0: object-assign "^4.0.1" thenify-all "^1.0.0" -nan@^2.12.1, nan@^2.13.2: +nan@^2.12.1: version "2.14.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== @@ -11510,6 +11495,11 @@ node-abi@^2.7.0: dependencies: semver "^5.4.1" +node-addon-api@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.0.tgz#f9afb8d777a91525244b01775ea0ddbe1125483b" + integrity sha512-ASCL5U13as7HhOExbT6OlWJJUV/lLzL2voOSP1UVehpRD8FbSrSDjfScK/KwAvVTI5AS6r4VwbOMlIqtvRidnA== + node-emoji@^1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.10.0.tgz#8886abd25d9c7bb61802a658523d1f8d2a89b2da" @@ -11630,7 +11620,7 @@ node-releases@^1.1.53: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.53.tgz#2d821bfa499ed7c5dffc5e2f28c88e78a08ee3f4" integrity sha512-wp8zyQVwef2hpZ/dJH7SfSrIPD6YoJz6BDQDpGEkcA0s3LpAQoxBIYmfIq6QAhC1DhwsyCgTaTTcONwX8qzCuQ== -node-vibrant@^3.1.4: +node-vibrant@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/node-vibrant/-/node-vibrant-3.1.5.tgz#8729bf35aabd54cd2eccbfadf22124ab4e1305b0" integrity sha512-Gk+iyBzPSN1SF5qL818QaBtuA38206Z8iPNa0PcLUPyIbZL4+i14VmYxkGCL0n/5Q1721CRSktqtACgkx7Qodg== @@ -13423,7 +13413,7 @@ postcss@^7.0.1, postcss@^7.0.16, postcss@^7.0.17, postcss@^7.0.23, postcss@^7.0. source-map "^0.6.1" supports-color "^6.1.0" -prebuild-install@^5.3.0: +prebuild-install@^5.3.3: version "5.3.3" resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-5.3.3.tgz#ef4052baac60d465f5ba6bf003c9c1de79b9da8e" integrity sha512-GV+nsUXuPW2p8Zy7SarF/2W/oiK8bFQgJcncoJ0d7kRpekEA0ftChjfEaF9/Y+QJEc/wFR7RAEa8lYByuUIe2g== @@ -15057,6 +15047,11 @@ semver@^6.0.0, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^7.1.3: + version "7.1.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.1.3.tgz#e4345ce73071c53f336445cfc19efb1c311df2a6" + integrity sha512-ekM0zfiA9SCBlsKa2X1hxyxiI4L3B6EbVJkkdgQXnSEEaHlGdvyodMruTiulSRWMMB4NeIuYNMC9rTKTz97GxA== + send@0.17.1: version "0.17.1" resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" @@ -15161,20 +15156,19 @@ shallowequal@^1.0.1: resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== -sharp@^0.22.1: - version "0.22.1" - resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.22.1.tgz#a67c0e75567f03dd5a7861b901fec04072c5b0f4" - integrity sha512-lXzSk/FL5b/MpWrT1pQZneKe25stVjEbl6uhhJcTULm7PhmJgKKRbTDM/vtjyUuC/RLqL2PRyC4rpKwbv3soEw== +sharp@^0.25.2: + version "0.25.2" + resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.25.2.tgz#f9003d73be50e9265e98f79f04fe53d8c66a3967" + integrity sha512-l1GN0kFNtJr3U9i9pt7a+vo2Ij0xv4tTKDIPx8W6G9WELhPwrMyZZJKAAQNBSI785XB4uZfS5Wpz8C9jWV4AFQ== dependencies: - color "^3.1.1" + color "^3.1.2" detect-libc "^1.0.3" - fs-copy-file-sync "^1.1.1" - nan "^2.13.2" + node-addon-api "^2.0.0" npmlog "^4.1.2" - prebuild-install "^5.3.0" - semver "^6.0.0" - simple-get "^3.0.3" - tar "^4.4.8" + prebuild-install "^5.3.3" + semver "^7.1.3" + simple-get "^3.1.0" + tar "^6.0.1" tunnel-agent "^0.6.0" shebang-command@^1.2.0: @@ -15238,7 +15232,7 @@ simple-concat@^1.0.0: resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.0.tgz#7344cbb8b6e26fb27d66b2fc86f9f6d5997521c6" integrity sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY= -simple-get@^3.0.3: +simple-get@^3.0.3, simple-get@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.0.tgz#b45be062435e50d159540b576202ceec40b9c6b3" integrity sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA== @@ -16138,7 +16132,7 @@ tar@^4.4.10, tar@^4.4.12, tar@^4.4.8: safe-buffer "^5.1.2" yallist "^3.0.3" -tar@^6.0.0: +tar@^6.0.0, tar@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/tar/-/tar-6.0.1.tgz#7b3bd6c313cb6e0153770108f8d70ac298607efa" integrity sha512-bKhKrrz2FJJj5s7wynxy/fyxpE0CmCjmOQ1KV4KkgXFWOgoIT/NbTMnB1n+LFNrNk0SSBVGGxcK5AGsyC+pW5Q==