From e28d03c5a473e31046e6586e111a7814e221ccee Mon Sep 17 00:00:00 2001 From: Steven Date: Fri, 24 Jun 2022 10:56:05 -0400 Subject: [PATCH] Add experimental `next/future/image` component (#37927) This PR introduces a new experimental component, `next/future/image`, which is inspired by the existing experimental `layout="raw"`. The difference is that much of the code has been deleted in order to reduce client-side code as well as reduce complexity: - No `layout` prop - No `loader` config (although `loader` prop works) - No `IntersectionObserver`, use native `loading="lazy"` - No `lazyBoundary` - No `lazyRoot` - No `fill` (yet) so width & height are required - No `objectFit` (use `style` instead) - No `objectPosition` (use `style` instead) This improves performance because native `loading="lazy"` doesn't need to wait for React Hydration and client-side JS. In a future PR, we will modify `next/image` to remove `layout="raw"` since this new component supersedes it. ## Feature - [x] Integration tests added - [x] Documentation added - [x] Telemetry added. In case of a feature if it's used or not. - [x] Errors have helpful link attached, see `contributing.md` --- docs/api-reference/next/future/image.md | 463 +++++++ docs/manifest.json | 4 + errors/invalid-images-config.md | 2 + packages/next/build/webpack-config.ts | 1 + packages/next/client/future/image.tsx | 773 ++++++++++++ packages/next/future/image.d.ts | 3 + packages/next/future/image.js | 1 + packages/next/index.d.ts | 1 + packages/next/package.json | 1 + packages/next/server/config-shared.ts | 1 + packages/next/telemetry/events/version.ts | 3 + packages/next/tsconfig.json | 2 +- .../image-future/asset-prefix/next.config.js | 9 + .../image-future/asset-prefix/pages/index.js | 14 + .../image-future/asset-prefix/public/test.jpg | Bin 0 -> 6765 bytes .../asset-prefix/test/index.test.js | 55 + .../base-path/components/TallImage.js | 20 + .../base-path/components/tall.png | Bin 0 -> 6391 bytes .../image-future/base-path/next.config.js | 8 + .../image-future/base-path/pages/flex.js | 24 + .../base-path/pages/hidden-parent.js | 21 + .../image-future/base-path/pages/index.js | 19 + .../pages/invalid-src-proto-relative.js | 13 + .../base-path/pages/invalid-src.js | 13 + .../base-path/pages/missing-src.js | 12 + .../image-future/base-path/pages/prose.js | 20 + .../base-path/pages/prose.module.css | 5 + .../image-future/base-path/pages/sizes.js | 20 + .../base-path/pages/static-img.js | 46 + .../image-future/base-path/pages/update.js | 23 + .../base-path/public/exif-rotation.jpg | Bin 0 -> 9767 bytes .../base-path/public/foo/test-rect.jpg | Bin 0 -> 6089 bytes .../image-future/base-path/public/test.avif | Bin 0 -> 1043 bytes .../image-future/base-path/public/test.bmp | Bin 0 -> 80858 bytes .../image-future/base-path/public/test.gif | Bin 0 -> 2301 bytes .../image-future/base-path/public/test.ico | Bin 0 -> 4286 bytes .../image-future/base-path/public/test.jpg | Bin 0 -> 6765 bytes .../image-future/base-path/public/test.png | Bin 0 -> 1545 bytes .../image-future/base-path/public/test.svg | 13 + .../image-future/base-path/public/test.tiff | Bin 0 -> 2260 bytes .../image-future/base-path/public/test.webp | Bin 0 -> 1018 bytes .../image-future/base-path/public/wide.png | Bin 0 -> 46715 bytes .../image-future/base-path/test/index.test.js | 219 ++++ .../base-path/test/static.test.js | 104 ++ .../default/components/TallImage.js | 20 + .../image-future/default/components/tall.png | Bin 0 -> 6391 bytes .../image-future/default/next.config.js | 7 + .../image-future/default/pages/_document.js | 13 + .../image-future/default/pages/blob.js | 26 + .../default/pages/blurry-placeholder.js | 31 + .../image-future/default/pages/drop-srcset.js | 20 + .../image-future/default/pages/flex.js | 19 + .../default/pages/hidden-parent.js | 21 + .../image-future/default/pages/index.js | 14 + .../default/pages/inside-paragraph.js | 13 + .../default/pages/invalid-loader.js | 50 + .../pages/invalid-placeholder-blur-static.js | 17 + .../default/pages/invalid-placeholder-blur.js | 18 + .../pages/invalid-src-proto-relative.js | 13 + .../image-future/default/pages/invalid-src.js | 13 + .../default/pages/invalid-width-or-height.js | 18 + .../pages/layout-raw-placeholder-blur.js | 21 + .../image-future/default/pages/layout-raw.js | 54 + .../image-future/default/pages/loader-svg.js | 22 + .../image-future/default/pages/missing-src.js | 12 + .../image-future/default/pages/on-error.js | 49 + .../image-future/default/pages/on-load.js | 93 ++ .../default/pages/on-loading-complete.js | 124 ++ .../default/pages/priority-missing-warning.js | 15 + .../image-future/default/pages/priority.js | 44 + .../image-future/default/pages/prose.js | 15 + .../default/pages/prose.module.css | 5 + .../image-future/default/pages/rotated.js | 19 + .../image-future/default/pages/sizes.js | 20 + .../default/pages/small-img-import.js | 13 + .../image-future/default/pages/static-img.js | 46 + .../default/pages/style-filter.js | 31 + .../default/pages/style-inheritance.js | 34 + .../image-future/default/pages/style-prop.js | 50 + .../image-future/default/pages/update.js | 23 + .../default/pages/valid-html-w3c.js | 20 + .../default/pages/warning-once.js | 23 + .../default/public/exif-rotation.jpg | Bin 0 -> 9767 bytes .../default/public/foo/test-rect.jpg | Bin 0 -> 6089 bytes .../image-future/default/public/small.jpg | Bin 0 -> 439 bytes .../image-future/default/public/test.avif | Bin 0 -> 1043 bytes .../image-future/default/public/test.bmp | Bin 0 -> 80858 bytes .../image-future/default/public/test.gif | Bin 0 -> 2301 bytes .../image-future/default/public/test.ico | Bin 0 -> 4286 bytes .../image-future/default/public/test.jpg | Bin 0 -> 6765 bytes .../image-future/default/public/test.png | Bin 0 -> 1545 bytes .../image-future/default/public/test.svg | 13 + .../image-future/default/public/test.tiff | Bin 0 -> 2260 bytes .../image-future/default/public/test.webp | Bin 0 -> 1018 bytes .../image-future/default/public/wide.png | Bin 0 -> 46715 bytes .../image-future/default/style.module.css | 18 + .../image-future/default/test/index.test.js | 1072 +++++++++++++++++ .../image-future/default/test/static.test.js | 104 ++ .../image-from-node-modules/next.config.js | 10 + .../node_modules/my-cool-image.js | 56 + .../image-from-node-modules/pages/index.js | 18 + .../test/index.test.js | 52 + .../image-future/noscript/next.config.js | 7 + .../image-future/noscript/pages/index.js | 24 + .../image-future/noscript/test/index.test.js | 42 + .../react-virtualized/next.config.js | 7 + .../react-virtualized/pages/index.js | 34 + .../react-virtualized/public/test.jpg | Bin 0 -> 6765 bytes .../react-virtualized/test/index.test.js | 80 ++ .../image-future/svgo-webpack/next.config.js | 18 + .../image-future/svgo-webpack/pages/index.tsx | 5 + .../image-future/svgo-webpack/public/test.svg | 10 + .../svgo-webpack/test/index.test.js | 52 + .../image-future/svgo-webpack/tsconfig.json | 20 + .../trailing-slash/next.config.js | 8 + .../trailing-slash/pages/index.js | 14 + .../trailing-slash/public/test.jpg | Bin 0 -> 6765 bytes .../trailing-slash/test/index.test.js | 74 ++ .../typescript/components/image-card.tsx | 14 + .../components/image-dynamic-src.tsx | 16 + .../image-future/typescript/next.config.js | 11 + .../image-future/typescript/pages/invalid.tsx | 27 + .../image-future/typescript/pages/valid.tsx | 73 ++ .../image-future/typescript/public/tall.png | Bin 0 -> 6391 bytes .../image-future/typescript/public/test.avif | Bin 0 -> 1043 bytes .../image-future/typescript/public/test.svg | 10 + .../typescript/test/index.test.js | 90 ++ .../image-future/typescript/tsconfig.json | 20 + .../image-future/unicode/next.config.js | 8 + .../image-future/unicode/pages/index.js | 33 + .../unicode/public/hello world.jpg | Bin 0 -> 1545 bytes ...3\274\305\241\304\215\305\231\303\255.png" | Bin 0 -> 1545 bytes .../image-future/unicode/test/index.test.js | 101 ++ .../image-future/unoptimized/next.config.js | 8 + .../image-future/unoptimized/pages/index.js | 33 + .../image-future/unoptimized/public/test.jpg | Bin 0 -> 6765 bytes .../image-future/unoptimized/public/test.png | Bin 0 -> 1545 bytes .../image-future/unoptimized/public/test.webp | Bin 0 -> 1018 bytes .../unoptimized/test/index.test.js | 120 ++ test/integration/telemetry/test/index.test.js | 2 + 140 files changed, 5262 insertions(+), 1 deletion(-) create mode 100644 docs/api-reference/next/future/image.md create mode 100644 packages/next/client/future/image.tsx create mode 100644 packages/next/future/image.d.ts create mode 100644 packages/next/future/image.js create mode 100644 test/integration/image-future/asset-prefix/next.config.js create mode 100644 test/integration/image-future/asset-prefix/pages/index.js create mode 100644 test/integration/image-future/asset-prefix/public/test.jpg create mode 100644 test/integration/image-future/asset-prefix/test/index.test.js create mode 100644 test/integration/image-future/base-path/components/TallImage.js create mode 100644 test/integration/image-future/base-path/components/tall.png create mode 100644 test/integration/image-future/base-path/next.config.js create mode 100644 test/integration/image-future/base-path/pages/flex.js create mode 100644 test/integration/image-future/base-path/pages/hidden-parent.js create mode 100644 test/integration/image-future/base-path/pages/index.js create mode 100644 test/integration/image-future/base-path/pages/invalid-src-proto-relative.js create mode 100644 test/integration/image-future/base-path/pages/invalid-src.js create mode 100644 test/integration/image-future/base-path/pages/missing-src.js create mode 100644 test/integration/image-future/base-path/pages/prose.js create mode 100644 test/integration/image-future/base-path/pages/prose.module.css create mode 100644 test/integration/image-future/base-path/pages/sizes.js create mode 100644 test/integration/image-future/base-path/pages/static-img.js create mode 100644 test/integration/image-future/base-path/pages/update.js create mode 100644 test/integration/image-future/base-path/public/exif-rotation.jpg create mode 100644 test/integration/image-future/base-path/public/foo/test-rect.jpg create mode 100644 test/integration/image-future/base-path/public/test.avif create mode 100644 test/integration/image-future/base-path/public/test.bmp create mode 100644 test/integration/image-future/base-path/public/test.gif create mode 100644 test/integration/image-future/base-path/public/test.ico create mode 100644 test/integration/image-future/base-path/public/test.jpg create mode 100644 test/integration/image-future/base-path/public/test.png create mode 100644 test/integration/image-future/base-path/public/test.svg create mode 100644 test/integration/image-future/base-path/public/test.tiff create mode 100644 test/integration/image-future/base-path/public/test.webp create mode 100644 test/integration/image-future/base-path/public/wide.png create mode 100644 test/integration/image-future/base-path/test/index.test.js create mode 100644 test/integration/image-future/base-path/test/static.test.js create mode 100644 test/integration/image-future/default/components/TallImage.js create mode 100644 test/integration/image-future/default/components/tall.png create mode 100644 test/integration/image-future/default/next.config.js create mode 100644 test/integration/image-future/default/pages/_document.js create mode 100644 test/integration/image-future/default/pages/blob.js create mode 100644 test/integration/image-future/default/pages/blurry-placeholder.js create mode 100644 test/integration/image-future/default/pages/drop-srcset.js create mode 100644 test/integration/image-future/default/pages/flex.js create mode 100644 test/integration/image-future/default/pages/hidden-parent.js create mode 100644 test/integration/image-future/default/pages/index.js create mode 100644 test/integration/image-future/default/pages/inside-paragraph.js create mode 100644 test/integration/image-future/default/pages/invalid-loader.js create mode 100644 test/integration/image-future/default/pages/invalid-placeholder-blur-static.js create mode 100644 test/integration/image-future/default/pages/invalid-placeholder-blur.js create mode 100644 test/integration/image-future/default/pages/invalid-src-proto-relative.js create mode 100644 test/integration/image-future/default/pages/invalid-src.js create mode 100644 test/integration/image-future/default/pages/invalid-width-or-height.js create mode 100644 test/integration/image-future/default/pages/layout-raw-placeholder-blur.js create mode 100644 test/integration/image-future/default/pages/layout-raw.js create mode 100644 test/integration/image-future/default/pages/loader-svg.js create mode 100644 test/integration/image-future/default/pages/missing-src.js create mode 100644 test/integration/image-future/default/pages/on-error.js create mode 100644 test/integration/image-future/default/pages/on-load.js create mode 100644 test/integration/image-future/default/pages/on-loading-complete.js create mode 100644 test/integration/image-future/default/pages/priority-missing-warning.js create mode 100644 test/integration/image-future/default/pages/priority.js create mode 100644 test/integration/image-future/default/pages/prose.js create mode 100644 test/integration/image-future/default/pages/prose.module.css create mode 100644 test/integration/image-future/default/pages/rotated.js create mode 100644 test/integration/image-future/default/pages/sizes.js create mode 100644 test/integration/image-future/default/pages/small-img-import.js create mode 100644 test/integration/image-future/default/pages/static-img.js create mode 100644 test/integration/image-future/default/pages/style-filter.js create mode 100644 test/integration/image-future/default/pages/style-inheritance.js create mode 100644 test/integration/image-future/default/pages/style-prop.js create mode 100644 test/integration/image-future/default/pages/update.js create mode 100644 test/integration/image-future/default/pages/valid-html-w3c.js create mode 100644 test/integration/image-future/default/pages/warning-once.js create mode 100644 test/integration/image-future/default/public/exif-rotation.jpg create mode 100644 test/integration/image-future/default/public/foo/test-rect.jpg create mode 100644 test/integration/image-future/default/public/small.jpg create mode 100644 test/integration/image-future/default/public/test.avif create mode 100644 test/integration/image-future/default/public/test.bmp create mode 100644 test/integration/image-future/default/public/test.gif create mode 100644 test/integration/image-future/default/public/test.ico create mode 100644 test/integration/image-future/default/public/test.jpg create mode 100644 test/integration/image-future/default/public/test.png create mode 100644 test/integration/image-future/default/public/test.svg create mode 100644 test/integration/image-future/default/public/test.tiff create mode 100644 test/integration/image-future/default/public/test.webp create mode 100644 test/integration/image-future/default/public/wide.png create mode 100644 test/integration/image-future/default/style.module.css create mode 100644 test/integration/image-future/default/test/index.test.js create mode 100644 test/integration/image-future/default/test/static.test.js create mode 100644 test/integration/image-future/image-from-node-modules/next.config.js create mode 100644 test/integration/image-future/image-from-node-modules/node_modules/my-cool-image.js create mode 100644 test/integration/image-future/image-from-node-modules/pages/index.js create mode 100644 test/integration/image-future/image-from-node-modules/test/index.test.js create mode 100644 test/integration/image-future/noscript/next.config.js create mode 100644 test/integration/image-future/noscript/pages/index.js create mode 100644 test/integration/image-future/noscript/test/index.test.js create mode 100644 test/integration/image-future/react-virtualized/next.config.js create mode 100644 test/integration/image-future/react-virtualized/pages/index.js create mode 100644 test/integration/image-future/react-virtualized/public/test.jpg create mode 100644 test/integration/image-future/react-virtualized/test/index.test.js create mode 100644 test/integration/image-future/svgo-webpack/next.config.js create mode 100644 test/integration/image-future/svgo-webpack/pages/index.tsx create mode 100644 test/integration/image-future/svgo-webpack/public/test.svg create mode 100644 test/integration/image-future/svgo-webpack/test/index.test.js create mode 100644 test/integration/image-future/svgo-webpack/tsconfig.json create mode 100644 test/integration/image-future/trailing-slash/next.config.js create mode 100644 test/integration/image-future/trailing-slash/pages/index.js create mode 100644 test/integration/image-future/trailing-slash/public/test.jpg create mode 100644 test/integration/image-future/trailing-slash/test/index.test.js create mode 100644 test/integration/image-future/typescript/components/image-card.tsx create mode 100644 test/integration/image-future/typescript/components/image-dynamic-src.tsx create mode 100644 test/integration/image-future/typescript/next.config.js create mode 100644 test/integration/image-future/typescript/pages/invalid.tsx create mode 100644 test/integration/image-future/typescript/pages/valid.tsx create mode 100644 test/integration/image-future/typescript/public/tall.png create mode 100644 test/integration/image-future/typescript/public/test.avif create mode 100644 test/integration/image-future/typescript/public/test.svg create mode 100644 test/integration/image-future/typescript/test/index.test.js create mode 100644 test/integration/image-future/typescript/tsconfig.json create mode 100644 test/integration/image-future/unicode/next.config.js create mode 100644 test/integration/image-future/unicode/pages/index.js create mode 100644 test/integration/image-future/unicode/public/hello world.jpg create mode 100644 "test/integration/image-future/unicode/public/\303\244\303\266\303\274\305\241\304\215\305\231\303\255.png" create mode 100644 test/integration/image-future/unicode/test/index.test.js create mode 100644 test/integration/image-future/unoptimized/next.config.js create mode 100644 test/integration/image-future/unoptimized/pages/index.js create mode 100644 test/integration/image-future/unoptimized/public/test.jpg create mode 100644 test/integration/image-future/unoptimized/public/test.png create mode 100644 test/integration/image-future/unoptimized/public/test.webp create mode 100644 test/integration/image-future/unoptimized/test/index.test.js diff --git a/docs/api-reference/next/future/image.md b/docs/api-reference/next/future/image.md new file mode 100644 index 0000000000000..11785b5cce74f --- /dev/null +++ b/docs/api-reference/next/future/image.md @@ -0,0 +1,463 @@ +--- +description: Try the latest Image Optimization with the experimental `next/future/image` component. +--- + +# next/future/image + +
+ Version History + +| Version | Changes | +| --------- | -------------------------------------------- | +| `v12.2.0` | Experimental `next/future/image` introduced. | + +
+ +> **Note: This is API documentation for the Image Component and Image Optimization. For a feature overview and usage information for images in Next.js, please see [Images](/docs/basic-features/image-optimization.md).** + +## Required Props + +The `` component requires the following properties. + +### src + +Must be one of the following: + +1. A [statically imported](/docs/basic-features/image-optimization.md#local-images) image file, or +2. A path string. This can be either an absolute external URL, + or an internal path depending on the [loader](#loader) prop. + +When using an external URL, you must add it to [domains](#domains) in `next.config.js`. + +### width + +The `width` property represents the _rendered_ width in pixels, so it will affect how large the image appears. + +Required, except for [statically imported images](/docs/basic-features/image-optimization.md#local-images). + +### height + +The `height` property represents the _rendered_ height in pixels, so it will affect how large the image appears. + +Required, except for [statically imported images](/docs/basic-features/image-optimization.md#local-images). + +## Optional Props + +The `` component accepts a number of additional properties beyond those which are required. This section describes the most commonly-used properties of the Image component. Find details about more rarely-used properties in the [Advanced Props](#advanced-props) section. + +### loader + +A custom function used to resolve image URLs. + +A `loader` is a function returning a URL string for the image, given the following parameters: + +- [`src`](#src) +- [`width`](#width) +- [`quality`](#quality) + +Here is an example of using a custom loader: + +```js +import Image from 'next/future/image' + +const myLoader = ({ src, width, quality }) => { + return `https://example.com/${src}?w=${width}&q=${quality || 75}` +} + +const MyImage = (props) => { + return ( + Picture of the author + ) +} +``` + +### sizes + +A string that provides information about how wide the image will be at different breakpoints. + +It's important to assign `sizes` for responsive images that takes up less than the full viewport width. For example, when the parent element will constrain the image to always be less than half the viewport width, use `sizes="50vw"`. Without `sizes`, the image will be sent at twice the necessary resolution, decreasing performance. + +[Learn more](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-sizes). + +### quality + +The quality of the optimized image, an integer between `1` and `100`, where `100` is the best quality and therefore largest file size. Defaults to `75`. + +### priority + +When true, the image will be considered high priority and +[preload](https://web.dev/preload-responsive-images/). Lazy loading is automatically disabled for images using `priority`. + +You should use the `priority` property on any image detected as the [Largest Contentful Paint (LCP)](https://nextjs.org/learn/seo/web-performance/lcp) element. It may be appropriate to have multiple priority images, as different images may be the LCP element for different viewport sizes. + +Should only be used when the image is visible above the fold. Defaults to `false`. + +### placeholder + +A placeholder to use while the image is loading. Possible values are `blur` or `empty`. Defaults to `empty`. + +When `blur`, the [`blurDataURL`](#blurdataurl) property will be used as the placeholder. If `src` is an object from a [static import](/docs/basic-features/image-optimization.md#local-images) and the imported image is `.jpg`, `.png`, `.webp`, or `.avif`, then `blurDataURL` will be automatically populated. + +For dynamic images, you must provide the [`blurDataURL`](#blurdataurl) property. Solutions such as [Plaiceholder](https://github.com/joe-bell/plaiceholder) can help with `base64` generation. + +When `empty`, there will be no placeholder while the image is loading, only empty space. + +Try it out: + +- [Demo the `blur` placeholder](https://image-component.nextjs.gallery/placeholder) +- [Demo the shimmer effect with `blurDataURL` prop](https://image-component.nextjs.gallery/shimmer) +- [Demo the color effect with `blurDataURL` prop](https://image-component.nextjs.gallery/color) + +## Advanced Props + +In some cases, you may need more advanced usage. The `` component optionally accepts the following advanced properties. + +### style + +Allows [passing CSS styles](https://reactjs.org/docs/dom-elements.html#style) to the underlying image element. + +Also keep in mind that the required `width` and `height` props can interact with your styling. If you use styling to modify an image's `width`, you must set the `height="auto"` style as well, or your image will be distorted. + +### onLoadingComplete + +A callback function that is invoked once the image is completely loaded and the [placeholder](#placeholder) has been removed. + +The callback function will be called with one argument, an object with the following properties: + +- [`naturalWidth`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/naturalWidth) +- [`naturalHeight`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/naturalHeight) + +### onError + +A callback function that is invoked if the image fails to load. + +Note that the error might occur before client-side hydration completes, so this callback might not be invoked in that case. + +### loading + +> **Attention**: This property is only meant for advanced usage. Switching an +> image to load with `eager` will normally **hurt performance**. +> +> We recommend using the [`priority`](#priority) property instead, which +> properly loads the image eagerly for nearly all use cases. + +The loading behavior of the image. Defaults to `lazy`. + +When `lazy`, defer loading the image until it reaches a calculated distance from +the viewport. + +When `eager`, load the image immediately. + +[Learn more](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-loading) + +### blurDataURL + +A [Data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) to +be used as a placeholder image before the `src` image successfully loads. Only takes effect when combined +with [`placeholder="blur"`](#placeholder). + +Must be a base64-encoded image. It will be enlarged and blurred, so a very small image (10px or +less) is recommended. Including larger images as placeholders may harm your application performance. + +Try it out: + +- [Demo the default `blurDataURL` prop](https://image-component.nextjs.gallery/placeholder) +- [Demo the shimmer effect with `blurDataURL` prop](https://image-component.nextjs.gallery/shimmer) +- [Demo the color effect with `blurDataURL` prop](https://image-component.nextjs.gallery/color) + +You can also [generate a solid color Data URL](https://png-pixel.com) to match the image. + +**Example pointing to a DOM element** + +```jsx +import Image from 'next/future/image' +import React from 'react' + +const lazyRoot = React.useRef(null) + +const Example = () => ( +
+ + +
+) +``` + +**Example pointing to a React component** + +```jsx +import Image from 'next/future/image' +import React from 'react' + +const Container = React.forwardRef((props, ref) => { + return ( +
+ {props.children} +
+ ) +}) + +const Example = () => { + const lazyRoot = React.useRef(null) + + return ( + + + + + ) +} +``` + +[Learn more](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/root) + +### unoptimized + +When true, the source image will be served as-is instead of changing quality, +size, or format. Defaults to `false`. + +This prop can be assigned to all images by updating `next.config.js` with the following experimental configuration: + +```js +module.exports = { + experimental: { + images: { + unoptimized: true, + }, + }, +} +``` + +## Other Props + +Other properties on the `` component will be passed to the underlying +`img` element with the exception of the following: + +- `srcSet`. Use + [Device Sizes](#device-sizes) + instead. +- `ref`. Use [`onLoadingComplete`](#onloadingcomplete) instead. +- `decoding`. It is always `"async"`. + +## Configuration Options + +### Remote Patterns + +> Note: The `remotePatterns` configuration is currently **experimental** and subject to change. Please use [`domains`](#domains) for production use cases. + +To protect your application from malicious users, configuration is required in order to use external images. This ensures that only external images from your account can be served from the Next.js Image Optimization API. These external images can be configured with the `remotePatterns` property in your `next.config.js` file, as shown below: + +```js +module.exports = { + experimental: { + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: 'example.com', + port: '', + pathname: '/account123/**', + }, + ], + }, + }, +} +``` + +> Note: The example above will ensure the `src` property of `next/future/image` must start with `https://example.com/account123/`. Any other protocol, hostname, port, or unmatched path will respond with 400 Bad Request. + +Below is another example of the `remotePatterns` property in the `next.config.js` file: + +```js +module.exports = { + experimental: { + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: '**.example.com', + }, + ], + }, + }, +} +``` + +> Note: The example above will ensure the `src` property of `next/future/image` must start with `https://img1.example.com` or `https://me.avatar.example.com` or any number of subdomains. Any other protocol or unmatched hostname will respond with 400 Bad Request. + +Wildcard patterns can be used for both `pathname` and `hostname` and have the following syntax: + +- `*` match a single path segment or subdomain +- `**` match any number of path segments at the end or subdomains at the beginning + +The `**` syntax does not work in the middle of the pattern. + +### Domains + +Similar to [`remotePatterns`](#remote-patterns), the `domains` configuration can be used to provide a list of allowed hostnames for external images. + +However, the `domains` configuration does not support wildcard pattern matching and it cannot restrict protocol, port, or pathname. + +Below is an example of the `domains` property in the `next.config.js` file: + +```js +module.exports = { + images: { + domains: ['assets.acme.com'], + }, +} +``` + +## Advanced + +The following configuration is for advanced use cases and is usually not necessary. If you choose to configure the properties below, you will override any changes to the Next.js defaults in future updates. + +### Device Sizes + +If you know the expected device widths of your users, you can specify a list of device width breakpoints using the `deviceSizes` property in `next.config.js`. These widths are used when the `next/future/image` component uses [`sizes`](#sizes) prop to ensure the correct image is served for user's device. + +If no configuration is provided, the default below is used. + +```js +module.exports = { + images: { + deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], + }, +} +``` + +### Image Sizes + +You can specify a list of image widths using the `images.imageSizes` property in your `next.config.js` file. These widths are concatenated with the array of [device sizes](#device-sizes) to form the full array of sizes used to generate image [srcset](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/srcset)s. + +The reason there are two separate lists is that imageSizes is only used for images which provide a [`sizes`](#sizes) prop, which indicates that the image is less than the full width of the screen. **Therefore, the sizes in imageSizes should all be smaller than the smallest size in deviceSizes.** + +If no configuration is provided, the default below is used. + +```js +module.exports = { + images: { + imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], + }, +} +``` + +### Acceptable Formats + +The default [Image Optimization API](#loader) will automatically detect the browser's supported image formats via the request's `Accept` header. + +If the `Accept` head matches more than one of the configured formats, the first match in the array is used. Therefore, the array order matters. If there is no match (or the source image is [animated](#animated-images)), the Image Optimization API will fallback to the original image's format. + +If no configuration is provided, the default below is used. + +```js +module.exports = { + images: { + formats: ['image/webp'], + }, +} +``` + +You can enable AVIF support with the following configuration. + +```js +module.exports = { + images: { + formats: ['image/avif', 'image/webp'], + }, +} +``` + +> Note: AVIF generally takes 20% longer to encode but it compresses 20% smaller compared to WebP. This means that the first time an image is requested, it will typically be slower and then subsequent requests that are cached will be faster. + +> Note: If you self-host with a Proxy/CDN in front of Next.js, you must configure the Proxy to forward the `Accept` header. + +## Caching Behavior + +The following describes the caching algorithm for the default [loader](#loader). For all other loaders, please refer to your cloud provider's documentation. + +Images are optimized dynamically upon request and stored in the `/cache/images` directory. The optimized image file will be served for subsequent requests until the expiration is reached. When a request is made that matches a cached but expired file, the expired image is served stale immediately. Then the image is optimized again in the background (also called revalidation) and saved to the cache with the new expiration date. + +The cache status of an image can be determined by reading the value of the `x-nextjs-cache` response header. The possible values are the following: + +- `MISS` - the path is not in the cache (occurs at most once, on the first visit) +- `STALE` - the path is in the cache but exceeded the revalidate time so it will be updated in the background +- `HIT` - the path is in the cache and has not exceeded the revalidate time + +The expiration (or rather Max Age) is defined by either the [`minimumCacheTTL`](#minimum-cache-ttl) configuration or the upstream image `Cache-Control` header, whichever is larger. Specifically, the `max-age` value of the `Cache-Control` header is used. If both `s-maxage` and `max-age` are found, then `s-maxage` is preferred. The `max-age` is also passed-through to any downstream clients including CDNs and browsers. + +- You can configure [`minimumCacheTTL`](#minimum-cache-ttl) to increase the cache duration when the upstream image does not include `Cache-Control` header or the value is very low. +- You can configure [`deviceSizes`](#device-sizes) and [`imageSizes`](#device-sizes) to reduce the total number of possible generated images. +- You can configure [formats](#acceptable-formats) to disable multiple formats in favor of a single image format. + +### Minimum Cache TTL + +You can configure the Time to Live (TTL) in seconds for cached optimized images. In many cases, it's better to use a [Static Image Import](/docs/basic-features/image-optimization.md#local-images) which will automatically hash the file contents and cache the image forever with a `Cache-Control` header of `immutable`. + +```js +module.exports = { + images: { + minimumCacheTTL: 60, + }, +} +``` + +The expiration (or rather Max Age) of the optimized image is defined by either the `minimumCacheTTL` or the upstream image `Cache-Control` header, whichever is larger. + +If you need to change the caching behavior per image, you can configure [`headers`](/docs/api-reference/next.config.js/headers) to set the `Cache-Control` header on the upstream image (e.g. `/some-asset.jpg`, not `/_next/image` itself). + +There is no mechanism to invalidate the cache at this time, so its best to keep `minimumCacheTTL` low. Otherwise you may need to manually change the [`src`](#src) prop or delete `/cache/images`. + +### Disable Static Imports + +The default behavior allows you to import static files such as `import icon from './icon.png` and then pass that to the `src` property. + +In some cases, you may wish to disable this feature if it conflicts with other plugins that expect the import to behave differently. + +You can disable static image imports inside your `next.config.js`: + +```js +module.exports = { + images: { + disableStaticImages: true, + }, +} +``` + +### Dangerously Allow SVG + +The default [loader](#loader) does not optimize SVG images for a few reasons. First, SVG is a vector format meaning it can be resized losslessly. Second, SVG has many of the same features as HTML/CSS, which can lead to vulnerabilities without proper [Content Security Policy (CSP) headers](/docs/advanced-features/security-headers.md). + +If you need to serve SVG images with the default Image Optimization API, you can set `dangerouslyAllowSVG` and `contentSecurityPolicy` inside your `next.config.js`: + +```js +module.exports = { + images: { + dangerouslyAllowSVG: true, + contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;", + }, +} +``` + +### Animated Images + +The default [loader](#loader) will automatically bypass Image Optimization for animated images and serve the image as-is. + +Auto-detection for animated files is best-effort and supports GIF, APNG, and WebP. If you want to explicitly bypass Image Optimization for a given animated image, use the [unoptimized](#unoptimized) prop. + +## Related + +For an overview of the Image component features and usage guidelines, see: + + diff --git a/docs/manifest.json b/docs/manifest.json index 841c77f26cf86..8915c6ab8ef19 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -400,6 +400,10 @@ "title": "next/streaming", "path": "/docs/api-reference/next/streaming.md" }, + { + "title": "next/future/image (experimental)", + "path": "/docs/api-reference/next/future/image.md" + }, { "title": "Edge Runtime", "path": "/docs/api-reference/edge-runtime.md" diff --git a/errors/invalid-images-config.md b/errors/invalid-images-config.md index 9050dfbc5b1ae..978eadd081342 100644 --- a/errors/invalid-images-config.md +++ b/errors/invalid-images-config.md @@ -39,6 +39,8 @@ module.exports = { remotePatterns: [], // when true, every image will be unoptimized unoptimized: false, + // when true, allow `next/future/image` to be imported + allowFutureImage: false, }, }, } diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 0b97e45af895d..85ea5bde27ebe 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -1519,6 +1519,7 @@ export default async function getBaseWebpackConfig( loader: config.images.loader, experimentalUnoptimized: config?.experimental?.images?.unoptimized, experimentalLayoutRaw: config.experimental?.images?.layoutRaw, + experimentalFuture: config.experimental?.images?.allowFutureImage, ...(dev ? { // pass domains in development to allow validating on the client diff --git a/packages/next/client/future/image.tsx b/packages/next/client/future/image.tsx new file mode 100644 index 0000000000000..26d88f8dfffd3 --- /dev/null +++ b/packages/next/client/future/image.tsx @@ -0,0 +1,773 @@ +import React, { + useRef, + useEffect, + useCallback, + useContext, + useMemo, + useState, +} from 'react' +import Head from '../../shared/lib/head' +import { + ImageConfigComplete, + imageConfigDefault, +} from '../../shared/lib/image-config' +import { ImageConfigContext } from '../../shared/lib/image-config-context' +import { warnOnce } from '../../shared/lib/utils' + +const { + experimentalFuture = false, + experimentalRemotePatterns = [], + experimentalUnoptimized, +} = (process.env.__NEXT_IMAGE_OPTS as any) || {} +const configEnv = process.env.__NEXT_IMAGE_OPTS as any as ImageConfigComplete +const allImgs = new Map< + string, + { src: string; priority: boolean; placeholder: string } +>() +let perfObserver: PerformanceObserver | undefined + +if (typeof window === 'undefined') { + ;(global as any).__NEXT_IMAGE_IMPORTED = true +} + +const VALID_LOADING_VALUES = ['lazy', 'eager', undefined] as const +type LoadingValue = typeof VALID_LOADING_VALUES[number] +type ImageConfig = ImageConfigComplete & { allSizes: number[] } +export type ImageLoader = (p: ImageLoaderProps) => string + +export type ImageLoaderProps = { + src: string + width: number + quality?: number +} + +// Do not export - this is an internal type only +// because `next.config.js` is only meant for the +// built-in loaders, not for a custom loader() prop. +type ImageLoaderWithConfig = (p: ImageLoaderPropsWithConfig) => string +type ImageLoaderPropsWithConfig = ImageLoaderProps & { + config: Readonly +} + +type PlaceholderValue = 'blur' | 'empty' + +type OnLoadingComplete = (result: { + naturalWidth: number + naturalHeight: number +}) => void + +type ImgElementStyle = NonNullable + +type ImgElementWithDataProp = HTMLImageElement & { + 'data-loaded-src': string | undefined +} + +export interface StaticImageData { + src: string + height: number + width: number + blurDataURL?: string +} + +interface StaticRequire { + default: StaticImageData +} + +type StaticImport = StaticRequire | StaticImageData + +function isStaticRequire( + src: StaticRequire | StaticImageData +): src is StaticRequire { + return (src as StaticRequire).default !== undefined +} + +function isStaticImageData( + src: StaticRequire | StaticImageData +): src is StaticImageData { + return (src as StaticImageData).src !== undefined +} + +function isStaticImport(src: string | StaticImport): src is StaticImport { + return ( + typeof src === 'object' && + (isStaticRequire(src as StaticImport) || + isStaticImageData(src as StaticImport)) + ) +} + +export type ImageProps = Omit< + JSX.IntrinsicElements['img'], + 'src' | 'srcSet' | 'ref' | 'width' | 'height' | 'loading' +> & { + src: string | StaticImport + width?: number | string + height?: number | string + loader?: ImageLoader + quality?: number | string + priority?: boolean + loading?: LoadingValue + placeholder?: PlaceholderValue + blurDataURL?: string + unoptimized?: boolean + onLoadingComplete?: OnLoadingComplete +} + +type ImageElementProps = Omit & { + srcString: string + imgAttributes: GenImgAttrsResult + heightInt: number | undefined + widthInt: number | undefined + qualityInt: number | undefined + imgStyle: ImgElementStyle + blurStyle: ImgElementStyle + isLazy: boolean + loading: LoadingValue + config: ImageConfig + unoptimized: boolean + loader: ImageLoaderWithConfig + placeholder: PlaceholderValue + onLoadingCompleteRef: React.MutableRefObject + setBlurComplete: (b: boolean) => void + noscriptSizes: string | undefined +} + +function getWidths( + { deviceSizes, allSizes }: ImageConfig, + width: number | undefined, + sizes: string | undefined +): { widths: number[]; kind: 'w' | 'x' } { + if (sizes) { + // Find all the "vw" percent sizes used in the sizes prop + const viewportWidthRe = /(^|\s)(1?\d?\d)vw/g + const percentSizes = [] + for (let match; (match = viewportWidthRe.exec(sizes)); match) { + percentSizes.push(parseInt(match[2])) + } + if (percentSizes.length) { + const smallestRatio = Math.min(...percentSizes) * 0.01 + return { + widths: allSizes.filter((s) => s >= deviceSizes[0] * smallestRatio), + kind: 'w', + } + } + return { widths: allSizes, kind: 'w' } + } + if (typeof width !== 'number') { + return { widths: deviceSizes, kind: 'w' } + } + + const widths = [ + ...new Set( + // > This means that most OLED screens that say they are 3x resolution, + // > are actually 3x in the green color, but only 1.5x in the red and + // > blue colors. Showing a 3x resolution image in the app vs a 2x + // > resolution image will be visually the same, though the 3x image + // > takes significantly more data. Even true 3x resolution screens are + // > wasteful as the human eye cannot see that level of detail without + // > something like a magnifying glass. + // https://blog.twitter.com/engineering/en_us/topics/infrastructure/2019/capping-image-fidelity-on-ultra-high-resolution-devices.html + [width, width * 2 /*, width * 3*/].map( + (w) => allSizes.find((p) => p >= w) || allSizes[allSizes.length - 1] + ) + ), + ] + return { widths, kind: 'x' } +} + +type GenImgAttrsData = { + config: ImageConfig + src: string + unoptimized: boolean + loader: ImageLoaderWithConfig + width?: number + quality?: number + sizes?: string +} + +type GenImgAttrsResult = { + src: string + srcSet: string | undefined + sizes: string | undefined +} + +function generateImgAttrs({ + config, + src, + unoptimized, + width, + quality, + sizes, + loader, +}: GenImgAttrsData): GenImgAttrsResult { + if (unoptimized) { + return { src, srcSet: undefined, sizes: undefined } + } + + const { widths, kind } = getWidths(config, width, sizes) + const last = widths.length - 1 + + return { + sizes: !sizes && kind === 'w' ? '100vw' : sizes, + srcSet: widths + .map( + (w, i) => + `${loader({ config, src, quality, width: w })} ${ + kind === 'w' ? w : i + 1 + }${kind}` + ) + .join(', '), + + // It's intended to keep `src` the last attribute because React updates + // attributes in order. If we keep `src` the first one, Safari will + // immediately start to fetch `src`, before `sizes` and `srcSet` are even + // updated by React. That causes multiple unnecessary requests if `srcSet` + // and `sizes` are defined. + // This bug cannot be reproduced in Chrome or Firefox. + src: loader({ config, src, quality, width: widths[last] }), + } +} + +function getInt(x: unknown): number | undefined { + if (typeof x === 'number') { + return x + } + if (typeof x === 'string') { + return parseInt(x, 10) + } + return undefined +} + +// See https://stackoverflow.com/q/39777833/266535 for why we use this ref +// handler instead of the img's onLoad attribute. +function handleLoading( + img: ImgElementWithDataProp, + src: string, + placeholder: PlaceholderValue, + onLoadingCompleteRef: React.MutableRefObject, + setBlurComplete: (b: boolean) => void +) { + if (!img || img['data-loaded-src'] === src) { + return + } + img['data-loaded-src'] = src + const p = 'decode' in img ? img.decode() : Promise.resolve() + p.catch(() => {}).then(() => { + if (!img.parentNode) { + // Exit early in case of race condition: + // - onload() is called + // - decode() is called but incomplete + // - unmount is called + // - decode() completes + return + } + if (placeholder === 'blur') { + setBlurComplete(true) + } + if (onLoadingCompleteRef?.current) { + const { naturalWidth, naturalHeight } = img + // Pass back read-only primitive values but not the + // underlying DOM element because it could be misused. + onLoadingCompleteRef.current({ naturalWidth, naturalHeight }) + } + if (process.env.NODE_ENV !== 'production') { + const heightModified = + img.height.toString() !== img.getAttribute('height') + const widthModified = img.width.toString() !== img.getAttribute('width') + if ( + (heightModified && !widthModified) || + (!heightModified && widthModified) + ) { + warnOnce( + `Image with src "${src}" has either width or height modified, but not the other. If you use CSS to change the size of your image, also include the styles 'width: "auto"' or 'height: "auto"' to maintain the aspect ratio.` + ) + } + } + }) +} + +export default function Image({ + src, + sizes, + unoptimized = false, + priority = false, + loading, + className, + quality, + width, + height, + style, + onLoadingComplete, + placeholder = 'empty', + blurDataURL, + ...all +}: ImageProps) { + if (!experimentalFuture) { + throw new Error( + `The "next/future/image" component is experimental and may be subject to breaking changes. To enable this experiment, please include \`experimental: { images: { allowFutureImage: true } }\` in your next.config.js file.` + ) + } + const configContext = useContext(ImageConfigContext) + const config: ImageConfig = useMemo(() => { + const c = configEnv || configContext || imageConfigDefault + const allSizes = [...c.deviceSizes, ...c.imageSizes].sort((a, b) => a - b) + const deviceSizes = c.deviceSizes.sort((a, b) => a - b) + return { ...c, allSizes, deviceSizes } + }, [configContext]) + + let rest: Partial = all + + let loader: ImageLoaderWithConfig = defaultLoader + if ('loader' in rest) { + if (rest.loader) { + const customImageLoader = rest.loader + loader = (obj) => { + const { config: _, ...opts } = obj + // The config object is internal only so we must + // not pass it to the user-defined loader() + return customImageLoader(opts) + } + } + // Remove property so it's not spread on + delete rest.loader + } + + let staticSrc = '' + if (isStaticImport(src)) { + const staticImageData = isStaticRequire(src) ? src.default : src + + if (!staticImageData.src) { + throw new Error( + `An object should only be passed to the image component src parameter if it comes from a static image import. It must include src. Received ${JSON.stringify( + staticImageData + )}` + ) + } + blurDataURL = blurDataURL || staticImageData.blurDataURL + staticSrc = staticImageData.src + + height = height || staticImageData.height + width = width || staticImageData.width + if (!staticImageData.height || !staticImageData.width) { + throw new Error( + `An object should only be passed to the image component src parameter if it comes from a static image import. It must include height and width. Received ${JSON.stringify( + staticImageData + )}` + ) + } + } + src = typeof src === 'string' ? src : staticSrc + + const widthInt = getInt(width) + const heightInt = getInt(height) + const qualityInt = getInt(quality) + + let isLazy = + !priority && (loading === 'lazy' || typeof loading === 'undefined') + if (src.startsWith('data:') || src.startsWith('blob:')) { + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs + unoptimized = true + isLazy = false + } + if (experimentalUnoptimized) { + unoptimized = true + } + + const [blurComplete, setBlurComplete] = useState(false) + + if (process.env.NODE_ENV !== 'production') { + if (!src) { + throw new Error( + `Image is missing required "src" property. Make sure you pass "src" in props to the \`next/image\` component. Received: ${JSON.stringify( + { width, height, quality } + )}` + ) + } + if ( + (typeof widthInt !== 'undefined' && isNaN(widthInt)) || + (typeof heightInt !== 'undefined' && isNaN(heightInt)) + ) { + throw new Error( + `Image with src "${src}" has invalid "width" or "height" property. These should be numeric values.` + ) + } + if (!VALID_LOADING_VALUES.includes(loading)) { + throw new Error( + `Image with src "${src}" has invalid "loading" property. Provided "${loading}" should be one of ${VALID_LOADING_VALUES.map( + String + ).join(',')}.` + ) + } + if (priority && loading === 'lazy') { + throw new Error( + `Image with src "${src}" has both "priority" and "loading='lazy'" properties. Only one should be used.` + ) + } + + if ('objectFit' in rest) { + throw new Error( + `Image with src "${src}" has unknown prop "objectFit". This style should be specified using the "style" attribute.` + ) + } + if ('objectPosition' in rest) { + throw new Error( + `Image with src "${src}" has unknown prop "objectPosition". This style should be specified using the "style" attribute.` + ) + } + + if (placeholder === 'blur') { + if ((widthInt || 0) * (heightInt || 0) < 1600) { + warnOnce( + `Image with src "${src}" is smaller than 40x40. Consider removing the "placeholder='blur'" property to improve performance.` + ) + } + if (!blurDataURL) { + const VALID_BLUR_EXT = ['jpeg', 'png', 'webp', 'avif'] // should match next-image-loader + + throw new Error( + `Image with src "${src}" has "placeholder='blur'" property but is missing the "blurDataURL" property. + Possible solutions: + - Add a "blurDataURL" property, the contents should be a small Data URL to represent the image + - Change the "src" property to a static import with one of the supported file types: ${VALID_BLUR_EXT.join( + ',' + )} + - Remove the "placeholder" property, effectively no blur effect + Read more: https://nextjs.org/docs/messages/placeholder-blur-data-url` + ) + } + } + if ('ref' in rest) { + warnOnce( + `Image with src "${src}" is using unsupported "ref" property. Consider using the "onLoadingComplete" property instead.` + ) + } + + if (!unoptimized && loader !== defaultLoader) { + const urlStr = loader({ + config, + src, + width: widthInt || 400, + quality: qualityInt || 75, + }) + let url: URL | undefined + try { + url = new URL(urlStr) + } catch (err) {} + if (urlStr === src || (url && url.pathname === src && !url.search)) { + warnOnce( + `Image with src "${src}" has a "loader" property that does not implement width. Please implement it or use the "unoptimized" property instead.` + + `\nRead more: https://nextjs.org/docs/messages/next-image-missing-loader-width` + ) + } + } + + if ( + typeof window !== 'undefined' && + !perfObserver && + window.PerformanceObserver + ) { + perfObserver = new PerformanceObserver((entryList) => { + for (const entry of entryList.getEntries()) { + // @ts-ignore - missing "LargestContentfulPaint" class with "element" prop + const imgSrc = entry?.element?.src || '' + const lcpImage = allImgs.get(imgSrc) + if ( + lcpImage && + !lcpImage.priority && + lcpImage.placeholder !== 'blur' && + !lcpImage.src.startsWith('data:') && + !lcpImage.src.startsWith('blob:') + ) { + // https://web.dev/lcp/#measure-lcp-in-javascript + warnOnce( + `Image with src "${lcpImage.src}" was detected as the Largest Contentful Paint (LCP). Please add the "priority" property if this image is above the fold.` + + `\nRead more: https://nextjs.org/docs/api-reference/next/image#priority` + ) + } + } + }) + try { + perfObserver.observe({ + type: 'largest-contentful-paint', + buffered: true, + }) + } catch (err) { + // Log error but don't crash the app + console.error(err) + } + } + } + const imgStyle = Object.assign({}, style) + const svgBlurPlaceholder = `url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http%3A//www.w3.org/2000/svg' xmlns%3Axlink='http%3A//www.w3.org/1999/xlink' viewBox='0 0 ${widthInt} ${heightInt}'%3E%3Cfilter id='b' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='50'%3E%3C/feGaussianBlur%3E%3CfeComponentTransfer%3E%3CfeFuncA type='discrete' tableValues='1 1'%3E%3C/feFuncA%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Cimage filter='url(%23b)' x='0' y='0' height='100%25' width='100%25' href='${blurDataURL}'%3E%3C/image%3E%3C/svg%3E");` + const blurStyle = + placeholder === 'blur' && !blurComplete + ? { + backgroundSize: imgStyle.objectFit || 'cover', + backgroundPosition: imgStyle.objectPosition || '0% 0%', + ...(blurDataURL?.startsWith('data:image') + ? { + backgroundImage: svgBlurPlaceholder, + } + : { + filter: 'blur(20px)', + backgroundImage: `url("${blurDataURL}")`, + }), + } + : {} + + const imgAttributes = generateImgAttrs({ + config, + src, + unoptimized, + width: widthInt, + quality: qualityInt, + sizes, + loader, + }) + + let srcString: string = src + + if (process.env.NODE_ENV !== 'production') { + if (typeof window !== 'undefined') { + let fullUrl: URL + try { + fullUrl = new URL(imgAttributes.src) + } catch (e) { + fullUrl = new URL(imgAttributes.src, window.location.href) + } + allImgs.set(fullUrl.href, { src, priority, placeholder }) + } + } + + let imageSrcSetPropName = 'imagesrcset' + let imageSizesPropName = 'imagesizes' + if (process.env.__NEXT_REACT_ROOT) { + imageSrcSetPropName = 'imageSrcSet' + imageSizesPropName = 'imageSizes' + } + const linkProps = { + // Note: imagesrcset and imagesizes are not in the link element type with react 17. + [imageSrcSetPropName]: imgAttributes.srcSet, + [imageSizesPropName]: imgAttributes.sizes, + } + + const onLoadingCompleteRef = useRef(onLoadingComplete) + + useEffect(() => { + onLoadingCompleteRef.current = onLoadingComplete + }, [onLoadingComplete]) + + const imgElementArgs: ImageElementProps = { + isLazy, + imgAttributes, + heightInt, + widthInt, + qualityInt, + className, + imgStyle, + blurStyle, + loading, + config, + unoptimized, + placeholder, + loader, + srcString, + onLoadingCompleteRef, + setBlurComplete, + noscriptSizes: sizes, + ...rest, + } + return ( + <> + {} + {priority ? ( + // Note how we omit the `href` attribute, as it would only be relevant + // for browsers that do not support `imagesrcset`, and in those cases + // it would likely cause the incorrect image to be preloaded. + // + // https://html.spec.whatwg.org/multipage/semantics.html#attr-link-imagesrcset + + + + ) : null} + + ) +} + +const ImageElement = ({ + imgAttributes, + heightInt, + widthInt, + qualityInt, + className, + imgStyle, + blurStyle, + isLazy, + placeholder, + loading, + srcString, + config, + unoptimized, + loader, + onLoadingCompleteRef, + setBlurComplete, + onLoad, + onError, + noscriptSizes, + ...rest +}: ImageElementProps) => { + loading = isLazy ? 'lazy' : loading + return ( + <> + { + if (img?.complete) { + handleLoading( + img, + srcString, + placeholder, + onLoadingCompleteRef, + setBlurComplete + ) + } + }, + [srcString, placeholder, onLoadingCompleteRef, setBlurComplete] + )} + onLoad={(event) => { + const img = event.currentTarget as ImgElementWithDataProp + handleLoading( + img, + srcString, + placeholder, + onLoadingCompleteRef, + setBlurComplete + ) + if (onLoad) { + onLoad(event) + } + }} + onError={(event) => { + if (placeholder === 'blur') { + // If the real image fails to load, this will still remove the placeholder. + setBlurComplete(true) + } + if (onError) { + onError(event) + } + }} + /> + {(isLazy || placeholder === 'blur') && ( + + )} + + ) +} + +function defaultLoader({ + config, + src, + width, + quality, +}: ImageLoaderPropsWithConfig): string { + if (process.env.NODE_ENV !== 'production') { + const missingValues = [] + + // these should always be provided but make sure they are + if (!src) missingValues.push('src') + if (!width) missingValues.push('width') + + if (missingValues.length > 0) { + throw new Error( + `Next Image Optimization requires ${missingValues.join( + ', ' + )} to be provided. Make sure you pass them as props to the \`next/image\` component. Received: ${JSON.stringify( + { src, width, quality } + )}` + ) + } + + if (src.startsWith('//')) { + throw new Error( + `Failed to parse src "${src}" on \`next/image\`, protocol-relative URL (//) must be changed to an absolute URL (http:// or https://)` + ) + } + + if ( + !src.startsWith('/') && + (config.domains || experimentalRemotePatterns) + ) { + let parsedSrc: URL + try { + parsedSrc = new URL(src) + } catch (err) { + console.error(err) + throw new Error( + `Failed to parse src "${src}" on \`next/image\`, if using relative image it must start with a leading slash "/" or be an absolute URL (http:// or https://)` + ) + } + + if (process.env.NODE_ENV !== 'test') { + // We use dynamic require because this should only error in development + const { hasMatch } = require('../../shared/lib/match-remote-pattern') + if (!hasMatch(config.domains, experimentalRemotePatterns, parsedSrc)) { + throw new Error( + `Invalid src prop (${src}) on \`next/image\`, hostname "${parsedSrc.hostname}" is not configured under images in your \`next.config.js\`\n` + + `See more info: https://nextjs.org/docs/messages/next-image-unconfigured-host` + ) + } + } + } + } + + if (src.endsWith('.svg') && !config.dangerouslyAllowSVG) { + // Special case to make svg serve as-is to avoid proxying + // through the built-in Image Optimization API. + return src + } + + return `${config.path}?url=${encodeURIComponent(src)}&w=${width}&q=${ + quality || 75 + }` +} diff --git a/packages/next/future/image.d.ts b/packages/next/future/image.d.ts new file mode 100644 index 0000000000000..1c0b651935fde --- /dev/null +++ b/packages/next/future/image.d.ts @@ -0,0 +1,3 @@ +import Image from '../dist/client/future/image' +export * from '../dist/client/future/image' +export default Image diff --git a/packages/next/future/image.js b/packages/next/future/image.js new file mode 100644 index 0000000000000..d79491df545d6 --- /dev/null +++ b/packages/next/future/image.js @@ -0,0 +1 @@ +module.exports = require('../dist/client/future/image') diff --git a/packages/next/index.d.ts b/packages/next/index.d.ts index 6a660719f6dd3..b1b25705b79fa 100644 --- a/packages/next/index.d.ts +++ b/packages/next/index.d.ts @@ -7,6 +7,7 @@ /// /// /// +/// /// /// /// diff --git a/packages/next/package.json b/packages/next/package.json index 6c805d9dd2c18..3104711f2e229 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -49,6 +49,7 @@ "index.d.ts", "types/index.d.ts", "types/global.d.ts", + "future/image.d.ts", "image-types/global.d.ts" ], "bin": { diff --git a/packages/next/server/config-shared.ts b/packages/next/server/config-shared.ts index c0c95b1374a73..9bd458598ceb2 100644 --- a/packages/next/server/config-shared.ts +++ b/packages/next/server/config-shared.ts @@ -122,6 +122,7 @@ export interface ExperimentalConfig { layoutRaw: boolean remotePatterns: RemotePattern[] unoptimized?: boolean + allowFutureImage?: boolean } middlewareSourceMaps?: boolean modularizeImports?: Record< diff --git a/packages/next/telemetry/events/version.ts b/packages/next/telemetry/events/version.ts index f8c313363e418..8810773b52499 100644 --- a/packages/next/telemetry/events/version.ts +++ b/packages/next/telemetry/events/version.ts @@ -17,6 +17,7 @@ type EventCliSessionStarted = { basePathEnabled: boolean i18nEnabled: boolean imageEnabled: boolean + imageFutureEnabled: boolean locales: string | null localeDomainsCount: number | null localeDetectionEnabled: boolean | null @@ -62,6 +63,7 @@ export function eventCliSession( | 'basePathEnabled' | 'i18nEnabled' | 'imageEnabled' + | 'imageFutureEnabled' | 'locales' | 'localeDomainsCount' | 'localeDetectionEnabled' @@ -93,6 +95,7 @@ export function eventCliSession( hasWebpackConfig: typeof nextConfig?.webpack === 'function', hasBabelConfig: hasBabelConfig(dir), imageEnabled: !!images, + imageFutureEnabled: !!experimental.images?.allowFutureImage, basePathEnabled: !!nextConfig?.basePath, i18nEnabled: !!i18n, locales: i18n?.locales ? i18n.locales.join(',') : null, diff --git a/packages/next/tsconfig.json b/packages/next/tsconfig.json index f51f9e25e9937..b023d7fb53c56 100644 --- a/packages/next/tsconfig.json +++ b/packages/next/tsconfig.json @@ -8,5 +8,5 @@ "moduleResolution": "node", "jsx": "react" }, - "exclude": ["dist", "./*.d.ts", "image-types/global.d.ts"] + "exclude": ["dist", "./*.d.ts", "future/*.d.ts", "image-types/global.d.ts"] } diff --git a/test/integration/image-future/asset-prefix/next.config.js b/test/integration/image-future/asset-prefix/next.config.js new file mode 100644 index 0000000000000..7659d462ee86f --- /dev/null +++ b/test/integration/image-future/asset-prefix/next.config.js @@ -0,0 +1,9 @@ +module.exports = { + assetPrefix: 'https://example.vercel.sh/pre', + // Intentionally omit `domains` and `remotePatterns` + experimental: { + images: { + allowFutureImage: true, + }, + }, +} diff --git a/test/integration/image-future/asset-prefix/pages/index.js b/test/integration/image-future/asset-prefix/pages/index.js new file mode 100644 index 0000000000000..25712bdddac34 --- /dev/null +++ b/test/integration/image-future/asset-prefix/pages/index.js @@ -0,0 +1,14 @@ +import React from 'react' +import Image from 'next/future/image' +import img from '../public/test.jpg' + +const Page = () => { + return ( +
+

Asset Prefix

+ +
+ ) +} + +export default Page diff --git a/test/integration/image-future/asset-prefix/public/test.jpg b/test/integration/image-future/asset-prefix/public/test.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d536c882412ed3df0dc162823ca5146bcc033499 GIT binary patch literal 6765 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 { + describe('dev mode', () => { + beforeAll(async () => { + appPort = await findPort() + app = await launchApp(appDir, appPort) + }) + afterAll(() => killApp(app)) + + it('should include assetPrefix when placeholder=blur during next dev', async () => { + const browser = await webdriver(appPort, '/') + const id = 'test1' + const bgImage = await browser.eval( + `document.getElementById('${id}').style['background-image']` + ) + expect(bgImage).toMatch( + /\/_next\/image\?url=https%3A%2F%2Fexample\.vercel\.sh%2Fpre%2F_next%2Fstatic%2Fmedia%2Ftest(.+).jpg&w=8&q=70/ + ) + }) + }) + + describe('server mode', () => { + beforeAll(async () => { + await nextBuild(appDir) + appPort = await findPort() + app = await nextStart(appDir, appPort) + }) + afterAll(() => killApp(app)) + + it('should use base64 data url with placeholder=blur during next start', async () => { + let browser = await webdriver(appPort, '/') + const id = 'test1' + const bgImage = await browser.eval( + `document.getElementById('${id}').style['background-image']` + ) + expect(bgImage).toMatch('data:image/jpeg;base64') + }) + }) +}) diff --git a/test/integration/image-future/base-path/components/TallImage.js b/test/integration/image-future/base-path/components/TallImage.js new file mode 100644 index 0000000000000..4b9addb902145 --- /dev/null +++ b/test/integration/image-future/base-path/components/TallImage.js @@ -0,0 +1,20 @@ +import React from 'react' +import Image from 'next/future/image' + +import testTall from './tall.png' + +const Page = () => { + return ( +
+

Static Image

+ +
+ ) +} + +export default Page diff --git a/test/integration/image-future/base-path/components/tall.png b/test/integration/image-future/base-path/components/tall.png new file mode 100644 index 0000000000000000000000000000000000000000..a792dda6c172ffb6254d0d82e3a9a8a7cfe4d3e3 GIT binary patch literal 6391 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 { + return ( +
+

Hello World

+ +

This is the index page

+ +
+ ) +} + +export default Page diff --git a/test/integration/image-future/base-path/pages/hidden-parent.js b/test/integration/image-future/base-path/pages/hidden-parent.js new file mode 100644 index 0000000000000..a9010cf55b8a3 --- /dev/null +++ b/test/integration/image-future/base-path/pages/hidden-parent.js @@ -0,0 +1,21 @@ +import Image from 'next/future/image' +import React from 'react' + +const Page = () => { + return ( +
+

Hello World

+
+ +
+

This is the hidden parent page

+
+ ) +} + +export default Page diff --git a/test/integration/image-future/base-path/pages/index.js b/test/integration/image-future/base-path/pages/index.js new file mode 100644 index 0000000000000..48c835e8057ef --- /dev/null +++ b/test/integration/image-future/base-path/pages/index.js @@ -0,0 +1,19 @@ +import React from 'react' +import Image from 'next/future/image' + +const Page = () => { + return ( +
+

Home Page

+ +

This is the index page

+
+ ) +} + +export default Page diff --git a/test/integration/image-future/base-path/pages/invalid-src-proto-relative.js b/test/integration/image-future/base-path/pages/invalid-src-proto-relative.js new file mode 100644 index 0000000000000..0874a9c209dbe --- /dev/null +++ b/test/integration/image-future/base-path/pages/invalid-src-proto-relative.js @@ -0,0 +1,13 @@ +import React from 'react' +import Image from 'next/future/image' + +const Page = () => { + return ( +
+

Invalid Protocol Relative Source

+ +
+ ) +} + +export default Page diff --git a/test/integration/image-future/base-path/pages/invalid-src.js b/test/integration/image-future/base-path/pages/invalid-src.js new file mode 100644 index 0000000000000..36047a94c6bc2 --- /dev/null +++ b/test/integration/image-future/base-path/pages/invalid-src.js @@ -0,0 +1,13 @@ +import React from 'react' +import Image from 'next/future/image' + +const Page = () => { + return ( +
+

Invalid Source

+ +
+ ) +} + +export default Page diff --git a/test/integration/image-future/base-path/pages/missing-src.js b/test/integration/image-future/base-path/pages/missing-src.js new file mode 100644 index 0000000000000..3f2186089f216 --- /dev/null +++ b/test/integration/image-future/base-path/pages/missing-src.js @@ -0,0 +1,12 @@ +import React from 'react' +import Image from 'next/future/image' + +const Page = () => { + return ( +
+ +
+ ) +} + +export default Page diff --git a/test/integration/image-future/base-path/pages/prose.js b/test/integration/image-future/base-path/pages/prose.js new file mode 100644 index 0000000000000..aa2f6ad9b3b94 --- /dev/null +++ b/test/integration/image-future/base-path/pages/prose.js @@ -0,0 +1,20 @@ +import Image from 'next/future/image' +import React from 'react' +import * as styles from './prose.module.css' + +const Page = () => { + return ( +
+

Hello World

+ +

This is the rotated page

+
+ ) +} + +export default Page diff --git a/test/integration/image-future/base-path/pages/prose.module.css b/test/integration/image-future/base-path/pages/prose.module.css new file mode 100644 index 0000000000000..0e432d4ffaf3a --- /dev/null +++ b/test/integration/image-future/base-path/pages/prose.module.css @@ -0,0 +1,5 @@ +/* @tailwindcss/typography does this */ +.prose img { + margin-top: 2em; + margin-bottom: 2em; +} diff --git a/test/integration/image-future/base-path/pages/sizes.js b/test/integration/image-future/base-path/pages/sizes.js new file mode 100644 index 0000000000000..4e5d59724b6cc --- /dev/null +++ b/test/integration/image-future/base-path/pages/sizes.js @@ -0,0 +1,20 @@ +import React from 'react' +import Image from 'next/future/image' + +const Page = () => { + return ( +
+

Assign sizes prop

+ +

Assign sizes prop

+
+ ) +} + +export default Page diff --git a/test/integration/image-future/base-path/pages/static-img.js b/test/integration/image-future/base-path/pages/static-img.js new file mode 100644 index 0000000000000..6865a263afaa4 --- /dev/null +++ b/test/integration/image-future/base-path/pages/static-img.js @@ -0,0 +1,46 @@ +import React from 'react' +import testImg from '../public/foo/test-rect.jpg' +import Image from 'next/future/image' + +import testJPG from '../public/test.jpg' +import testPNG from '../public/test.png' +import testWEBP from '../public/test.webp' +import testAVIF from '../public/test.avif' +import testSVG from '../public/test.svg' +import testGIF from '../public/test.gif' +import testBMP from '../public/test.bmp' +import testICO from '../public/test.ico' + +import TallImage from '../components/TallImage' + +const Page = () => { + return ( +
+

Static Image

+ + + + + + +
+ + + + + + + + +
+ +
+ ) +} + +export default Page diff --git a/test/integration/image-future/base-path/pages/update.js b/test/integration/image-future/base-path/pages/update.js new file mode 100644 index 0000000000000..4618cbe3c0049 --- /dev/null +++ b/test/integration/image-future/base-path/pages/update.js @@ -0,0 +1,23 @@ +import React, { useState } from 'react' +import Image from 'next/future/image' + +const Page = () => { + const [toggled, setToggled] = useState(false) + return ( +
+

Update Page

+ +

This is the index page

+ +
+ ) +} + +export default Page diff --git a/test/integration/image-future/base-path/public/exif-rotation.jpg b/test/integration/image-future/base-path/public/exif-rotation.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5470a458f09336557e017e6c555c966af798a32d GIT binary patch literal 9767 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 literal 0 HcmV?d00001 diff --git a/test/integration/image-future/base-path/public/foo/test-rect.jpg b/test/integration/image-future/base-path/public/foo/test-rect.jpg new file mode 100644 index 0000000000000000000000000000000000000000..68d3a8415f5e640fa5dc79e09d2f9fd455067a4b GIT binary patch literal 6089 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 literal 0 HcmV?d00001 diff --git a/test/integration/image-future/base-path/public/test.avif b/test/integration/image-future/base-path/public/test.avif new file mode 100644 index 0000000000000000000000000000000000000000..e2c8170a6833ebff1ae4e11b9abff1bee6f4bdeb GIT binary patch literal 1043 zcmZQzU{FXasVqn=%S>Yc0uY^>nP!-qnV9D5Xy^nK`jnemk_eIm0*#E6oFWL5fuSHX zxdg@r(K(q(Fk|=%GD~v7a*RMyE;A=T8N_p8U|HGYPp_`;RN zQI{$oYx^~h{C(Tl1>lMeCPK)4A9urjDmcF}Sym>Oq zuQvx$8Y^FDRZDZOWigVCZeiEkrgxdQRO`S}W#70Swno%#aR?0O_-fq@A z-td)488LdsvuqsOrJH7$mpA;W{kt{rw6ka2>P6SsmbvX-Aywq>sQL4q8{^?~mjnJg z20obKDzi<|bg6crjj7nL&JLcE+fO_VBRQ8@g@t_nB@%R!>CSEYYUf!`7hLns*tTd> zSWy-Gb7sq_oii%;?o^*IcRoF4R$A*kG5^U`OFsSW3(c9ZQt8M zob`Q+N!|6H`07RK4oh-e^;{*{7S<|t-8djNcUhnIfm{C!4jj3$VX>{8g{4pX*>^6T zT(>g5aGCZtO|AQ=c0|p&tk~r!&%xKXR-Whno43-~uSGn0M(wpNqM?_#CeJPGxvD(z z{6Zl!X%nq=i^}`Lg)}a;PG*i{bJ7WFX4p~rhiM^mP+0QgIn%Gt(YT`Pp7ZwbcCmX8 zWVc7J-??N}_n$*M6TdNLr07%x{9L-mp21sQc-6yijyltyR=BP#OPsFth@+or&m-O` ai*%+vXFpb>?I3lmL0LhJC*)VwLI(iE8=mC= literal 0 HcmV?d00001 diff --git a/test/integration/image-future/base-path/public/test.bmp b/test/integration/image-future/base-path/public/test.bmp new file mode 100644 index 0000000000000000000000000000000000000000..f33feda8616b735b81ededeb14244f820342f24b GIT binary patch literal 80858 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 F{{U@jL16#@ literal 0 HcmV?d00001 diff --git a/test/integration/image-future/base-path/public/test.gif b/test/integration/image-future/base-path/public/test.gif new file mode 100644 index 0000000000000000000000000000000000000000..6bbbd315e9fe876cbdbd61261aceabd359efb49f GIT binary patch literal 2301 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 SFaH;^1F{2ux&x}M-`O2o>eJW& literal 0 HcmV?d00001 diff --git a/test/integration/image-future/base-path/public/test.jpg b/test/integration/image-future/base-path/public/test.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d536c882412ed3df0dc162823ca5146bcc033499 GIT binary patch literal 6765 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 literal 0 HcmV?d00001 diff --git a/test/integration/image-future/base-path/public/test.svg b/test/integration/image-future/base-path/public/test.svg new file mode 100644 index 0000000000000..a9b392f1aad59 --- /dev/null +++ b/test/integration/image-future/base-path/public/test.svg @@ -0,0 +1,13 @@ + + + + + + + diff --git a/test/integration/image-future/base-path/public/test.tiff b/test/integration/image-future/base-path/public/test.tiff new file mode 100644 index 0000000000000000000000000000000000000000..c2cc3e203bb3fdb5d828597623a630e6b0e59bfb GIT binary patch literal 2260 zcmYk8X*|?h9LE2H$(405mTgq;xLX1{585!sB> zlHk2xXGw4(o7xY>&t4og@!=H#quZ z+dP}zUTJm?u+yVVeF|@Py<*q4yz^h&Q};l7eHmfy#9oge`*tsFiF?m+!4CQ*cFI|U zULg+c@5A8*qnEEb!ez3oN?-bhI(B%`Tx$NshdIGZru}Je0>Yg--tWeAz6*?SM#u`M z_AdoTIzRE&&Kp_1N^n7x+Eq=jhef$OSqbl$+l+{IHIU63TKP(daqaY`z1`T0j&(CA zo&L1@JvPp*b7lAY&!>D`V9?VvC2rO(K1(RLD-AEf-p!YClA}AVQkk=dZ%~uFCv6?h z+Y8|`f%K*;Sqbz(EHTi&bi9l3Gf0Z3XpQt9fP9ubPzHe0UsS6S0ia9#+>2~1I>Gxn zzy^REDx}NV_MTpsJPGTOQvf;S000BYBV@+t&XN^a7;KKF*ERH(NP)jT7YBi(ySR>u zD5;2YCmJn4M2Ob%&^soJknEQMIuiHJE%A#A-j#a^?V6JU^eN3s85ZvaM2W*bnz>dh z{33$V&;8ic98lgaRh&^DI^XzQoC9pLfoDG_lYo@6(qUB%vQeOZpw{!0u% zQjhn%zmh<%n>@-F$e)MXB zdPSeP7zSpGVIdAs+39M?mmjTQ6x(eR$6QIlx-{Pr%~0Pe#pEhACp#amEQ69&eTu8S zk6=6nX)~GRc^uhF)$YK$JW9^Fej>Q2V#0*tFAc%OxmyY)(1HL+`32+hKuw9DH^zH) zrA`BN$e%7@Z+Tw0aC+E=x{_T|BV=lpLu0W6Z^Gl~>jxEq??M~sW`hIWvm_^&ryoXz zt!(T+t)!zhccdyxcZ4n=&DW}yamz0iz@{o6K5OJy*{bjqkA-T5 zBV_#obhmV03GUikH-CIM?V~~bkEEz&T6lVPW;-u>j?h4*o)Y2luI^`SvGoDpruumk z{CP?$mG^ljMO%?!L0oDPuFE`)D*5#V8|8PmMZLHtf@%Yi=+$9ZBrk;R-ys=rKjO8b zMM7|(EsYlN93el5sQ1=x1H>*M*5Thel}JzWG-kSk!egzb+U@EuUH#ILTX^Kv;9Y)J zh6_D)bc3G1Hom9)UV?8MrP#HkGl}IMQ%NV}Eq?n1rGoE2YdS=sZ-;}~|>WE+Q-zQXaTM}+f@LsKSe;}cPcV3rY+2!CYFM^koHVq$baNP5oz6JKA z6l{C5|3kfmOZ)-psVmzb+3!k30p%^MA16K3JRNGeNJi($C~VOieLoN3KK?SAdslxP z%Dn$L$qi33j7bdO*b-jOZ~wG`VIJ>dp{jdXonG#A^z!12 zQQT-e?9?7eYJjM2{Ke1&G6lPr%~-l!2S1{CGj_hXj>RnfheAf_yv%^umht)v6u&9H z5|ycwp#C7GenvmSkDa#u2qGm};q7jGxoK=hQoFs0LY|V1fT3SqA0G*T1 zrralYP85D#={z!1+Qcepb6i#+2U$vRw6JGCi%J;{<)y1q)|eg%C#i~S^G@tl@fOfZ1=At zeW6BWB%vZSA>GCoV?iZpm7@0x(Y_?xauT)zL#W02PC8YP!fG)4UG{#^OO+&YJ(e)$ z=!fyEA~n&l`}59zq>yUTRGS^)i>u#cR1ImT)9wJ|j)&fOOcv^~kAisMF?VXoTK)D1 z|KRYX%sMib?hvKsg`do=Cx?wV9Gt!E4=rjSlP4UbP`>_{ibitNwBx}=yg#YFi9Ge% zDJm$yf3l56-kEbcxE>e)?P<;uT6B&|4-UZ4Te7rj)A;xR0K^J_002m1PM|-SEo literal 0 HcmV?d00001 diff --git a/test/integration/image-future/base-path/public/test.webp b/test/integration/image-future/base-path/public/test.webp new file mode 100644 index 0000000000000000000000000000000000000000..4b306cb0898cc93e83dd6f72566e13cbd1339eaa GIT binary patch literal 1018 zcmWIYbaVT}%)k)t>J$(bV4?5~$lhSgFqctl0^gPIDuB8qHv48H-EEbk|FBuIt(=eGzI!$@2M@ z#TTkpe-pQ_(pI`{*bt+CqK4g>WuD^-8`F(nA0CC+|Bv@@pq)xt!UWH|e=J2I&PYA^ zWlqk)0|9oTHfu`3u4+!vT_4xYrvJXmxxu=z`*eD{~o~@4h9AWfq9;% zqR$+A*q{}?M)}6!)9;$Um~P0+FB3LdlX7i)VN-;n!!5QNkGLvHafX9Y89~=x2}Fh~ z6{=Xw=Xf8owQpHQQhfe*DWBsR$IZ_)v2DNh4;YT~5_h_!EDZl17V~0$^#W^&%R;M9 z1TEjSC+p$CeWvFQFka)8vCV&;F*&+u0)t$j-NbM5|Eshts#iT?RJqI9Y@6$P?sJE; z(L-rLjkkxn89m~?Lr5i{_oxx zSN0*UK=A6}>)z2FvXA5!`;<=WJryi<{b@Bvy@XxL&U~+rMl7KV^LSQ#f56oEGG)cK z=rpw;j*oejQy>@ zO`}}>1>=?-D{WT%kUkpAsF(X!&bNaVHSjask0+H4Wp zPLZ)O`L|h_E7aw-7d((IxEm7R?EL(E9LSfq9oZdPx)1Qg`#-(@lK0TJ8-elG*X7c( z`yam$?FnSP{2}Dw-$B|+seK+ P+udJbdSnVPy+8l}7RKRp literal 0 HcmV?d00001 diff --git a/test/integration/image-future/base-path/public/wide.png b/test/integration/image-future/base-path/public/wide.png new file mode 100644 index 0000000000000000000000000000000000000000..b7bb4dc1497bac8e6f4c339b760dcecbc67de692 GIT binary patch literal 46715 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 Sp zOVd&fKiPEUh{fCxHkoy8=)A-CtH)I3Yjrg%8!S} zSFCh)i?o;Sb1Q7@D&|z^Mi&XET{9A~t=zT1c}nqU8BeD+qhx7!>(*x-iP@<(NoKCf zS&3Cb{KzFh(Mx)8+J&3by}!B7eJ`S( zw*K@OWFw>NKstq?i^$lpqE_#_4=x&d7;X{7<-BkHy8GtJ^=Edal?`rq9i?_4)2c1? zh(M;8dR%HsS7xGy-bRb0k5y z@0m$Oc}{cmlFQPjB_&~oA|6NIiapQF!SAHC?%26ZK2yd-F(xU?4wqFf?px4tB&&Ks zNy&w_;1YR>rr<2$I?rN}Ye~^vM+Q=(*SC$ain8fTO|h5$buA&odc)sHw_wRZ&7xbjahcT?_gg zkColKyl%&A`KQq-Zr_7qQ{24lkC=Jy<$s>~)in8>V^fAkTdLEg0lB|Qc$c)bso~m9 zZbq?It!{Brrh6~!^$zLiD{Ased~4O@5$Dl#x5QQK!gX`ErY@O%>!W<^Gt;oQ<0tHY zEX{xLH}qE_ib-7$4>~IKU6q^K@u%0AW4<)MBBqU~SOd}m2JLABn;VAy%0okjF28bT zRn=46%#oR?y-RIsQ=*`Yt4MNYTQgQ}Oh-<0igb~1>^6rsB~$*6)aFjlI77UoihWa8 zbg{54iCgfYJfro+W42WtP-|;_e7*eh=}A7fH#S#nz3~0*^XEfGu{Oi6QEi;|+^wRs z&V1-ZuJ@3IjY)aC`G&NP)Y&OMPpT3ZSYOQ4d>7yuW?1|sjxm!rzL1DUMo_&AxG|SN;O-8gU}{K)Lt z#s!fNR!n+srrIQ*q}XX?e7@>yc5-LU*K9uzqPAUz)A8Y&v5B@up(Z9h-r#ZtYVEDMFxY+%F{y*+N zbjZK))~fjB47-MAi~AQa!U0K0((2@_zsXZpQK7A&`i~TkbU0O8Ngh zvMh-a)i-j<-~as%lrQ>t%O}t8xC|G6|NSyc$bbJSx&HSrsNDhgi6iSe{$axZd4K{W z`jHDKF5|Yx{&&*;Z({y$V*YQ({NIlGe+3fcLjM=a{(pYP{2m%9DzV^5<>jY}Bk(_U N6|GG%8+M=ie*nVr4@Lk0 literal 0 HcmV?d00001 diff --git a/test/integration/image-future/base-path/test/index.test.js b/test/integration/image-future/base-path/test/index.test.js new file mode 100644 index 0000000000000..b394442d11962 --- /dev/null +++ b/test/integration/image-future/base-path/test/index.test.js @@ -0,0 +1,219 @@ +/* eslint-env jest */ + +import { + check, + findPort, + getRedboxHeader, + hasRedbox, + killApp, + launchApp, + nextBuild, + nextStart, + waitFor, +} from 'next-test-utils' +import webdriver from 'next-webdriver' +import { join } from 'path' + +const appDir = join(__dirname, '../') + +let appPort +let app + +async function hasImageMatchingUrl(browser, url) { + const links = await browser.elementsByCss('img') + let foundMatch = false + for (const link of links) { + const src = await link.getAttribute('src') + if (new URL(src, `http://localhost:${appPort}`).toString() === url) { + foundMatch = true + break + } + } + return foundMatch +} + +async function getComputed(browser, id, prop) { + const val = await browser.eval(`document.getElementById('${id}').${prop}`) + if (typeof val === 'number') { + return val + } + if (typeof val === 'string') { + const v = parseInt(val, 10) + if (isNaN(v)) { + return val + } + return v + } + return null +} + +function getRatio(width, height) { + return height / width +} + +function runTests(mode) { + it('should load the images', async () => { + let browser + try { + browser = await webdriver(appPort, '/docs') + + await check(async () => { + const result = await browser.eval( + `document.getElementById('basic-image').naturalWidth` + ) + + if (result === 0) { + throw new Error('Incorrectly loaded image') + } + + return 'result-correct' + }, /result-correct/) + + expect( + await hasImageMatchingUrl( + browser, + `http://localhost:${appPort}/docs/_next/image?url=%2Fdocs%2Ftest.jpg&w=828&q=75` + ) + ).toBe(true) + } finally { + if (browser) { + await browser.close() + } + } + }) + + it('should update the image on src change', async () => { + let browser + try { + browser = await webdriver(appPort, '/docs/update') + + await check( + () => browser.eval(`document.getElementById("update-image").src`), + /test\.jpg/ + ) + + await browser.eval(`document.getElementById("toggle").click()`) + + await check( + () => browser.eval(`document.getElementById("update-image").src`), + /test\.png/ + ) + } finally { + if (browser) { + await browser.close() + } + } + }) + + it('should work when using flexbox', async () => { + let browser + try { + browser = await webdriver(appPort, '/docs/flex') + await check(async () => { + const result = await browser.eval( + `document.getElementById('basic-image').width` + ) + if (result === 0) { + throw new Error('Incorrectly loaded image') + } + + return 'result-correct' + }, /result-correct/) + } finally { + if (browser) { + await browser.close() + } + } + }) + + if (mode === 'dev') { + it('should show missing src error', async () => { + const browser = await webdriver(appPort, '/docs/missing-src') + + expect(await hasRedbox(browser)).toBe(true) + expect(await getRedboxHeader(browser)).toContain( + 'Image is missing required "src" property. Make sure you pass "src" in props to the `next/image` component. Received: {"width":200}' + ) + }) + + it('should show invalid src error', async () => { + const browser = await webdriver(appPort, '/docs/invalid-src') + + expect(await hasRedbox(browser)).toBe(true) + expect(await getRedboxHeader(browser)).toContain( + 'Invalid src prop (https://google.com/test.png) on `next/image`, hostname "google.com" is not configured under images in your `next.config.js`' + ) + }) + + it('should show invalid src error when protocol-relative', async () => { + const browser = await webdriver( + appPort, + '/docs/invalid-src-proto-relative' + ) + + expect(await hasRedbox(browser)).toBe(true) + expect(await getRedboxHeader(browser)).toContain( + 'Failed to parse src "//assets.example.com/img.jpg" on `next/image`, protocol-relative URL (//) must be changed to an absolute URL (http:// or https://)' + ) + }) + } + + it('should correctly ignore prose styles', async () => { + let browser + try { + browser = await webdriver(appPort, '/docs/prose') + + const id = 'prose-image' + + // Wait for image to load: + await check(async () => { + const result = await browser.eval( + `document.getElementById(${JSON.stringify(id)}).naturalWidth` + ) + + if (result < 1) { + throw new Error('Image not ready') + } + + return 'result-correct' + }, /result-correct/) + + await waitFor(1000) + + const computedWidth = await getComputed(browser, id, 'width') + const computedHeight = await getComputed(browser, id, 'height') + expect(getRatio(computedWidth, computedHeight)).toBeCloseTo(1, 1) + } finally { + if (browser) { + await browser.close() + } + } + }) +} + +describe('Image Component basePath Tests', () => { + describe('dev mode', () => { + beforeAll(async () => { + appPort = await findPort() + app = await launchApp(appDir, appPort) + }) + afterAll(async () => { + await killApp(app) + }) + + runTests('dev') + }) + + describe('server mode', () => { + beforeAll(async () => { + await nextBuild(appDir) + appPort = await findPort() + app = await nextStart(appDir, appPort) + }) + afterAll(async () => { + await killApp(app) + }) + + runTests('server') + }) +}) diff --git a/test/integration/image-future/base-path/test/static.test.js b/test/integration/image-future/base-path/test/static.test.js new file mode 100644 index 0000000000000..67a99e6e13666 --- /dev/null +++ b/test/integration/image-future/base-path/test/static.test.js @@ -0,0 +1,104 @@ +import { + findPort, + killApp, + nextBuild, + nextStart, + renderViaHTTP, + File, + waitFor, +} from 'next-test-utils' +import webdriver from 'next-webdriver' +import { join } from 'path' + +const appDir = join(__dirname, '../') +let appPort +let app +let browser +let html + +const indexPage = new File(join(appDir, 'pages/static-img.js')) + +const runTests = () => { + it('Should allow an image with a static src to omit height and width', async () => { + expect(await browser.elementById('basic-static')).toBeTruthy() + expect(await browser.elementById('blur-png')).toBeTruthy() + expect(await browser.elementById('blur-webp')).toBeTruthy() + expect(await browser.elementById('blur-avif')).toBeTruthy() + expect(await browser.elementById('blur-jpg')).toBeTruthy() + expect(await browser.elementById('static-svg')).toBeTruthy() + expect(await browser.elementById('static-gif')).toBeTruthy() + expect(await browser.elementById('static-bmp')).toBeTruthy() + expect(await browser.elementById('static-ico')).toBeTruthy() + expect(await browser.elementById('static-unoptimized')).toBeTruthy() + }) + it('Should use immutable cache-control header for static import', async () => { + await browser.eval( + `document.getElementById("basic-static").scrollIntoView()` + ) + await waitFor(1000) + const url = await browser.eval( + `document.getElementById("basic-static").src` + ) + const res = await fetch(url) + expect(res.headers.get('cache-control')).toBe( + 'public, max-age=315360000, immutable' + ) + }) + it('Should use immutable cache-control header even when unoptimized', async () => { + await browser.eval( + `document.getElementById("static-unoptimized").scrollIntoView()` + ) + await waitFor(1000) + const url = await browser.eval( + `document.getElementById("static-unoptimized").src` + ) + const res = await fetch(url) + expect(res.headers.get('cache-control')).toBe( + 'public, max-age=31536000, immutable' + ) + }) + it('Should automatically provide an image height and width', async () => { + expect(html).toContain('width="400" height="300"') + }) + it('Should allow provided width and height to override intrinsic', async () => { + expect(html).toContain('width="150" height="150"') + }) + it('Should add a blur to a statically imported image in "raw" mode', async () => { + expect(html).toContain( + `style="background-size:cover;background-position:0% 0%;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http%3A//www.w3.org/2000/svg' xmlns%3Axlink='http%3A//www.w3.org/1999/xlink' viewBox='0 0 400 300'%3E%3Cfilter id='b' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='50'%3E%3C/feGaussianBlur%3E%3CfeComponentTransfer%3E%3CfeFuncA type='discrete' tableValues='1 1'%3E%3C/feFuncA%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Cimage filter='url(%23b)' x='0' y='0' height='100%25' width='100%25' href='data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAoKCgoKCgsMDAsPEA4QDxYUExMUFiIYGhgaGCIzICUgICUgMy03LCksNy1RQDg4QFFeT0pPXnFlZXGPiI+7u/sBCgoKCgoKCwwMCw8QDhAPFhQTExQWIhgaGBoYIjMgJSAgJSAzLTcsKSw3LVFAODhAUV5PSk9ecWVlcY+Ij7u7+//CABEIAAYACAMBIgACEQEDEQH/xAAnAAEBAAAAAAAAAAAAAAAAAAAABwEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEAMQAAAAmgP/xAAcEAACAQUBAAAAAAAAAAAAAAASFBMAAQMFERX/2gAIAQEAAT8AZ1HjrKZX55JysIc4Ff/EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQIBAT8Af//EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQMBAT8Af//Z'%3E%3C/image%3E%3C/svg%3E");` + ) + }) +} + +describe('Build Error Tests', () => { + it('should throw build error when import statement is used with missing file', async () => { + await indexPage.replace( + '../public/foo/test-rect.jpg', + '../public/foo/test-rect-broken.jpg' + ) + + const { stderr } = await nextBuild(appDir, undefined, { stderr: true }) + await indexPage.restore() + + expect(stderr).toContain( + "Module not found: Can't resolve '../public/foo/test-rect-broken.jpg" + ) + // should contain the importing module + expect(stderr).toContain('./pages/static-img.js') + // should contain a import trace + expect(stderr).not.toContain('Import trace for requested module') + }) +}) +describe('Future Static Image Component Tests for basePath', () => { + beforeAll(async () => { + await nextBuild(appDir) + appPort = await findPort() + app = await nextStart(appDir, appPort) + html = await renderViaHTTP(appPort, '/docs/static-img') + browser = await webdriver(appPort, '/docs/static-img') + }) + afterAll(() => { + killApp(app) + }) + runTests() +}) diff --git a/test/integration/image-future/default/components/TallImage.js b/test/integration/image-future/default/components/TallImage.js new file mode 100644 index 0000000000000..4b9addb902145 --- /dev/null +++ b/test/integration/image-future/default/components/TallImage.js @@ -0,0 +1,20 @@ +import React from 'react' +import Image from 'next/future/image' + +import testTall from './tall.png' + +const Page = () => { + return ( +
+

Static Image

+ +
+ ) +} + +export default Page diff --git a/test/integration/image-future/default/components/tall.png b/test/integration/image-future/default/components/tall.png new file mode 100644 index 0000000000000000000000000000000000000000..a792dda6c172ffb6254d0d82e3a9a8a7cfe4d3e3 GIT binary patch literal 6391 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 + + +
+ + + + ) +} diff --git a/test/integration/image-future/default/pages/blob.js b/test/integration/image-future/default/pages/blob.js new file mode 100644 index 0000000000000..00b3cf367cd44 --- /dev/null +++ b/test/integration/image-future/default/pages/blob.js @@ -0,0 +1,26 @@ +import React, { useEffect, useState } from 'react' +import Image from 'next/future/image' + +const Page = () => { + const [src, setSrc] = useState() + + useEffect(() => { + fetch('/test.jpg') + .then((res) => { + return res.blob() + }) + .then((blob) => { + const url = URL.createObjectURL(blob) + setSrc(url) + }) + }, []) + + return ( +
+

Blob URL

+ {src ? : null} +
+ ) +} + +export default Page diff --git a/test/integration/image-future/default/pages/blurry-placeholder.js b/test/integration/image-future/default/pages/blurry-placeholder.js new file mode 100644 index 0000000000000..ffe30bd04a0f9 --- /dev/null +++ b/test/integration/image-future/default/pages/blurry-placeholder.js @@ -0,0 +1,31 @@ +import React from 'react' +import Image from 'next/future/image' + +export default function Page() { + return ( +
+

Blurry Placeholder

+ + + +
+ + +
+ ) +} diff --git a/test/integration/image-future/default/pages/drop-srcset.js b/test/integration/image-future/default/pages/drop-srcset.js new file mode 100644 index 0000000000000..08506f74e8ab9 --- /dev/null +++ b/test/integration/image-future/default/pages/drop-srcset.js @@ -0,0 +1,20 @@ +import React from 'react' +import Image from 'next/future/image' + +const Page = () => { + return ( +
+

Drop srcSet prop (cannot be manually provided)

+ +

Assign sizes prop

+
+ ) +} + +export default Page diff --git a/test/integration/image-future/default/pages/flex.js b/test/integration/image-future/default/pages/flex.js new file mode 100644 index 0000000000000..53db2563c0dca --- /dev/null +++ b/test/integration/image-future/default/pages/flex.js @@ -0,0 +1,19 @@ +import React from 'react' +import Image from 'next/future/image' + +const Page = () => { + return ( +
+

Hello World

+ +

This is the index page

+ +
+ ) +} + +export default Page diff --git a/test/integration/image-future/default/pages/hidden-parent.js b/test/integration/image-future/default/pages/hidden-parent.js new file mode 100644 index 0000000000000..95b460505889d --- /dev/null +++ b/test/integration/image-future/default/pages/hidden-parent.js @@ -0,0 +1,21 @@ +import Image from 'next/future/image' +import React from 'react' + +const Page = () => { + return ( +
+

Hello World

+
+ +
+

This is the hidden parent page

+
+ ) +} + +export default Page diff --git a/test/integration/image-future/default/pages/index.js b/test/integration/image-future/default/pages/index.js new file mode 100644 index 0000000000000..a46b62be71739 --- /dev/null +++ b/test/integration/image-future/default/pages/index.js @@ -0,0 +1,14 @@ +import React from 'react' +import Image from 'next/future/image' + +const Page = () => { + return ( +
+

Home Page

+ +

This is the index page

+
+ ) +} + +export default Page diff --git a/test/integration/image-future/default/pages/inside-paragraph.js b/test/integration/image-future/default/pages/inside-paragraph.js new file mode 100644 index 0000000000000..3c3033bd8e34e --- /dev/null +++ b/test/integration/image-future/default/pages/inside-paragraph.js @@ -0,0 +1,13 @@ +import React from 'react' +import Image from 'next/future/image' +import img from '../public/test.jpg' + +const Page = () => { + return ( +

+ +

+ ) +} + +export default Page diff --git a/test/integration/image-future/default/pages/invalid-loader.js b/test/integration/image-future/default/pages/invalid-loader.js new file mode 100644 index 0000000000000..9affbe804b4a5 --- /dev/null +++ b/test/integration/image-future/default/pages/invalid-loader.js @@ -0,0 +1,50 @@ +import React from 'react' +import Image from 'next/future/image' + +const Page = () => { + return ( +
+

Warn for this loader that doesnt use width

+ `${src}`} + /> + `${src}/${width}/file.jpg`} + /> + `${src}?w=${width / 2}`} + /> + + `https://example.vercel.sh${src}?width=${width * 2}` + } + /> + `https://example.vercel.sh${src}?size=medium`} + /> +
footer
+
+ ) +} + +export default Page diff --git a/test/integration/image-future/default/pages/invalid-placeholder-blur-static.js b/test/integration/image-future/default/pages/invalid-placeholder-blur-static.js new file mode 100644 index 0000000000000..0221bc7d8ede0 --- /dev/null +++ b/test/integration/image-future/default/pages/invalid-placeholder-blur-static.js @@ -0,0 +1,17 @@ +import React from 'react' +import Image from 'next/future/image' +import testBMP from '../public/test.bmp' + +const Page = () => { + return ( +
+ +
+ ) +} + +export default Page diff --git a/test/integration/image-future/default/pages/invalid-placeholder-blur.js b/test/integration/image-future/default/pages/invalid-placeholder-blur.js new file mode 100644 index 0000000000000..87fe60aeea00b --- /dev/null +++ b/test/integration/image-future/default/pages/invalid-placeholder-blur.js @@ -0,0 +1,18 @@ +import React from 'react' +import Image from 'next/future/image' + +const Page = () => { + return ( +
+ +
+ ) +} + +export default Page diff --git a/test/integration/image-future/default/pages/invalid-src-proto-relative.js b/test/integration/image-future/default/pages/invalid-src-proto-relative.js new file mode 100644 index 0000000000000..0874a9c209dbe --- /dev/null +++ b/test/integration/image-future/default/pages/invalid-src-proto-relative.js @@ -0,0 +1,13 @@ +import React from 'react' +import Image from 'next/future/image' + +const Page = () => { + return ( +
+

Invalid Protocol Relative Source

+ +
+ ) +} + +export default Page diff --git a/test/integration/image-future/default/pages/invalid-src.js b/test/integration/image-future/default/pages/invalid-src.js new file mode 100644 index 0000000000000..36047a94c6bc2 --- /dev/null +++ b/test/integration/image-future/default/pages/invalid-src.js @@ -0,0 +1,13 @@ +import React from 'react' +import Image from 'next/future/image' + +const Page = () => { + return ( +
+

Invalid Source

+ +
+ ) +} + +export default Page diff --git a/test/integration/image-future/default/pages/invalid-width-or-height.js b/test/integration/image-future/default/pages/invalid-width-or-height.js new file mode 100644 index 0000000000000..d596af6d51659 --- /dev/null +++ b/test/integration/image-future/default/pages/invalid-width-or-height.js @@ -0,0 +1,18 @@ +import React from 'react' +import Image from 'next/future/image' + +export default function Page() { + return ( +
+

Invalid width or height

+ + +
+ ) +} diff --git a/test/integration/image-future/default/pages/layout-raw-placeholder-blur.js b/test/integration/image-future/default/pages/layout-raw-placeholder-blur.js new file mode 100644 index 0000000000000..5ce94676731a2 --- /dev/null +++ b/test/integration/image-future/default/pages/layout-raw-placeholder-blur.js @@ -0,0 +1,21 @@ +import React from 'react' +import Image from 'next/future/image' + +import testJPG from '../public/test.jpg' +import testPNG from '../public/test.png' + +const Page = () => { + return ( +
+

Layout Raw with Placeholder Blur

+

Scroll down...

+
+ +
+ +
Footer
+
+ ) +} + +export default Page diff --git a/test/integration/image-future/default/pages/layout-raw.js b/test/integration/image-future/default/pages/layout-raw.js new file mode 100644 index 0000000000000..a4b059e20b8a6 --- /dev/null +++ b/test/integration/image-future/default/pages/layout-raw.js @@ -0,0 +1,54 @@ +import React from 'react' +import Image from 'next/future/image' + +const Page = () => { + return ( +
+

Layout Raw

+
+ +
+
+ +
+
+ +
+
+ +
+
+ ) +} + +export default Page diff --git a/test/integration/image-future/default/pages/loader-svg.js b/test/integration/image-future/default/pages/loader-svg.js new file mode 100644 index 0000000000000..bafba5e9cfacd --- /dev/null +++ b/test/integration/image-future/default/pages/loader-svg.js @@ -0,0 +1,22 @@ +import React from 'react' +import Image from 'next/future/image' + +const Page = () => { + return ( +
+

Should work with SVG

+ `${src}?size=${width}`} + /> +
+ +
footer
+
+ ) +} + +export default Page diff --git a/test/integration/image-future/default/pages/missing-src.js b/test/integration/image-future/default/pages/missing-src.js new file mode 100644 index 0000000000000..3f2186089f216 --- /dev/null +++ b/test/integration/image-future/default/pages/missing-src.js @@ -0,0 +1,12 @@ +import React from 'react' +import Image from 'next/future/image' + +const Page = () => { + return ( +
+ +
+ ) +} + +export default Page diff --git a/test/integration/image-future/default/pages/on-error.js b/test/integration/image-future/default/pages/on-error.js new file mode 100644 index 0000000000000..a566e457c6edb --- /dev/null +++ b/test/integration/image-future/default/pages/on-error.js @@ -0,0 +1,49 @@ +import { useState } from 'react' +import Image from 'next/future/image' + +const Page = () => { + const [clicked, setClicked] = useState(false) + + return ( +
+

Test onError

+

+ If error occured while loading image, native onError should be called. +

+ + + + + + ) +} + +function ImageWithMessage({ id, ...props }) { + const [msg, setMsg] = useState(`no error occured for img${id}`) + + return ( + <> +
+ { + setMsg(`error occured while loading ${e.target.id}`) + }} + {...props} + /> +
+

{msg}

+
+ + ) +} + +export default Page diff --git a/test/integration/image-future/default/pages/on-load.js b/test/integration/image-future/default/pages/on-load.js new file mode 100644 index 0000000000000..b868235bbeb93 --- /dev/null +++ b/test/integration/image-future/default/pages/on-load.js @@ -0,0 +1,93 @@ +import { useState } from 'react' +import Image from 'next/future/image' + +const Page = () => { + // Hoisted state to count each image load callback + const [idToCount, setIdToCount] = useState({}) + const [clicked, setClicked] = useState(false) + + return ( +
+

Test onLoad

+

+ This is the native onLoad which doesn't work as many places as + onLoadingComplete +

+ + + + + + + + + + + + + ) +} + +function ImageWithMessage({ id, idToCount, setIdToCount, ...props }) { + const [msg, setMsg] = useState('[LOADING]') + return ( + <> +
+ { + let count = idToCount[id] || 0 + count++ + idToCount[id] = count + setIdToCount(idToCount) + const msg = `loaded ${count} ${e.target.id} with native onLoad` + setMsg(msg) + console.log(msg) + }} + {...props} + /> +
+

{msg}

+
+ + ) +} + +export default Page diff --git a/test/integration/image-future/default/pages/on-loading-complete.js b/test/integration/image-future/default/pages/on-loading-complete.js new file mode 100644 index 0000000000000..858532aff1688 --- /dev/null +++ b/test/integration/image-future/default/pages/on-loading-complete.js @@ -0,0 +1,124 @@ +import { useState } from 'react' +import Image from 'next/future/image' + +const Page = () => { + // Hoisted state to count each image load callback + const [idToCount, setIdToCount] = useState({}) + const [clicked, setClicked] = useState(false) + + return ( +
+

On Loading Complete Test

+ + + + + + + + + + + + + + + + + + + + + ) +} + +function ImageWithMessage({ id, idToCount, setIdToCount, ...props }) { + const [msg, setMsg] = useState('[LOADING]') + return ( + <> +
+ { + let count = idToCount[id] || 0 + count++ + idToCount[id] = count + setIdToCount(idToCount) + const msg = `loaded ${count} img${id} with dimensions ${naturalWidth}x${naturalHeight}` + setMsg(msg) + console.log(msg) + }} + {...props} + /> +
+

{msg}

+
+ + ) +} + +export default Page diff --git a/test/integration/image-future/default/pages/priority-missing-warning.js b/test/integration/image-future/default/pages/priority-missing-warning.js new file mode 100644 index 0000000000000..0f16973fe538e --- /dev/null +++ b/test/integration/image-future/default/pages/priority-missing-warning.js @@ -0,0 +1,15 @@ +import React from 'react' +import Image from 'next/future/image' + +const Page = () => { + return ( +
+

Priority Missing Warning Page

+ + +
Priority Missing Warning Footer
+
+ ) +} + +export default Page diff --git a/test/integration/image-future/default/pages/priority.js b/test/integration/image-future/default/pages/priority.js new file mode 100644 index 0000000000000..e73327d727c38 --- /dev/null +++ b/test/integration/image-future/default/pages/priority.js @@ -0,0 +1,44 @@ +import React from 'react' +import Image from 'next/future/image' + +const Page = () => { + return ( +
+

Priority Page

+ + + + + +

This is the priority page

+
+ ) +} + +export default Page diff --git a/test/integration/image-future/default/pages/prose.js b/test/integration/image-future/default/pages/prose.js new file mode 100644 index 0000000000000..513e3d3e6eb14 --- /dev/null +++ b/test/integration/image-future/default/pages/prose.js @@ -0,0 +1,15 @@ +import Image from 'next/future/image' +import React from 'react' +import * as styles from './prose.module.css' + +const Page = () => { + return ( +
+

Hello World

+ +

This is the rotated page

+
+ ) +} + +export default Page diff --git a/test/integration/image-future/default/pages/prose.module.css b/test/integration/image-future/default/pages/prose.module.css new file mode 100644 index 0000000000000..0e432d4ffaf3a --- /dev/null +++ b/test/integration/image-future/default/pages/prose.module.css @@ -0,0 +1,5 @@ +/* @tailwindcss/typography does this */ +.prose img { + margin-top: 2em; + margin-bottom: 2em; +} diff --git a/test/integration/image-future/default/pages/rotated.js b/test/integration/image-future/default/pages/rotated.js new file mode 100644 index 0000000000000..65c2bb3fe0358 --- /dev/null +++ b/test/integration/image-future/default/pages/rotated.js @@ -0,0 +1,19 @@ +import Image from 'next/future/image' +import React from 'react' + +const Page = () => { + return ( +
+

Hello World

+ +

This is the rotated page

+
+ ) +} + +export default Page diff --git a/test/integration/image-future/default/pages/sizes.js b/test/integration/image-future/default/pages/sizes.js new file mode 100644 index 0000000000000..cc9b509657d6c --- /dev/null +++ b/test/integration/image-future/default/pages/sizes.js @@ -0,0 +1,20 @@ +import React from 'react' +import Image from 'next/future/image' + +const Page = () => { + return ( +
+

Assign sizes prop

+ +

Assign sizes prop

+
+ ) +} + +export default Page diff --git a/test/integration/image-future/default/pages/small-img-import.js b/test/integration/image-future/default/pages/small-img-import.js new file mode 100644 index 0000000000000..fc6fee07a0afc --- /dev/null +++ b/test/integration/image-future/default/pages/small-img-import.js @@ -0,0 +1,13 @@ +import React from 'react' +import Image from 'next/future/image' +import Small from '../public/small.jpg' + +const Page = () => { + return ( +
+ +
+ ) +} + +export default Page diff --git a/test/integration/image-future/default/pages/static-img.js b/test/integration/image-future/default/pages/static-img.js new file mode 100644 index 0000000000000..6865a263afaa4 --- /dev/null +++ b/test/integration/image-future/default/pages/static-img.js @@ -0,0 +1,46 @@ +import React from 'react' +import testImg from '../public/foo/test-rect.jpg' +import Image from 'next/future/image' + +import testJPG from '../public/test.jpg' +import testPNG from '../public/test.png' +import testWEBP from '../public/test.webp' +import testAVIF from '../public/test.avif' +import testSVG from '../public/test.svg' +import testGIF from '../public/test.gif' +import testBMP from '../public/test.bmp' +import testICO from '../public/test.ico' + +import TallImage from '../components/TallImage' + +const Page = () => { + return ( +
+

Static Image

+ + + + + + +
+ + + + + + + + +
+ +
+ ) +} + +export default Page diff --git a/test/integration/image-future/default/pages/style-filter.js b/test/integration/image-future/default/pages/style-filter.js new file mode 100644 index 0000000000000..508481ea193a1 --- /dev/null +++ b/test/integration/image-future/default/pages/style-filter.js @@ -0,0 +1,31 @@ +import React from 'react' +import Image from 'next/future/image' +import style from '../style.module.css' +import img from '../public/test.jpg' + +const Page = () => { + return ( +
+

Image Style Filter

+ + + + + +
Footer
+
+ ) +} + +export default Page diff --git a/test/integration/image-future/default/pages/style-inheritance.js b/test/integration/image-future/default/pages/style-inheritance.js new file mode 100644 index 0000000000000..c4868b8551258 --- /dev/null +++ b/test/integration/image-future/default/pages/style-inheritance.js @@ -0,0 +1,34 @@ +import React from 'react' +import Image from 'next/future/image' +import style from '../style.module.css' + +const Page = () => { + return ( +
+

Image Style Inheritance

+ + + + + + + + +
Footer
+
+ ) +} + +export default Page diff --git a/test/integration/image-future/default/pages/style-prop.js b/test/integration/image-future/default/pages/style-prop.js new file mode 100644 index 0000000000000..7daa8638fef52 --- /dev/null +++ b/test/integration/image-future/default/pages/style-prop.js @@ -0,0 +1,50 @@ +import React from 'react' +import Image from 'next/future/image' + +const Page = () => { + return ( +
+

Style prop usage and warnings

+ + + + + +
+ ) +} + +export default Page diff --git a/test/integration/image-future/default/pages/update.js b/test/integration/image-future/default/pages/update.js new file mode 100644 index 0000000000000..4618cbe3c0049 --- /dev/null +++ b/test/integration/image-future/default/pages/update.js @@ -0,0 +1,23 @@ +import React, { useState } from 'react' +import Image from 'next/future/image' + +const Page = () => { + const [toggled, setToggled] = useState(false) + return ( +
+

Update Page

+ +

This is the index page

+ +
+ ) +} + +export default Page diff --git a/test/integration/image-future/default/pages/valid-html-w3c.js b/test/integration/image-future/default/pages/valid-html-w3c.js new file mode 100644 index 0000000000000..1cef418bd90c0 --- /dev/null +++ b/test/integration/image-future/default/pages/valid-html-w3c.js @@ -0,0 +1,20 @@ +import Head from 'next/head' +import Image from 'next/future/image' + +const Page = () => { + return ( +
+ + Title + + + + +
+ basic image +
+
+ ) +} + +export default Page diff --git a/test/integration/image-future/default/pages/warning-once.js b/test/integration/image-future/default/pages/warning-once.js new file mode 100644 index 0000000000000..f75ac8fe0fc96 --- /dev/null +++ b/test/integration/image-future/default/pages/warning-once.js @@ -0,0 +1,23 @@ +import React from 'react' +import Image from 'next/future/image' + +const Page = () => { + const [count, setCount] = React.useState(0) + return ( + <> +

Warning should print at most once

+ + +
footer here
+ + ) +} + +export default Page diff --git a/test/integration/image-future/default/public/exif-rotation.jpg b/test/integration/image-future/default/public/exif-rotation.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5470a458f09336557e017e6c555c966af798a32d GIT binary patch literal 9767 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 literal 0 HcmV?d00001 diff --git a/test/integration/image-future/default/public/foo/test-rect.jpg b/test/integration/image-future/default/public/foo/test-rect.jpg new file mode 100644 index 0000000000000000000000000000000000000000..68d3a8415f5e640fa5dc79e09d2f9fd455067a4b GIT binary patch literal 6089 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 literal 0 HcmV?d00001 diff --git a/test/integration/image-future/default/public/small.jpg b/test/integration/image-future/default/public/small.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cb60a66dfd882a51eead0c6f0f3483f47976b978 GIT binary patch literal 439 zcmex=DK+5#AqGJX27U&9W=2H@CP7AK zLB{__7&L&+1UeB3u(EKVi2x=4-(uilW@KOzU={%KPA}XwPuiP}=6U!n!5vM7~XW?v;M8w_(|_^y^P5Oa1Md&XuNo z6I~e?UpU>du6OIp%C2WK#l&@gP6=b>b6#61z2LS>Rr$8PJJiqgt872D=KhxLQ$ML$ oHT2mmxFMJ^YuUXd8=yl(1X2CKz#ss&oQV-=xjh~Q%n$`P0qJyIZ~y=R literal 0 HcmV?d00001 diff --git a/test/integration/image-future/default/public/test.avif b/test/integration/image-future/default/public/test.avif new file mode 100644 index 0000000000000000000000000000000000000000..e2c8170a6833ebff1ae4e11b9abff1bee6f4bdeb GIT binary patch literal 1043 zcmZQzU{FXasVqn=%S>Yc0uY^>nP!-qnV9D5Xy^nK`jnemk_eIm0*#E6oFWL5fuSHX zxdg@r(K(q(Fk|=%GD~v7a*RMyE;A=T8N_p8U|HGYPp_`;RN zQI{$oYx^~h{C(Tl1>lMeCPK)4A9urjDmcF}Sym>Oq zuQvx$8Y^FDRZDZOWigVCZeiEkrgxdQRO`S}W#70Swno%#aR?0O_-fq@A z-td)488LdsvuqsOrJH7$mpA;W{kt{rw6ka2>P6SsmbvX-Aywq>sQL4q8{^?~mjnJg z20obKDzi<|bg6crjj7nL&JLcE+fO_VBRQ8@g@t_nB@%R!>CSEYYUf!`7hLns*tTd> zSWy-Gb7sq_oii%;?o^*IcRoF4R$A*kG5^U`OFsSW3(c9ZQt8M zob`Q+N!|6H`07RK4oh-e^;{*{7S<|t-8djNcUhnIfm{C!4jj3$VX>{8g{4pX*>^6T zT(>g5aGCZtO|AQ=c0|p&tk~r!&%xKXR-Whno43-~uSGn0M(wpNqM?_#CeJPGxvD(z z{6Zl!X%nq=i^}`Lg)}a;PG*i{bJ7WFX4p~rhiM^mP+0QgIn%Gt(YT`Pp7ZwbcCmX8 zWVc7J-??N}_n$*M6TdNLr07%x{9L-mp21sQc-6yijyltyR=BP#OPsFth@+or&m-O` ai*%+vXFpb>?I3lmL0LhJC*)VwLI(iE8=mC= literal 0 HcmV?d00001 diff --git a/test/integration/image-future/default/public/test.bmp b/test/integration/image-future/default/public/test.bmp new file mode 100644 index 0000000000000000000000000000000000000000..f33feda8616b735b81ededeb14244f820342f24b GIT binary patch literal 80858 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 F{{U@jL16#@ literal 0 HcmV?d00001 diff --git a/test/integration/image-future/default/public/test.gif b/test/integration/image-future/default/public/test.gif new file mode 100644 index 0000000000000000000000000000000000000000..6bbbd315e9fe876cbdbd61261aceabd359efb49f GIT binary patch literal 2301 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 SFaH;^1F{2ux&x}M-`O2o>eJW& literal 0 HcmV?d00001 diff --git a/test/integration/image-future/default/public/test.jpg b/test/integration/image-future/default/public/test.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d536c882412ed3df0dc162823ca5146bcc033499 GIT binary patch literal 6765 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 literal 0 HcmV?d00001 diff --git a/test/integration/image-future/default/public/test.svg b/test/integration/image-future/default/public/test.svg new file mode 100644 index 0000000000000..a9b392f1aad59 --- /dev/null +++ b/test/integration/image-future/default/public/test.svg @@ -0,0 +1,13 @@ + + + + + + + diff --git a/test/integration/image-future/default/public/test.tiff b/test/integration/image-future/default/public/test.tiff new file mode 100644 index 0000000000000000000000000000000000000000..c2cc3e203bb3fdb5d828597623a630e6b0e59bfb GIT binary patch literal 2260 zcmYk8X*|?h9LE2H$(405mTgq;xLX1{585!sB> zlHk2xXGw4(o7xY>&t4og@!=H#quZ z+dP}zUTJm?u+yVVeF|@Py<*q4yz^h&Q};l7eHmfy#9oge`*tsFiF?m+!4CQ*cFI|U zULg+c@5A8*qnEEb!ez3oN?-bhI(B%`Tx$NshdIGZru}Je0>Yg--tWeAz6*?SM#u`M z_AdoTIzRE&&Kp_1N^n7x+Eq=jhef$OSqbl$+l+{IHIU63TKP(daqaY`z1`T0j&(CA zo&L1@JvPp*b7lAY&!>D`V9?VvC2rO(K1(RLD-AEf-p!YClA}AVQkk=dZ%~uFCv6?h z+Y8|`f%K*;Sqbz(EHTi&bi9l3Gf0Z3XpQt9fP9ubPzHe0UsS6S0ia9#+>2~1I>Gxn zzy^REDx}NV_MTpsJPGTOQvf;S000BYBV@+t&XN^a7;KKF*ERH(NP)jT7YBi(ySR>u zD5;2YCmJn4M2Ob%&^soJknEQMIuiHJE%A#A-j#a^?V6JU^eN3s85ZvaM2W*bnz>dh z{33$V&;8ic98lgaRh&^DI^XzQoC9pLfoDG_lYo@6(qUB%vQeOZpw{!0u% zQjhn%zmh<%n>@-F$e)MXB zdPSeP7zSpGVIdAs+39M?mmjTQ6x(eR$6QIlx-{Pr%~0Pe#pEhACp#amEQ69&eTu8S zk6=6nX)~GRc^uhF)$YK$JW9^Fej>Q2V#0*tFAc%OxmyY)(1HL+`32+hKuw9DH^zH) zrA`BN$e%7@Z+Tw0aC+E=x{_T|BV=lpLu0W6Z^Gl~>jxEq??M~sW`hIWvm_^&ryoXz zt!(T+t)!zhccdyxcZ4n=&DW}yamz0iz@{o6K5OJy*{bjqkA-T5 zBV_#obhmV03GUikH-CIM?V~~bkEEz&T6lVPW;-u>j?h4*o)Y2luI^`SvGoDpruumk z{CP?$mG^ljMO%?!L0oDPuFE`)D*5#V8|8PmMZLHtf@%Yi=+$9ZBrk;R-ys=rKjO8b zMM7|(EsYlN93el5sQ1=x1H>*M*5Thel}JzWG-kSk!egzb+U@EuUH#ILTX^Kv;9Y)J zh6_D)bc3G1Hom9)UV?8MrP#HkGl}IMQ%NV}Eq?n1rGoE2YdS=sZ-;}~|>WE+Q-zQXaTM}+f@LsKSe;}cPcV3rY+2!CYFM^koHVq$baNP5oz6JKA z6l{C5|3kfmOZ)-psVmzb+3!k30p%^MA16K3JRNGeNJi($C~VOieLoN3KK?SAdslxP z%Dn$L$qi33j7bdO*b-jOZ~wG`VIJ>dp{jdXonG#A^z!12 zQQT-e?9?7eYJjM2{Ke1&G6lPr%~-l!2S1{CGj_hXj>RnfheAf_yv%^umht)v6u&9H z5|ycwp#C7GenvmSkDa#u2qGm};q7jGxoK=hQoFs0LY|V1fT3SqA0G*T1 zrralYP85D#={z!1+Qcepb6i#+2U$vRw6JGCi%J;{<)y1q)|eg%C#i~S^G@tl@fOfZ1=At zeW6BWB%vZSA>GCoV?iZpm7@0x(Y_?xauT)zL#W02PC8YP!fG)4UG{#^OO+&YJ(e)$ z=!fyEA~n&l`}59zq>yUTRGS^)i>u#cR1ImT)9wJ|j)&fOOcv^~kAisMF?VXoTK)D1 z|KRYX%sMib?hvKsg`do=Cx?wV9Gt!E4=rjSlP4UbP`>_{ibitNwBx}=yg#YFi9Ge% zDJm$yf3l56-kEbcxE>e)?P<;uT6B&|4-UZ4Te7rj)A;xR0K^J_002m1PM|-SEo literal 0 HcmV?d00001 diff --git a/test/integration/image-future/default/public/test.webp b/test/integration/image-future/default/public/test.webp new file mode 100644 index 0000000000000000000000000000000000000000..4b306cb0898cc93e83dd6f72566e13cbd1339eaa GIT binary patch literal 1018 zcmWIYbaVT}%)k)t>J$(bV4?5~$lhSgFqctl0^gPIDuB8qHv48H-EEbk|FBuIt(=eGzI!$@2M@ z#TTkpe-pQ_(pI`{*bt+CqK4g>WuD^-8`F(nA0CC+|Bv@@pq)xt!UWH|e=J2I&PYA^ zWlqk)0|9oTHfu`3u4+!vT_4xYrvJXmxxu=z`*eD{~o~@4h9AWfq9;% zqR$+A*q{}?M)}6!)9;$Um~P0+FB3LdlX7i)VN-;n!!5QNkGLvHafX9Y89~=x2}Fh~ z6{=Xw=Xf8owQpHQQhfe*DWBsR$IZ_)v2DNh4;YT~5_h_!EDZl17V~0$^#W^&%R;M9 z1TEjSC+p$CeWvFQFka)8vCV&;F*&+u0)t$j-NbM5|Eshts#iT?RJqI9Y@6$P?sJE; z(L-rLjkkxn89m~?Lr5i{_oxx zSN0*UK=A6}>)z2FvXA5!`;<=WJryi<{b@Bvy@XxL&U~+rMl7KV^LSQ#f56oEGG)cK z=rpw;j*oejQy>@ zO`}}>1>=?-D{WT%kUkpAsF(X!&bNaVHSjask0+H4Wp zPLZ)O`L|h_E7aw-7d((IxEm7R?EL(E9LSfq9oZdPx)1Qg`#-(@lK0TJ8-elG*X7c( z`yam$?FnSP{2}Dw-$B|+seK+ P+udJbdSnVPy+8l}7RKRp literal 0 HcmV?d00001 diff --git a/test/integration/image-future/default/public/wide.png b/test/integration/image-future/default/public/wide.png new file mode 100644 index 0000000000000000000000000000000000000000..b7bb4dc1497bac8e6f4c339b760dcecbc67de692 GIT binary patch literal 46715 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 Sp zOVd&fKiPEUh{fCxHkoy8=)A-CtH)I3Yjrg%8!S} zSFCh)i?o;Sb1Q7@D&|z^Mi&XET{9A~t=zT1c}nqU8BeD+qhx7!>(*x-iP@<(NoKCf zS&3Cb{KzFh(Mx)8+J&3by}!B7eJ`S( zw*K@OWFw>NKstq?i^$lpqE_#_4=x&d7;X{7<-BkHy8GtJ^=Edal?`rq9i?_4)2c1? zh(M;8dR%HsS7xGy-bRb0k5y z@0m$Oc}{cmlFQPjB_&~oA|6NIiapQF!SAHC?%26ZK2yd-F(xU?4wqFf?px4tB&&Ks zNy&w_;1YR>rr<2$I?rN}Ye~^vM+Q=(*SC$ain8fTO|h5$buA&odc)sHw_wRZ&7xbjahcT?_gg zkColKyl%&A`KQq-Zr_7qQ{24lkC=Jy<$s>~)in8>V^fAkTdLEg0lB|Qc$c)bso~m9 zZbq?It!{Brrh6~!^$zLiD{Ased~4O@5$Dl#x5QQK!gX`ErY@O%>!W<^Gt;oQ<0tHY zEX{xLH}qE_ib-7$4>~IKU6q^K@u%0AW4<)MBBqU~SOd}m2JLABn;VAy%0okjF28bT zRn=46%#oR?y-RIsQ=*`Yt4MNYTQgQ}Oh-<0igb~1>^6rsB~$*6)aFjlI77UoihWa8 zbg{54iCgfYJfro+W42WtP-|;_e7*eh=}A7fH#S#nz3~0*^XEfGu{Oi6QEi;|+^wRs z&V1-ZuJ@3IjY)aC`G&NP)Y&OMPpT3ZSYOQ4d>7yuW?1|sjxm!rzL1DUMo_&AxG|SN;O-8gU}{K)Lt z#s!fNR!n+srrIQ*q}XX?e7@>yc5-LU*K9uzqPAUz)A8Y&v5B@up(Z9h-r#ZtYVEDMFxY+%F{y*+N zbjZK))~fjB47-MAi~AQa!U0K0((2@_zsXZpQK7A&`i~TkbU0O8Ngh zvMh-a)i-j<-~as%lrQ>t%O}t8xC|G6|NSyc$bbJSx&HSrsNDhgi6iSe{$axZd4K{W z`jHDKF5|Yx{&&*;Z({y$V*YQ({NIlGe+3fcLjM=a{(pYP{2m%9DzV^5<>jY}Bk(_U N6|GG%8+M=ie*nVr4@Lk0 literal 0 HcmV?d00001 diff --git a/test/integration/image-future/default/style.module.css b/test/integration/image-future/default/style.module.css new file mode 100644 index 0000000000000..e538759372d08 --- /dev/null +++ b/test/integration/image-future/default/style.module.css @@ -0,0 +1,18 @@ +.displayFlex { + display: flex; +} + +.mainContainer span { + margin: 57px; +} + +.mainContainer img { + border-radius: 139px; +} + +.overrideImg { + filter: opacity(0.5); + background-size: 30%; + background-image: url('data:image/png;base64,iVBORw0KGgo='); + background-position: 1px 2px; +} diff --git a/test/integration/image-future/default/test/index.test.js b/test/integration/image-future/default/test/index.test.js new file mode 100644 index 0000000000000..09b77797f309f --- /dev/null +++ b/test/integration/image-future/default/test/index.test.js @@ -0,0 +1,1072 @@ +/* eslint-env jest */ + +import cheerio from 'cheerio' +import validateHTML from 'html-validator' +import { + check, + findPort, + getRedboxHeader, + hasRedbox, + killApp, + launchApp, + nextBuild, + nextStart, + renderViaHTTP, + waitFor, +} from 'next-test-utils' +import webdriver from 'next-webdriver' +import { join } from 'path' +import { existsSync } from 'fs' + +const appDir = join(__dirname, '../') + +let appPort +let app + +async function hasImageMatchingUrl(browser, url) { + const links = await browser.elementsByCss('img') + let foundMatch = false + for (const link of links) { + const src = await link.getAttribute('src') + if (new URL(src, `http://localhost:${appPort}`).toString() === url) { + foundMatch = true + break + } + } + return foundMatch +} + +async function getComputed(browser, id, prop) { + const val = await browser.eval(`document.getElementById('${id}').${prop}`) + if (typeof val === 'number') { + return val + } + if (typeof val === 'string') { + const v = parseInt(val, 10) + if (isNaN(v)) { + return val + } + return v + } + return null +} + +async function getComputedStyle(browser, id, prop) { + return browser.eval( + `window.getComputedStyle(document.getElementById('${id}')).getPropertyValue('${prop}')` + ) +} + +async function getSrc(browser, id) { + const src = await browser.elementById(id).getAttribute('src') + if (src) { + const url = new URL(src, `http://localhost:${appPort}`) + return url.href.slice(url.origin.length) + } +} + +function getRatio(width, height) { + return height / width +} + +function runTests(mode) { + it('should load the images', async () => { + let browser + try { + browser = await webdriver(appPort, '/') + + await check(async () => { + const result = await browser.eval( + `document.getElementById('basic-image').naturalWidth` + ) + + if (result === 0) { + throw new Error('Incorrectly loaded image') + } + + return 'result-correct' + }, /result-correct/) + + expect( + await hasImageMatchingUrl( + browser, + `http://localhost:${appPort}/_next/image?url=%2Ftest.jpg&w=828&q=75` + ) + ).toBe(true) + } finally { + if (browser) { + await browser.close() + } + } + }) + + it('should preload priority images', async () => { + let browser + try { + browser = await webdriver(appPort, '/priority') + + await check(async () => { + const result = await browser.eval( + `document.getElementById('basic-image').naturalWidth` + ) + + if (result === 0) { + throw new Error('Incorrectly loaded image') + } + + return 'result-correct' + }, /result-correct/) + + const links = await browser.elementsByCss('link[rel=preload][as=image]') + const entries = [] + for (const link of links) { + const imagesrcset = await link.getAttribute('imagesrcset') + const imagesizes = await link.getAttribute('imagesizes') + entries.push({ imagesrcset, imagesizes }) + } + expect(entries).toEqual([ + { + imagesizes: '', + imagesrcset: + '/_next/image?url=%2Ftest.jpg&w=640&q=75 1x, /_next/image?url=%2Ftest.jpg&w=828&q=75 2x', + }, + { + imagesizes: '100vw', + imagesrcset: + '/_next/image?url=%2Fwide.png&w=640&q=75 640w, /_next/image?url=%2Fwide.png&w=750&q=75 750w, /_next/image?url=%2Fwide.png&w=828&q=75 828w, /_next/image?url=%2Fwide.png&w=1080&q=75 1080w, /_next/image?url=%2Fwide.png&w=1200&q=75 1200w, /_next/image?url=%2Fwide.png&w=1920&q=75 1920w, /_next/image?url=%2Fwide.png&w=2048&q=75 2048w, /_next/image?url=%2Fwide.png&w=3840&q=75 3840w', + }, + { + imagesizes: '', + imagesrcset: + '/_next/image?url=%2Ftest.webp&w=1200&q=75 1x, /_next/image?url=%2Ftest.webp&w=3840&q=75 2x', + }, + ]) + + // When priority={true}, we should _not_ set loading="lazy" + expect( + await browser.elementById('basic-image').getAttribute('loading') + ).toBe(null) + expect( + await browser.elementById('load-eager').getAttribute('loading') + ).toBe('eager') + expect( + await browser.elementById('responsive1').getAttribute('loading') + ).toBe(null) + expect( + await browser.elementById('responsive2').getAttribute('loading') + ).toBe(null) + expect(await browser.elementById('raw1').getAttribute('loading')).toBe( + null + ) + + const warnings = (await browser.log('browser')) + .map((log) => log.message) + .join('\n') + expect(warnings).not.toMatch( + /was detected as the Largest Contentful Paint/gm + ) + } finally { + if (browser) { + await browser.close() + } + } + }) + + it('should not pass through user-provided srcset (causing a flash)', async () => { + const html = await renderViaHTTP(appPort, '/drop-srcset') + const $html = cheerio.load(html) + + const els = [].slice.apply($html('img')) + expect(els.length).toBe(2) + + const [el, noscriptEl] = els + expect(noscriptEl.attribs.src).toBeDefined() + expect(noscriptEl.attribs.srcset).toBeDefined() + + expect(el.attribs.src).not.toBe('/truck.jpg') + expect(el.attribs.srcset).not.toBe( + '/truck375.jpg 375w, /truck640.jpg 640w, /truck.jpg' + ) + expect(el.attribs.srcSet).not.toBe( + '/truck375.jpg 375w, /truck640.jpg 640w, /truck.jpg' + ) + }) + + it('should update the image on src change', async () => { + let browser + try { + browser = await webdriver(appPort, '/update') + + await check( + () => browser.eval(`document.getElementById("update-image").src`), + /test\.jpg/ + ) + + await browser.eval(`document.getElementById("toggle").click()`) + + await check( + () => browser.eval(`document.getElementById("update-image").src`), + /test\.png/ + ) + } finally { + if (browser) { + await browser.close() + } + } + }) + + it('should callback onLoadingComplete when image is fully loaded', async () => { + let browser + try { + browser = await webdriver(appPort, '/on-loading-complete') + + await browser.eval( + `document.getElementById("footer").scrollIntoView({behavior: "smooth"})` + ) + + await check( + () => browser.eval(`document.getElementById("img1").currentSrc`), + /test(.*)jpg/ + ) + await check( + () => browser.eval(`document.getElementById("img2").currentSrc`), + /test(.*).png/ + ) + await check( + () => browser.eval(`document.getElementById("img3").currentSrc`), + /test\.svg/ + ) + await check( + () => browser.eval(`document.getElementById("img4").currentSrc`), + /test(.*)ico/ + ) + await check( + () => browser.eval(`document.getElementById("msg1").textContent`), + 'loaded 1 img1 with dimensions 128x128' + ) + await check( + () => browser.eval(`document.getElementById("msg2").textContent`), + 'loaded 1 img2 with dimensions 400x400' + ) + await check( + () => browser.eval(`document.getElementById("msg3").textContent`), + 'loaded 1 img3 with dimensions 400x400' + ) + await check( + () => browser.eval(`document.getElementById("msg4").textContent`), + 'loaded 1 img4 with dimensions 32x32' + ) + await check( + () => browser.eval(`document.getElementById("msg5").textContent`), + 'loaded 1 img5 with dimensions 3x5' + ) + await check( + () => browser.eval(`document.getElementById("msg6").textContent`), + 'loaded 1 img6 with dimensions 3x5' + ) + await check( + () => browser.eval(`document.getElementById("msg7").textContent`), + 'loaded 1 img7 with dimensions 400x400' + ) + await check( + () => browser.eval(`document.getElementById("msg8").textContent`), + 'loaded 1 img8 with dimensions 640x373' + ) + await check( + () => + browser.eval( + `document.getElementById("img8").getAttribute("data-nimg")` + ), + 'future' + ) + await check( + () => browser.eval(`document.getElementById("img8").currentSrc`), + /wide.png/ + ) + await browser.eval('document.getElementById("toggle").click()') + await check( + () => browser.eval(`document.getElementById("msg8").textContent`), + 'loaded 2 img8 with dimensions 400x300' + ) + await check( + () => + browser.eval( + `document.getElementById("img8").getAttribute("data-nimg")` + ), + 'future' + ) + await check( + () => browser.eval(`document.getElementById("img8").currentSrc`), + /test-rect.jpg/ + ) + await check( + () => browser.eval(`document.getElementById("msg9").textContent`), + 'loaded 1 img9 with dimensions 400x400' + ) + } finally { + if (browser) { + await browser.close() + } + } + }) + + it('should callback native onLoad in most cases', async () => { + let browser = await webdriver(appPort, '/on-load') + + for (let i = 1; i < 6; i++) { + await browser.eval( + `document.getElementById("img${i}").scrollIntoView({behavior: "smooth"})` + ) + await waitFor(100) + } + + await check( + () => browser.eval(`document.getElementById("img1").currentSrc`), + /test(.*)jpg/ + ) + await check( + () => browser.eval(`document.getElementById("img2").currentSrc`), + /test(.*).png/ + ) + await check( + () => browser.eval(`document.getElementById("img3").currentSrc`), + /test\.svg/ + ) + await check( + () => browser.eval(`document.getElementById("img4").currentSrc`), + /test(.*)ico/ + ) + await check( + () => browser.eval(`document.getElementById("msg1").textContent`), + 'loaded 1 img1 with native onLoad' + ) + await check( + () => browser.eval(`document.getElementById("msg2").textContent`), + 'loaded 1 img2 with native onLoad' + ) + await check( + () => browser.eval(`document.getElementById("msg3").textContent`), + 'loaded 1 img3 with native onLoad' + ) + await check( + () => browser.eval(`document.getElementById("msg4").textContent`), + 'loaded 1 img4 with native onLoad' + ) + await check( + () => browser.eval(`document.getElementById("msg5").textContent`), + 'loaded 1 img5 with native onLoad' + ) + await check( + () => + browser.eval( + `document.getElementById("img5").getAttribute("data-nimg")` + ), + 'future' + ) + await check( + () => browser.eval(`document.getElementById("img5").currentSrc`), + /wide.png/ + ) + await browser.eval('document.getElementById("toggle").click()') + await check( + () => browser.eval(`document.getElementById("msg5").textContent`), + 'loaded 2 img5 with native onLoad' + ) + await check( + () => + browser.eval( + `document.getElementById("img5").getAttribute("data-nimg")` + ), + 'future' + ) + await check( + () => browser.eval(`document.getElementById("img5").currentSrc`), + /test-rect.jpg/ + ) + }) + + it('should callback native onError when error occured while loading image', async () => { + let browser = await webdriver(appPort, '/on-error') + await browser.eval( + `document.getElementById("img1").scrollIntoView({behavior: "smooth"})` + ) + await check( + () => browser.eval(`document.getElementById("msg1").textContent`), + 'no error occured for img1' + ) + await browser.eval( + `document.getElementById("img2").scrollIntoView({behavior: "smooth"})` + ) + await check( + () => browser.eval(`document.getElementById("msg2").textContent`), + 'no error occured for img2' + ) + await browser.eval(`document.getElementById("toggle").click()`) + await check( + () => browser.eval(`document.getElementById("msg2").textContent`), + 'error occured while loading img2' + ) + }) + + it('should work with image with blob src', async () => { + let browser + try { + browser = await webdriver(appPort, '/blob') + + await check( + () => browser.eval(`document.getElementById("blob-image").src`), + /^blob:/ + ) + await check( + () => browser.eval(`document.getElementById("blob-image").srcset`), + '' + ) + } finally { + if (browser) { + await browser.close() + } + } + }) + + it('should work when using flexbox', async () => { + let browser + try { + browser = await webdriver(appPort, '/flex') + await check(async () => { + const result = await browser.eval( + `document.getElementById('basic-image').width` + ) + if (result === 0) { + throw new Error('Incorrectly loaded image') + } + + return 'result-correct' + }, /result-correct/) + } finally { + if (browser) { + await browser.close() + } + } + }) + + it('should work with sizes and automatically use responsive srcset', async () => { + const browser = await webdriver(appPort, '/sizes') + const id = 'sizes1' + expect(await getSrc(browser, id)).toBe( + '/_next/image?url=%2Fwide.png&w=3840&q=75' + ) + expect(await browser.elementById(id).getAttribute('srcset')).toBe( + '/_next/image?url=%2Fwide.png&w=16&q=75 16w, /_next/image?url=%2Fwide.png&w=32&q=75 32w, /_next/image?url=%2Fwide.png&w=48&q=75 48w, /_next/image?url=%2Fwide.png&w=64&q=75 64w, /_next/image?url=%2Fwide.png&w=96&q=75 96w, /_next/image?url=%2Fwide.png&w=128&q=75 128w, /_next/image?url=%2Fwide.png&w=256&q=75 256w, /_next/image?url=%2Fwide.png&w=384&q=75 384w, /_next/image?url=%2Fwide.png&w=640&q=75 640w, /_next/image?url=%2Fwide.png&w=750&q=75 750w, /_next/image?url=%2Fwide.png&w=828&q=75 828w, /_next/image?url=%2Fwide.png&w=1080&q=75 1080w, /_next/image?url=%2Fwide.png&w=1200&q=75 1200w, /_next/image?url=%2Fwide.png&w=1920&q=75 1920w, /_next/image?url=%2Fwide.png&w=2048&q=75 2048w, /_next/image?url=%2Fwide.png&w=3840&q=75 3840w' + ) + expect(await browser.elementById(id).getAttribute('sizes')).toBe( + '(max-width: 2048px) 1200px, 3840px' + ) + }) + + it('should render no wrappers or sizers and minimal styling with layout-raw', async () => { + let browser + try { + browser = await webdriver(appPort, '/layout-raw') + + const numberOfChildren = await browser.eval( + `document.getElementById('image-container1').children.length` + ) + expect(numberOfChildren).toBe(1) + const childElementType = await browser.eval( + `document.getElementById('image-container1').children[0].nodeName` + ) + expect(childElementType).toBe('IMG') + + expect(await browser.elementById('raw1').getAttribute('style')).toBeNull() + expect(await browser.elementById('raw1').getAttribute('height')).toBe( + '700' + ) + expect(await browser.elementById('raw1').getAttribute('width')).toBe( + '1200' + ) + expect(await browser.elementById('raw1').getAttribute('srcset')).toBe( + `/_next/image?url=%2Fwide.png&w=1200&q=75 1x, /_next/image?url=%2Fwide.png&w=3840&q=75 2x` + ) + expect(await browser.elementById('raw1').getAttribute('loading')).toBe( + 'eager' + ) + + expect(await browser.elementById('raw2').getAttribute('style')).toBe( + 'padding-left:4rem;width:100%;object-position:30% 30%' + ) + expect(await browser.elementById('raw2').getAttribute('height')).toBe( + '700' + ) + expect(await browser.elementById('raw2').getAttribute('width')).toBe( + '1200' + ) + expect(await browser.elementById('raw2').getAttribute('srcset')).toBe( + `/_next/image?url=%2Fwide.png&w=16&q=75 16w, /_next/image?url=%2Fwide.png&w=32&q=75 32w, /_next/image?url=%2Fwide.png&w=48&q=75 48w, /_next/image?url=%2Fwide.png&w=64&q=75 64w, /_next/image?url=%2Fwide.png&w=96&q=75 96w, /_next/image?url=%2Fwide.png&w=128&q=75 128w, /_next/image?url=%2Fwide.png&w=256&q=75 256w, /_next/image?url=%2Fwide.png&w=384&q=75 384w, /_next/image?url=%2Fwide.png&w=640&q=75 640w, /_next/image?url=%2Fwide.png&w=750&q=75 750w, /_next/image?url=%2Fwide.png&w=828&q=75 828w, /_next/image?url=%2Fwide.png&w=1080&q=75 1080w, /_next/image?url=%2Fwide.png&w=1200&q=75 1200w, /_next/image?url=%2Fwide.png&w=1920&q=75 1920w, /_next/image?url=%2Fwide.png&w=2048&q=75 2048w, /_next/image?url=%2Fwide.png&w=3840&q=75 3840w` + ) + expect(await browser.elementById('raw2').getAttribute('loading')).toBe( + 'lazy' + ) + + expect(await browser.elementById('raw3').getAttribute('style')).toBeNull() + expect(await browser.elementById('raw3').getAttribute('srcset')).toBe( + `/_next/image?url=%2Ftest.png&w=640&q=75 1x, /_next/image?url=%2Ftest.png&w=828&q=75 2x` + ) + if (mode === 'dev') { + await waitFor(1000) + const warnings = (await browser.log('browser')) + .map((log) => log.message) + .join('\n') + expect(warnings).toMatch( + /Image with src "\/wide.png" has either width or height modified, but not the other./gm + ) + expect(warnings).not.toMatch( + /Image with src "\/test.png" has either width or height modified, but not the other./gm + ) + } + } finally { + if (browser) { + await browser.close() + } + } + }) + + it('should lazy load layout=raw and placeholder=blur', async () => { + const browser = await webdriver(appPort, '/layout-raw-placeholder-blur') + + // raw1 + expect(await browser.elementById('raw1').getAttribute('src')).toBe( + '/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.fab2915d.jpg&w=828&q=75' + ) + expect(await browser.elementById('raw1').getAttribute('srcset')).toBe( + '/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.fab2915d.jpg&w=640&q=75 1x, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.fab2915d.jpg&w=828&q=75 2x' + ) + expect(await browser.elementById('raw1').getAttribute('loading')).toBe( + 'lazy' + ) + expect(await browser.elementById('raw1').getAttribute('sizes')).toBeNull() + expect(await browser.elementById('raw1').getAttribute('style')).toMatch( + 'background-size:cover;background-position:0% 0%;' + ) + expect(await browser.elementById('raw1').getAttribute('height')).toBe('400') + expect(await browser.elementById('raw1').getAttribute('width')).toBe('400') + await browser.eval( + `document.getElementById("raw1").scrollIntoView({behavior: "smooth"})` + ) + await check( + () => browser.eval(`document.getElementById("raw1").currentSrc`), + /test(.*)jpg/ + ) + expect(await browser.elementById('raw1').getAttribute('src')).toBe( + '/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.fab2915d.jpg&w=828&q=75' + ) + expect(await browser.elementById('raw1').getAttribute('srcset')).toBe( + '/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.fab2915d.jpg&w=640&q=75 1x, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.fab2915d.jpg&w=828&q=75 2x' + ) + expect(await browser.elementById('raw1').getAttribute('loading')).toBe( + 'lazy' + ) + expect(await browser.elementById('raw1').getAttribute('sizes')).toBeNull() + expect(await browser.elementById('raw1').getAttribute('style')).toMatch('') + expect(await browser.elementById('raw1').getAttribute('height')).toBe('400') + expect(await browser.elementById('raw1').getAttribute('width')).toBe('400') + + // raw2 + expect(await browser.elementById('raw2').getAttribute('src')).toBe( + '/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=3840&q=75' + ) + expect(await browser.elementById('raw2').getAttribute('srcset')).toBe( + '/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=384&q=75 384w, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=640&q=75 640w, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=750&q=75 750w, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=828&q=75 828w, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=1080&q=75 1080w, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=1200&q=75 1200w, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=1920&q=75 1920w, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=2048&q=75 2048w, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=3840&q=75 3840w' + ) + expect(await browser.elementById('raw2').getAttribute('sizes')).toBe('50vw') + expect(await browser.elementById('raw2').getAttribute('loading')).toBe( + 'lazy' + ) + expect(await browser.elementById('raw2').getAttribute('style')).toMatch( + 'background-size:cover;background-position:0% 0%;' + ) + expect(await browser.elementById('raw2').getAttribute('height')).toBe('400') + expect(await browser.elementById('raw2').getAttribute('width')).toBe('400') + await browser.eval( + `document.getElementById("raw2").scrollIntoView({behavior: "smooth"})` + ) + await check( + () => browser.eval(`document.getElementById("raw2").currentSrc`), + /test(.*)png/ + ) + expect(await browser.elementById('raw2').getAttribute('src')).toBe( + '/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=3840&q=75' + ) + expect(await browser.elementById('raw2').getAttribute('srcset')).toBe( + '/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=384&q=75 384w, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=640&q=75 640w, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=750&q=75 750w, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=828&q=75 828w, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=1080&q=75 1080w, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=1200&q=75 1200w, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=1920&q=75 1920w, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=2048&q=75 2048w, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=3840&q=75 3840w' + ) + expect(await browser.elementById('raw2').getAttribute('sizes')).toBe('50vw') + expect(await browser.elementById('raw2').getAttribute('loading')).toBe( + 'lazy' + ) + expect(await browser.elementById('raw2').getAttribute('style')).toBe('') + expect(await browser.elementById('raw2').getAttribute('height')).toBe('400') + expect(await browser.elementById('raw2').getAttribute('width')).toBe('400') + }) + + it('should handle the styles prop appropriately', async () => { + const browser = await webdriver(appPort, '/style-prop') + + expect(await browser.elementById('with-styles').getAttribute('style')).toBe( + 'border-radius:10px;padding:10px' + ) + expect( + await browser + .elementById('with-overlapping-styles-intrinsic') + .getAttribute('style') + ).toBe('width:10px;border-radius:10px;margin:15px') + expect( + await browser + .elementById('with-overlapping-styles-raw') + .getAttribute('style') + ).toBe('width:10px;border-radius:10px;margin:15px') + expect( + await browser + .elementById('without-styles-responsive') + .getAttribute('style') + ).toBeNull() + expect( + await browser.elementById('without-styles-raw').getAttribute('style') + ).toBeNull() + }) + + if (mode === 'dev') { + it('should show missing src error', async () => { + const browser = await webdriver(appPort, '/missing-src') + + expect(await hasRedbox(browser)).toBe(true) + expect(await getRedboxHeader(browser)).toContain( + 'Image is missing required "src" property. Make sure you pass "src" in props to the `next/image` component. Received: {"width":200}' + ) + }) + + it('should show invalid src error', async () => { + const browser = await webdriver(appPort, '/invalid-src') + + expect(await hasRedbox(browser)).toBe(true) + expect(await getRedboxHeader(browser)).toContain( + 'Invalid src prop (https://google.com/test.png) on `next/image`, hostname "google.com" is not configured under images in your `next.config.js`' + ) + }) + + it('should show invalid src error when protocol-relative', async () => { + const browser = await webdriver(appPort, '/invalid-src-proto-relative') + + expect(await hasRedbox(browser)).toBe(true) + expect(await getRedboxHeader(browser)).toContain( + 'Failed to parse src "//assets.example.com/img.jpg" on `next/image`, protocol-relative URL (//) must be changed to an absolute URL (http:// or https://)' + ) + }) + + it('should show error when string src and placeholder=blur and blurDataURL is missing', async () => { + const browser = await webdriver(appPort, '/invalid-placeholder-blur') + + expect(await hasRedbox(browser)).toBe(true) + expect(await getRedboxHeader(browser)).toContain( + `Image with src "/test.png" has "placeholder='blur'" property but is missing the "blurDataURL" property.` + ) + }) + + it('should show error when not numeric string width or height', async () => { + const browser = await webdriver(appPort, '/invalid-width-or-height') + + expect(await hasRedbox(browser)).toBe(true) + expect(await getRedboxHeader(browser)).toContain( + `Image with src "/test.jpg" has invalid "width" or "height" property. These should be numeric values.` + ) + }) + + it('should show error when static import and placeholder=blur and blurDataUrl is missing', async () => { + const browser = await webdriver( + appPort, + '/invalid-placeholder-blur-static' + ) + + expect(await hasRedbox(browser)).toBe(true) + expect(await getRedboxHeader(browser)).toMatch( + /Image with src "(.*)bmp" has "placeholder='blur'" property but is missing the "blurDataURL" property/ + ) + }) + + it('should warn when using a very small image with placeholder=blur', async () => { + const browser = await webdriver(appPort, '/small-img-import') + + const warnings = (await browser.log('browser')) + .map((log) => log.message) + .join('\n') + expect(await hasRedbox(browser)).toBe(false) + expect(warnings).toMatch( + /Image with src (.*)jpg(.*) is smaller than 40x40. Consider removing(.*)/gm + ) + }) + + it('should not warn when Image is child of p', async () => { + const browser = await webdriver(appPort, '/inside-paragraph') + + const warnings = (await browser.log('browser')) + .map((log) => log.message) + .join('\n') + expect(await hasRedbox(browser)).toBe(false) + expect(warnings).not.toMatch( + /Expected server HTML to contain a matching/gm + ) + expect(warnings).not.toMatch(/cannot appear as a descendant/gm) + }) + + it('should warn when priority prop is missing on LCP image', async () => { + let browser + try { + browser = await webdriver(appPort, '/priority-missing-warning') + // Wait for image to load: + await check(async () => { + const result = await browser.eval( + `document.getElementById('responsive').naturalWidth` + ) + if (result < 1) { + throw new Error('Image not ready') + } + return 'done' + }, 'done') + await waitFor(1000) + const warnings = (await browser.log('browser')) + .map((log) => log.message) + .join('\n') + expect(await hasRedbox(browser)).toBe(false) + expect(warnings).toMatch( + /Image with src (.*)wide.png(.*) was detected as the Largest Contentful Paint/gm + ) + } finally { + if (browser) { + await browser.close() + } + } + }) + + it('should warn when loader is missing width', async () => { + const browser = await webdriver(appPort, '/invalid-loader') + await browser.eval(`document.querySelector("footer").scrollIntoView()`) + const warnings = (await browser.log('browser')) + .map((log) => log.message) + .join('\n') + expect(await hasRedbox(browser)).toBe(false) + expect(warnings).toMatch( + /Image with src (.*)png(.*) has a "loader" property that does not implement width/gm + ) + expect(warnings).not.toMatch( + /Image with src (.*)jpg(.*) has a "loader" property that does not implement width/gm + ) + expect(warnings).not.toMatch( + /Image with src (.*)webp(.*) has a "loader" property that does not implement width/gm + ) + expect(warnings).not.toMatch( + /Image with src (.*)gif(.*) has a "loader" property that does not implement width/gm + ) + expect(warnings).not.toMatch( + /Image with src (.*)tiff(.*) has a "loader" property that does not implement width/gm + ) + }) + + it('should not warn when svg, even if with loader prop or without', async () => { + const browser = await webdriver(appPort, '/loader-svg') + await browser.eval(`document.querySelector("footer").scrollIntoView()`) + const warnings = (await browser.log('browser')) + .map((log) => log.message) + .join('\n') + expect(await hasRedbox(browser)).toBe(false) + expect(warnings).not.toMatch( + /Image with src (.*) has a "loader" property that does not implement width/gm + ) + expect(await browser.elementById('with-loader').getAttribute('src')).toBe( + '/test.svg?size=256' + ) + expect( + await browser.elementById('with-loader').getAttribute('srcset') + ).toBe('/test.svg?size=128 1x, /test.svg?size=256 2x') + expect( + await browser.elementById('without-loader').getAttribute('src') + ).toBe('/test.svg') + expect( + await browser.elementById('without-loader').getAttribute('srcset') + ).toBe('/test.svg 1x, /test.svg 2x') + }) + + it('should warn at most once even after state change', async () => { + const browser = await webdriver(appPort, '/warning-once') + await browser.eval(`document.querySelector("footer").scrollIntoView()`) + await browser.eval(`document.querySelector("button").click()`) + await browser.eval(`document.querySelector("button").click()`) + const count = await browser.eval( + `document.querySelector("button").textContent` + ) + expect(count).toBe('Count: 2') + await check(async () => { + const result = await browser.eval( + 'document.getElementById("w").naturalWidth' + ) + if (result < 1) { + throw new Error('Image not loaded') + } + return 'done' + }, 'done') + await waitFor(1000) + const warnings = (await browser.log('browser')) + .map((log) => log.message) + .filter((log) => log.startsWith('Image with src')) + expect(warnings[0]).toMatch( + 'Image with src "/test.png" was detected as the Largest Contentful Paint (LCP).' + ) + expect(warnings.length).toBe(1) + }) + } else { + //server-only tests + it('should not create an image folder in server/chunks', async () => { + expect( + existsSync(join(appDir, '.next/server/chunks/static/media')) + ).toBeFalsy() + }) + } + + it('should correctly ignore prose styles', async () => { + let browser + try { + browser = await webdriver(appPort, '/prose') + + const id = 'prose-image' + + // Wait for image to load: + await check(async () => { + const result = await browser.eval( + `document.getElementById(${JSON.stringify(id)}).naturalWidth` + ) + + if (result < 1) { + throw new Error('Image not ready') + } + + return 'result-correct' + }, /result-correct/) + + await waitFor(1000) + + const computedWidth = await getComputed(browser, id, 'width') + const computedHeight = await getComputed(browser, id, 'height') + expect(getRatio(computedWidth, computedHeight)).toBeCloseTo(1, 1) + } finally { + if (browser) { + await browser.close() + } + } + }) + + it('should apply style inheritance for img elements but not wrapper elements', async () => { + let browser + try { + browser = await webdriver(appPort, '/style-inheritance') + + await browser.eval( + `document.querySelector("footer").scrollIntoView({behavior: "smooth"})` + ) + + const imagesWithIds = await browser.eval(` + function foo() { + const imgs = document.querySelectorAll("img[id]"); + for (let img of imgs) { + const br = window.getComputedStyle(img).getPropertyValue("border-radius"); + if (!br) return 'no-border-radius'; + if (br !== '139px') return br; + } + return true; + }() + `) + expect(imagesWithIds).toBe(true) + + const allSpans = await browser.eval(` + function foo() { + const spans = document.querySelectorAll("span"); + for (let span of spans) { + const m = window.getComputedStyle(span).getPropertyValue("margin"); + if (m && m !== '0px') return m; + } + return false; + }() + `) + expect(allSpans).toBe(false) + } finally { + if (browser) { + await browser.close() + } + } + }) + + it('should apply filter style after image loads', async () => { + const browser = await webdriver(appPort, '/style-filter') + await check(() => getSrc(browser, 'img-plain'), /^\/_next\/image/) + await check(() => getSrc(browser, 'img-blur'), /^\/_next\/image/) + await waitFor(1000) + + expect(await getComputedStyle(browser, 'img-plain', 'filter')).toBe( + 'opacity(0.5)' + ) + expect( + await getComputedStyle(browser, 'img-plain', 'background-size') + ).toBe('30%') + expect( + await getComputedStyle(browser, 'img-plain', 'background-image') + ).toMatch('iVBORw0KGgo=') + expect( + await getComputedStyle(browser, 'img-plain', 'background-position') + ).toBe('1px 2px') + + expect(await getComputedStyle(browser, 'img-blur', 'filter')).toBe( + 'opacity(0.5)' + ) + expect(await getComputedStyle(browser, 'img-blur', 'background-size')).toBe( + '30%' + ) + expect( + await getComputedStyle(browser, 'img-blur', 'background-image') + ).toMatch('iVBORw0KGgo=') + expect( + await getComputedStyle(browser, 'img-blur', 'background-position') + ).toBe('1px 2px') + }) + + // Tests that use the `unsized` attribute: + if (mode !== 'dev') { + it('should correctly rotate image', async () => { + const browser = await webdriver(appPort, '/rotated') + + const id = 'exif-rotation-image' + + // Wait for image to load: + await check(async () => { + const result = await browser.eval( + `document.getElementById(${JSON.stringify(id)}).naturalWidth` + ) + + if (result === 0) { + throw new Error('Image not ready') + } + + return 'result-correct' + }, /result-correct/) + + await waitFor(500) + + const computedWidth = await getComputed(browser, id, 'width') + const computedHeight = await getComputed(browser, id, 'height') + expect(getRatio(computedHeight, computedWidth)).toBeCloseTo(0.75, 1) + }) + } + + it('should have blurry placeholder when enabled', async () => { + const html = await renderViaHTTP(appPort, '/blurry-placeholder') + const $html = cheerio.load(html) + + $html('noscript > img').attr('id', 'unused') + + expect($html('#blurry-placeholder-raw')[0].attribs.style).toContain( + `background-size:cover;background-position:0% 0%;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http%3A//www.w3.org/2000/svg' xmlns%3Axlink='http%3A//www.w3.org/1999/xlink' viewBox='0 0 400 400'%3E%3Cfilter id='b' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='50'%3E%3C/feGaussianBlur%3E%3CfeComponentTransfer%3E%3CfeFuncA type='discrete' tableValues='1 1'%3E%3C/feFuncA%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Cimage filter='url(%23b)' x='0' y='0' height='100%25' width='100%25' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8P4nhDwAGuAKPn6cicwAAAABJRU5ErkJggg=='%3E%3C/image%3E%3C/svg%3E")` + ) + + expect($html('#blurry-placeholder-with-lazy')[0].attribs.style).toContain( + `background-size:cover;background-position:0% 0%;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http%3A//www.w3.org/2000/svg' xmlns%3Axlink='http%3A//www.w3.org/1999/xlink' viewBox='0 0 400 400'%3E%3Cfilter id='b' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='50'%3E%3C/feGaussianBlur%3E%3CfeComponentTransfer%3E%3CfeFuncA type='discrete' tableValues='1 1'%3E%3C/feFuncA%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Cimage filter='url(%23b)' x='0' y='0' height='100%25' width='100%25' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mO0/8/wBwAE/wI85bEJ6gAAAABJRU5ErkJggg=='%3E%3C/image%3E%3C/svg%3E")` + ) + }) + + it('should not use blurry placeholder for