diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx index fc88c8897941090..b1adfe91c7abdae 100644 --- a/packages/next/client/image.tsx +++ b/packages/next/client/image.tsx @@ -108,6 +108,27 @@ export type ImageProps = Omit< blurDataURL?: string unoptimized?: boolean onLoadingComplete?: OnLoadingComplete + /** + * @deprecated Use `fill` prop instead of `layout="fill"` or change import to `next/legacy/image`. + * @see https://nextjs.org/docs/api-reference/next/legacy/image + */ + layout?: string + /** + * @deprecated Use `style` prop instead. + */ + objectFit?: string + /** + * @deprecated Use `style` prop instead. + */ + objectPosition?: string + /** + * @deprecated This prop does not do anything. + */ + lazyBoundary?: string + /** + * @deprecated This prop does not do anything. + */ + lazyRoot?: string } type ImageElementProps = Omit & { @@ -467,6 +488,11 @@ export default function Image({ onLoadingComplete, placeholder = 'empty', blurDataURL, + layout, + objectFit, + objectPosition, + lazyBoundary, + lazyRoot, ...all }: ImageProps) { const configContext = useContext(ImageConfigContext) @@ -479,7 +505,6 @@ export default function Image({ let rest: Partial = all let loader: ImageLoaderWithConfig = rest.loader || defaultLoader - // Remove property so it's not spread on element delete rest.loader @@ -503,6 +528,28 @@ export default function Image({ } } + if (layout) { + if (layout === 'fill') { + fill = true + } + const layoutToStyle: Record | undefined> = { + intrinsic: { maxWidth: '100%', height: 'auto' }, + responsive: { width: '100%', height: 'auto' }, + } + const layoutToSizes: Record = { + responsive: '100vw', + fill: '100vw', + } + const layoutStyle = layoutToStyle[layout] + if (layoutStyle) { + style = { ...style, ...layoutStyle } + } + const layoutSizes = layoutToSizes[layout] + if (layoutSizes && !sizes) { + sizes = layoutSizes + } + } + let staticSrc = '' let widthInt = getInt(width) let heightInt = getInt(height) @@ -546,21 +593,6 @@ export default function Image({ } src = typeof src === 'string' ? src : staticSrc - for (const legacyProp of [ - 'layout', - 'objectFit', - 'objectPosition', - 'lazyBoundary', - 'lazyRoot', - ]) { - if (legacyProp in rest) { - throw new Error( - `Image with src "${src}" has legacy prop "${legacyProp}". Did you forget to run the codemod?` + - `\nRead more: https://nextjs.org/docs/messages/next-image-upgrade-to-13` - ) - } - } - let isLazy = !priority && (loading === 'lazy' || typeof loading === 'undefined') if (src.startsWith('data:') || src.startsWith('blob:')) { @@ -691,6 +723,21 @@ export default function Image({ } } + for (const [legacyKey, legacyValue] of Object.entries({ + layout, + objectFit, + objectPosition, + lazyBoundary, + lazyRoot, + })) { + if (legacyValue) { + warnOnce( + `Image with src "${src}" has legacy prop "${legacyKey}". Did you forget to run the codemod?` + + `\nRead more: https://nextjs.org/docs/messages/next-image-upgrade-to-13` + ) + } + } + if ( typeof window !== 'undefined' && !perfObserver && @@ -737,6 +784,8 @@ export default function Image({ top: 0, right: 0, bottom: 0, + objectFit, + objectPosition, } : {}, showAltText ? {} : { color: 'transparent' }, diff --git a/test/integration/next-image-new/default/pages/legacy-layout-fill.js b/test/integration/next-image-new/default/pages/legacy-layout-fill.js new file mode 100644 index 000000000000000..d0d88416b73cb8c --- /dev/null +++ b/test/integration/next-image-new/default/pages/legacy-layout-fill.js @@ -0,0 +1,26 @@ +import Image from 'next/image' + +export default function Page() { + return ( +
+

Using legacy prop layout="fill"

+

+ Even though we don't support "layout" in next/image, we can try to + correct the style and print a warning. +

+
+ my fill image +
+
+ ) +} diff --git a/test/integration/next-image-new/default/pages/legacy-layout-responsive.js b/test/integration/next-image-new/default/pages/legacy-layout-responsive.js new file mode 100644 index 000000000000000..c35f79c8d308b3e --- /dev/null +++ b/test/integration/next-image-new/default/pages/legacy-layout-responsive.js @@ -0,0 +1,22 @@ +import Image from 'next/image' + +export default function Page() { + return ( +
+

Using legacy prop layout="responsive"

+

+ Even though we don't support "layout" in next/image, we can try to + correct the style and print a warning. +

+ my responsive image +
+ ) +} diff --git a/test/integration/next-image-new/default/test/index.test.ts b/test/integration/next-image-new/default/test/index.test.ts index 1bb5dfb3923ae87..65892992a95c015 100644 --- a/test/integration/next-image-new/default/test/index.test.ts +++ b/test/integration/next-image-new/default/test/index.test.ts @@ -669,6 +669,64 @@ function runTests(mode) { ).toBe('color:transparent') }) + it('should warn when legacy prop layout=fill', async () => { + let browser = await webdriver(appPort, '/legacy-layout-fill') + const img = await browser.elementById('img') + expect(img).toBeDefined() + expect(await img.getAttribute('data-nimg')).toBe('fill') + expect(await img.getAttribute('sizes')).toBe('200px') + expect(await img.getAttribute('src')).toBe( + '/_next/image?url=%2Ftest.jpg&w=3840&q=50' + ) + expect(await img.getAttribute('srcset')).toContain( + '/_next/image?url=%2Ftest.jpg&w=640&q=50 640w,' + ) + expect(await img.getAttribute('style')).toBe( + 'position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;object-fit:cover;object-position:10% 10%;color:transparent' + ) + if (mode === 'dev') { + expect(await hasRedbox(browser)).toBe(false) + const warnings = (await browser.log()) + .map((log) => log.message) + .join('\n') + expect(warnings).toContain( + 'Image with src "/test.jpg" has legacy prop "layout". Did you forget to run the codemod?' + ) + expect(warnings).toContain( + 'Image with src "/test.jpg" has legacy prop "objectFit". Did you forget to run the codemod?' + ) + expect(warnings).toContain( + 'Image with src "/test.jpg" has legacy prop "objectPosition". Did you forget to run the codemod?' + ) + } + }) + + it('should warn when legacy prop layout=responsive', async () => { + let browser = await webdriver(appPort, '/legacy-layout-responsive') + const img = await browser.elementById('img') + expect(img).toBeDefined() + expect(await img.getAttribute('sizes')).toBe('100vw') + expect(await img.getAttribute('data-nimg')).toBe('1') + expect(await img.getAttribute('src')).toBe( + '/_next/image?url=%2Ftest.png&w=3840&q=75' + ) + expect(await img.getAttribute('srcset')).toContain( + '/_next/image?url=%2Ftest.png&w=640&q=75 640w,' + ) + expect(await img.getAttribute('style')).toBe( + 'color:transparent;width:100%;height:auto' + ) + if (mode === 'dev') { + expect(await hasRedbox(browser)).toBe(false) + const warnings = (await browser.log()) + .map((log) => log.message) + .join('\n') + expect(warnings).toContain( + 'Image with src "/test.png" has legacy prop "layout". Did you forget to run the codemod?' + ) + } + }) + if (mode === 'dev') { it('should show missing src error', async () => { const browser = await webdriver(appPort, '/missing-src') diff --git a/test/integration/next-image-new/invalid-layout/pages/index.js b/test/integration/next-image-new/invalid-layout/pages/index.js deleted file mode 100644 index e728970add15324..000000000000000 --- a/test/integration/next-image-new/invalid-layout/pages/index.js +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react' -import Image from 'next/image' -import logo from '../public/logo.png' - -const Page = () => { - return ( -
-

Should not use "layout" prop

- -
- ) -} - -export default Page diff --git a/test/integration/next-image-new/invalid-layout/public/logo.png b/test/integration/next-image-new/invalid-layout/public/logo.png deleted file mode 100644 index e14fafc5cf3bc63..000000000000000 Binary files a/test/integration/next-image-new/invalid-layout/public/logo.png and /dev/null differ diff --git a/test/integration/next-image-new/invalid-layout/test/index.test.ts b/test/integration/next-image-new/invalid-layout/test/index.test.ts deleted file mode 100644 index 3d79162c0849f48..000000000000000 --- a/test/integration/next-image-new/invalid-layout/test/index.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* eslint-env jest */ - -import { join } from 'path' -import { - findPort, - hasRedbox, - killApp, - launchApp, - nextBuild, -} from 'next-test-utils' -import webdriver from 'next-webdriver' - -const appDir = join(__dirname, '../') -let appPort: number -let app -let stderr = '' -const msg = - /Error: Image with src "(.*)logo(.*)png" has legacy prop "layout". Did you forget to run the codemod?./ - -function runTests({ isDev }) { - it('should show error', async () => { - if (isDev) { - const browser = await webdriver(appPort, '/') - expect(await hasRedbox(browser, true)).toBe(true) - expect(stderr).toMatch(msg) - } else { - expect(stderr).toMatch(msg) - } - }) -} - -describe('Missing Import Image Tests', () => { - describe('dev mode', () => { - beforeAll(async () => { - stderr = '' - appPort = await findPort() - app = await launchApp(appDir, appPort, { - onStderr(msg) { - stderr += msg || '' - }, - }) - }) - afterAll(async () => { - if (app) { - await killApp(app) - } - }) - - runTests({ isDev: true }) - }) - - describe('server mode', () => { - beforeAll(async () => { - stderr = '' - const result = await nextBuild(appDir, [], { stderr: true }) - stderr = result.stderr - }) - afterAll(async () => { - if (app) { - await killApp(app) - } - }) - - runTests({ isDev: false }) - }) -}) diff --git a/test/integration/next-image-new/invalid-objectfit/pages/index.js b/test/integration/next-image-new/invalid-objectfit/pages/index.js deleted file mode 100644 index ff4d5726a1a92b7..000000000000000 --- a/test/integration/next-image-new/invalid-objectfit/pages/index.js +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react' -import Image from 'next/image' -import logo from '../public/logo.png' - -const Page = () => { - return ( -
-

Should not use "objectFit" prop

- -
- ) -} - -export default Page diff --git a/test/integration/next-image-new/invalid-objectfit/public/logo.png b/test/integration/next-image-new/invalid-objectfit/public/logo.png deleted file mode 100644 index e14fafc5cf3bc63..000000000000000 Binary files a/test/integration/next-image-new/invalid-objectfit/public/logo.png and /dev/null differ diff --git a/test/integration/next-image-new/invalid-objectfit/test/index.test.ts b/test/integration/next-image-new/invalid-objectfit/test/index.test.ts deleted file mode 100644 index 4304f5b6a1209f5..000000000000000 --- a/test/integration/next-image-new/invalid-objectfit/test/index.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* eslint-env jest */ - -import { join } from 'path' -import { - findPort, - hasRedbox, - killApp, - launchApp, - nextBuild, -} from 'next-test-utils' -import webdriver from 'next-webdriver' - -const appDir = join(__dirname, '../') -let appPort: number -let app -let stderr = '' -const msg = - /Error: Image with src "(.*)logo(.*)png" has legacy prop "objectFit". Did you forget to run the codemod?./ - -function runTests({ isDev }) { - it('should show error', async () => { - if (isDev) { - const browser = await webdriver(appPort, '/') - expect(await hasRedbox(browser, true)).toBe(true) - expect(stderr).toMatch(msg) - } else { - expect(stderr).toMatch(msg) - } - }) -} - -describe('Missing Import Image Tests', () => { - describe('dev mode', () => { - beforeAll(async () => { - stderr = '' - appPort = await findPort() - app = await launchApp(appDir, appPort, { - onStderr(msg) { - stderr += msg || '' - }, - }) - }) - afterAll(async () => { - if (app) { - await killApp(app) - } - }) - - runTests({ isDev: true }) - }) - - describe('server mode', () => { - beforeAll(async () => { - stderr = '' - const result = await nextBuild(appDir, [], { stderr: true }) - stderr = result.stderr - }) - afterAll(async () => { - if (app) { - await killApp(app) - } - }) - - runTests({ isDev: false }) - }) -})