From 089e34084d535fb20ffa1e1c71af9dc25602a2cb Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Mon, 30 Oct 2023 09:42:28 +0100 Subject: [PATCH] Next.js: Add avif support --- code/frameworks/nextjs/README.md | 5 - code/frameworks/nextjs/package.json | 4 +- .../nextjs/src/next-image-loader-stub.ts | 35 ++- .../nextjs/template/stories/Image.stories.jsx | 8 + .../common/assets/avif-test-image.avif | Bin 0 -> 829 bytes code/yarn.lock | 248 +++++++++++++++++- 6 files changed, 281 insertions(+), 19 deletions(-) create mode 100644 code/lib/cli/rendererAssets/common/assets/avif-test-image.avif diff --git a/code/frameworks/nextjs/README.md b/code/frameworks/nextjs/README.md index d2549a159c26..03cc1cbebc62 100644 --- a/code/frameworks/nextjs/README.md +++ b/code/frameworks/nextjs/README.md @@ -14,7 +14,6 @@ - [Next.js's Image Component](#nextjss-image-component) - [Local Images](#local-images) - [Remote Images](#remote-images) - - [AVIF](#avif) - [Next.js Font Optimization](#nextjs-font-optimization) - [next/font/google](#nextfontgoogle) - [next/font/local](#nextfontlocal) @@ -220,10 +219,6 @@ export default function Home() { } ``` -#### AVIF - -This format is not supported by this framework yet. Feel free to [open up an issue](https://github.com/storybookjs/storybook/issues) if this is something you want to see. - ### Next.js Font Optimization [next/font](https://nextjs.org/docs/basic-features/font-optimization) is partially supported in Storybook. The packages `next/font/google` and `next/font/local` are supported. diff --git a/code/frameworks/nextjs/package.json b/code/frameworks/nextjs/package.json index d5ba68b05c9f..5395127a6398 100644 --- a/code/frameworks/nextjs/package.json +++ b/code/frameworks/nextjs/package.json @@ -100,7 +100,7 @@ "find-up": "^5.0.0", "fs-extra": "^11.1.0", "image-size": "^1.0.0", - "loader-utils": "^3.2.0", + "loader-utils": "^3.2.1", "node-polyfill-webpack-plugin": "^2.0.1", "pnp-webpack-plugin": "^1.7.0", "postcss": "^8.4.21", @@ -108,6 +108,7 @@ "resolve-url-loader": "^5.0.0", "sass-loader": "^12.4.0", "semver": "^7.3.5", + "sharp": "^0.32.6", "style-loader": "^3.3.1", "styled-jsx": "5.1.1", "ts-dedent": "^2.0.0", @@ -119,6 +120,7 @@ "@types/babel__core": "^7", "@types/babel__plugin-transform-runtime": "^7", "@types/babel__preset-env": "^7", + "@types/loader-utils": "^2.0.5", "next": "^14.0.0", "typescript": "^4.9.3", "webpack": "^5.65.0" diff --git a/code/frameworks/nextjs/src/next-image-loader-stub.ts b/code/frameworks/nextjs/src/next-image-loader-stub.ts index 04a97fdead81..6f69e8e274d3 100644 --- a/code/frameworks/nextjs/src/next-image-loader-stub.ts +++ b/code/frameworks/nextjs/src/next-image-loader-stub.ts @@ -1,29 +1,52 @@ -// @ts-expect-error (loader-utils has no webpack5 compatible types) import { interpolateName } from 'loader-utils'; import imageSizeOf from 'image-size'; import type { RawLoaderDefinition } from 'webpack'; import type { NextConfig } from 'next'; +import sharp from 'sharp'; +import { cpus } from 'os'; interface LoaderOptions { filename: string; nextConfig: NextConfig; } -const nextImageLoaderStub: RawLoaderDefinition = function (content) { +if (sharp.concurrency() > 1) { + // Reducing concurrency reduces the memory usage too. + const divisor = process.env.NODE_ENV === 'development' ? 4 : 2; + sharp.concurrency(Math.floor(Math.max(cpus().length / divisor, 1))); +} + +const nextImageLoaderStub: RawLoaderDefinition = async function NextImageLoader( + content +) { const { filename, nextConfig } = this.getOptions(); - const outputPath = interpolateName(this, filename.replace('[ext]', '.[ext]'), { + const opts = { context: this.rootContext, content, - }); + }; + const outputPath = interpolateName(this, filename.replace('[ext]', '.[ext]'), opts); + const extension = interpolateName(this, '[ext]', opts); this.emitFile(outputPath, content); - const { width, height } = imageSizeOf(this.resourcePath); - if (nextConfig.images?.disableStaticImages) { return `const src = '${outputPath}'; export default src;`; } + let width; + let height; + + if (extension === 'avif') { + const transformer = sharp(content); + const result = await transformer.metadata(); + width = result.width; + height = result.height; + } else { + const result = imageSizeOf(this.resourcePath); + width = result.width; + height = result.height; + } + return `export default ${JSON.stringify({ src: outputPath, height, diff --git a/code/frameworks/nextjs/template/stories/Image.stories.jsx b/code/frameworks/nextjs/template/stories/Image.stories.jsx index 7a8803a6e992..d0da833485d4 100644 --- a/code/frameworks/nextjs/template/stories/Image.stories.jsx +++ b/code/frameworks/nextjs/template/stories/Image.stories.jsx @@ -3,6 +3,7 @@ import Image from 'next/image'; import { waitFor } from '@storybook/testing-library'; import Accessibility from '../../assets/accessibility.svg'; +import AvifImage from '../../assets/avif-test-image.avif'; export default { component: Image, @@ -14,6 +15,13 @@ export default { export const Default = {}; +export const Avif = { + args: { + src: AvifImage, + alt: 'Avif Test Image', + }, +}; + export const BlurredPlaceholder = { args: { placeholder: 'blur', diff --git a/code/lib/cli/rendererAssets/common/assets/avif-test-image.avif b/code/lib/cli/rendererAssets/common/assets/avif-test-image.avif new file mode 100644 index 0000000000000000000000000000000000000000..530709bc1217efa55381695b81007deaababe82d GIT binary patch literal 829 zcmZQzU{FXasVqn=%S>Yc0uY^>nP!-qnV9D5Xy^nK`jnemk_eIm0*#E6oFWL5fuSHX zxdg@r(K(q(Fk|=%GD~v7a*RMyE;A=T8N_p8U|NMfa2lA)ySt}E;Fo4e`ZA}%C)1~T?3x^wFC+AJ}Nue9fu zvir!(Rn79mU}yYI%k#VY_lIia#&Cqmew;6H>G9@`!JnLz`M5XLs=f1Se#de~&6LY* z?)T{y=cZm0mAkxio?7x98>v8>E7NapxgH2)t&UW3Z>gKPmQzt$Rjj8IHlYc+iS$yaGH5#esZ`bh1Pk*~L z@W|5UJuw#lM1xr`eNW`S!ti*#V{LnMj^}B=W|QA%uAaIaB;PV$Z^w;?6E5!A%6Ynp zZRP51-@kONTEN!P{gb)%_koWOlbsvQelK{^&*b0nX!m1<33uwl1