diff --git a/.cspell.json b/.cspell.json index a98ce80bb5f3..525114e723ad 100644 --- a/.cspell.json +++ b/.cspell.json @@ -31,6 +31,8 @@ "website/docusaurus.config.localized.json", "*.xyz", "*.docx", + "*.webp", + "*.avif", "versioned_docs", "*.min.*", "jest/vendor" diff --git a/packages/docusaurus-plugin-ideal-image/src/deps.d.ts b/packages/docusaurus-plugin-ideal-image/src/deps.d.ts index f5a6da75b378..deb55991a5c0 100644 --- a/packages/docusaurus-plugin-ideal-image/src/deps.d.ts +++ b/packages/docusaurus-plugin-ideal-image/src/deps.d.ts @@ -37,7 +37,7 @@ declare module '@endiliey/react-ideal-image' { width: number; src?: string; size?: number; - format?: 'webp' | 'jpeg' | 'png' | 'gif'; + format?: 'webp' | 'jpeg' | 'png' | 'gif' | 'avif'; }; type ThemeKey = 'placeholder' | 'img' | 'icon' | 'noscript'; diff --git a/packages/docusaurus-plugin-ideal-image/src/index.ts b/packages/docusaurus-plugin-ideal-image/src/index.ts index 4b8862171859..56a8a614991c 100644 --- a/packages/docusaurus-plugin-ideal-image/src/index.ts +++ b/packages/docusaurus-plugin-ideal-image/src/index.ts @@ -46,6 +46,11 @@ export default function pluginIdealImage( return {}; } + const rulesRegex = new RegExp( + `\\.(?:png|jpe?g${options.enableWebpAvif ? '|webp|avif' : ''})$`, + 'i', + ); + return { mergeStrategy: { 'module.rules': 'prepend', @@ -53,7 +58,7 @@ export default function pluginIdealImage( module: { rules: [ { - test: /\.(?:png|jpe?g)$/i, + test: rulesRegex, use: [ require.resolve('@docusaurus/lqip-loader'), { diff --git a/packages/docusaurus-plugin-ideal-image/src/plugin-ideal-image.d.ts b/packages/docusaurus-plugin-ideal-image/src/plugin-ideal-image.d.ts index 0c151f88cbf9..b7b63a43f918 100644 --- a/packages/docusaurus-plugin-ideal-image/src/plugin-ideal-image.d.ts +++ b/packages/docusaurus-plugin-ideal-image/src/plugin-ideal-image.d.ts @@ -46,17 +46,24 @@ declare module '@docusaurus/plugin-ideal-image' { * Tip: use network throttling in your browser to simulate slow networks. */ disableInDev?: boolean; + /** + * You can enable this plugin for WebP/AVIF images + * by setting this to `true`. + * Note: the default is `false` to keep backward compatibility. + */ + enableWebpAvif?: boolean; }; } declare module '@theme/IdealImage' { import type {ComponentProps} from 'react'; + import type {ImageWithLqip} from '@docusaurus/lqip-loader'; export type SrcType = { width: number; path?: string; size?: number; - format?: 'webp' | 'jpeg' | 'png' | 'gif'; + format?: 'webp' | 'jpeg' | 'png' | 'gif' | 'avif'; }; export type SrcImage = { @@ -67,8 +74,11 @@ declare module '@theme/IdealImage' { images: SrcType[]; }; + export type IdealImageEnabledSrc = ImageWithLqip; + export type IdealImageSrc = IdealImageEnabledSrc | {default: string} | string; + export interface Props extends ComponentProps<'img'> { - readonly img: {default: string} | {src: SrcImage; preSrc: string} | string; + readonly img: IdealImageSrc; } export default function IdealImage(props: Props): JSX.Element; } diff --git a/packages/lqip-loader/src/__tests__/__fixtures__/docusaurus.avif b/packages/lqip-loader/src/__tests__/__fixtures__/docusaurus.avif new file mode 100644 index 000000000000..9be208e0b115 Binary files /dev/null and b/packages/lqip-loader/src/__tests__/__fixtures__/docusaurus.avif differ diff --git a/packages/lqip-loader/src/__tests__/__fixtures__/docusaurus.png b/packages/lqip-loader/src/__tests__/__fixtures__/docusaurus.png new file mode 100644 index 000000000000..f458149e3c8f Binary files /dev/null and b/packages/lqip-loader/src/__tests__/__fixtures__/docusaurus.png differ diff --git a/packages/lqip-loader/src/__tests__/__fixtures__/docusaurus.webp b/packages/lqip-loader/src/__tests__/__fixtures__/docusaurus.webp new file mode 100644 index 000000000000..f4cb121e0807 Binary files /dev/null and b/packages/lqip-loader/src/__tests__/__fixtures__/docusaurus.webp differ diff --git a/packages/lqip-loader/src/__tests__/lqip.test.ts b/packages/lqip-loader/src/__tests__/lqip.test.ts index a433f0f0459c..068f382d693e 100644 --- a/packages/lqip-loader/src/__tests__/lqip.test.ts +++ b/packages/lqip-loader/src/__tests__/lqip.test.ts @@ -8,8 +8,10 @@ import path from 'path'; import {base64} from '../lqip'; -const imgPath = path.join(__dirname, '__fixtures__', 'endi.jpg'); -const invalidPath = path.join(__dirname, '__fixtures__', 'docusaurus.svg'); +const resolveFixturePath = (name: string) => + path.join(__dirname, '__fixtures__', name); + +const invalidPath = resolveFixturePath('docusaurus.svg'); describe('base64', () => { it('rejects unknown or unsupported file format', async () => { @@ -18,8 +20,38 @@ describe('base64', () => { ); }); - it('generates a valid base64', async () => { - const expectedBase64 = 'data:image/jpeg;base64,/9j/2wBDA'; - await expect(base64(imgPath)).resolves.toContain(expectedBase64); - }); + it.each([ + ['endi.jpg', 'data:image/jpeg;base64,/9j/2wBDA'], + // PNG's magic number (common to all PNGs) + ['docusaurus.png', 'data:image/png;base64,iVBORw0KGgoA'], + [ + 'docusaurus.avif', + // cspell:disable-next-line + // AVIF's signature: \0\0\0\x1cftypavif\0\0\0\0avifmif1miaf + 'data:image/avif;base64,AAAAHGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZ', + ], + ] as [string, string][])( + 'generates a valid base64 for %s', + async (imgName, expectedBase64) => { + await expect(base64(resolveFixturePath(imgName))).resolves.toContain( + expectedBase64, + ); + }, + ); + + it.each([ + [ + 'docusaurus.webp', + // WebP's magic number; expects size is less than 64kiB + // cspell:disable-next-line + /^data:image\/webp;base64,UklGR...AABXRUJQ/, + ], + ] as [string, RegExp][])( + 'generates a valid base64 for %s (using regexp)', + async (imgName, expectedBase64) => { + await expect(base64(resolveFixturePath(imgName))).resolves.toMatch( + expectedBase64, + ); + }, + ); }); diff --git a/packages/lqip-loader/src/index.ts b/packages/lqip-loader/src/index.ts index 66d828fffb80..b8b9c373cd81 100644 --- a/packages/lqip-loader/src/index.ts +++ b/packages/lqip-loader/src/index.ts @@ -13,6 +13,11 @@ type Options = { palette: boolean; }; +export type ImageWithLqip = { + preSrc: string; + src: Source; +}; + export default async function lqipLoader( this: LoaderContext, contentBuffer: Buffer, diff --git a/packages/lqip-loader/src/lqip.ts b/packages/lqip-loader/src/lqip.ts index 929e53ba6a9e..40387cb51b04 100644 --- a/packages/lqip-loader/src/lqip.ts +++ b/packages/lqip-loader/src/lqip.ts @@ -18,6 +18,8 @@ const SUPPORTED_MIMES: {[ext: string]: string} = { jpeg: 'image/jpeg', jpg: 'image/jpeg', png: 'image/png', + webp: 'image/webp', + avif: 'image/avif', }; /** diff --git a/website/_dogfooding/_docs tests/tests/img-tests.mdx b/website/_dogfooding/_docs tests/tests/img-tests.mdx index bddaf876340e..df7d9ee0221e 100644 --- a/website/_dogfooding/_docs tests/tests/img-tests.mdx +++ b/website/_dogfooding/_docs tests/tests/img-tests.mdx @@ -10,6 +10,10 @@ import docusaurusImport from '@site/static/img/docusaurus.png'; export const docusaurusRequire = require('@site/static/img/docusaurus.png'); +import docusaurusWebPImport from './img/docusaurus.webp'; + +import docusaurusAVIFImport from './img/docusaurus.avif'; + ![URL encoded image](./img/oss_logo%20%282%29.png) ## Regular images @@ -20,6 +24,16 @@ export const docusaurusRequire = require('@site/static/img/docusaurus.png'); ## Ideal images +PNG + + +WebP + + + +AVIF + + diff --git a/website/_dogfooding/_docs tests/tests/img/docusaurus.avif b/website/_dogfooding/_docs tests/tests/img/docusaurus.avif new file mode 100644 index 000000000000..9be208e0b115 Binary files /dev/null and b/website/_dogfooding/_docs tests/tests/img/docusaurus.avif differ diff --git a/website/_dogfooding/_docs tests/tests/img/docusaurus.webp b/website/_dogfooding/_docs tests/tests/img/docusaurus.webp new file mode 100644 index 000000000000..f4cb121e0807 Binary files /dev/null and b/website/_dogfooding/_docs tests/tests/img/docusaurus.webp differ diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index 156ab21d68d6..802dfc730665 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -238,6 +238,8 @@ module.exports = async function createConfigAsync() { max: 1030, min: 640, steps: 2, + // For ideal-image-plugin test + enableWebpAvif: true, // Use false to debug, but it incurs huge perf costs disableInDev: true, }),