diff --git a/errors/manifest.json b/errors/manifest.json index f63ffecb29c5e46..87e687e8cbefcac 100644 --- a/errors/manifest.json +++ b/errors/manifest.json @@ -346,6 +346,10 @@ "title": "next-image-unconfigured-host", "path": "/errors/next-image-unconfigured-host.md" }, + { + "title": "next-image-upgrade-to-13", + "path": "/errors/next-image-upgrade-to-13.md" + }, { "title": "next-script-for-ga", "path": "/errors/next-script-for-ga.md" diff --git a/errors/next-image-upgrade-to-13.md b/errors/next-image-upgrade-to-13.md new file mode 100644 index 000000000000000..eeb42566da13ed9 --- /dev/null +++ b/errors/next-image-upgrade-to-13.md @@ -0,0 +1,40 @@ +# `next/image` changed in version 13 + +#### Why This Error Occurred + +Starting in Next.js 13, the `next/image` component has undergone some major changes. + +Compared to the legacy component, the new `next/image` component has the following changes: + +- Removes `` wrapper around `` in favor of [native computed aspect ratio](https://caniuse.com/mdn-html_elements_img_aspect_ratio_computed_from_attributes) +- Adds support for canonical `style` prop + - Removes `layout` prop in favor of `style` or `className` + - Removes `objectFit` prop in favor of `style` or `className` + - Removes `objectPosition` prop in favor of `style` or `className` +- Removes `IntersectionObserver` implementation in favor of [native lazy loading](https://caniuse.com/loading-lazy-attr) + - Removes `lazyBoundary` prop since there is no native equivalent + - Removes `lazyRoot` prop since there is no native equivalent +- Removes `loader` config in favor of [`loader`](#loader) prop +- Changed `alt` prop from optional to required + +#### Possible Ways to Fix It + +Run the [next-image-to-legacy-image](https://nextjs.org/docs/advanced-features/codemods#next-image-to-legacy-image) codemod to automatically change `next/image` imports to `next/legacy/image`, for example: + +``` +npx @next/codemod next-image-to-legacy-image . +``` + +After running this codemod, you can optionally upgrade `next/legacy/image` to the new `next/image` with another codemod, for example: + +``` +npx @next/codemod next-image-experimental . +``` + +Please note this second codemod is experimental and only covers static usage, not dynamic usage (such ``). + +### Useful Links + +- [Next.js 13 Blog Post](https://nextjs.org/blog/next-13) +- [`next/image` Documentation](https://nextjs.org/docs/api-reference/next/image) +- [`next/legacy/image` Documentation](https://nextjs.org/docs/api-reference/next/legacy/image) diff --git a/packages/next-codemod/transforms/next-image-experimental.ts b/packages/next-codemod/transforms/next-image-experimental.ts index e787164962614e4..9b5ef2952c3232a 100644 --- a/packages/next-codemod/transforms/next-image-experimental.ts +++ b/packages/next-codemod/transforms/next-image-experimental.ts @@ -57,6 +57,12 @@ function findAndReplaceProps( objectPosition = String(a.value.value) return false } + if (a.name.name === 'lazyBoundary') { + return false + } + if (a.name.name === 'lazyRoot') { + return false + } if (a.name.name === 'style') { if ( diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx index 7788f6a9e10f765..87d51e53708499b 100644 --- a/packages/next/client/image.tsx +++ b/packages/next/client/image.tsx @@ -394,24 +394,6 @@ const ImageElement = ({ if (!srcString) { console.error(`Image is missing required "src" property:`, img) } - if ( - img.getAttribute('objectFit') || - img.getAttribute('objectfit') - ) { - console.error( - `Image has unknown prop "objectFit". Did you mean to use the "style" prop instead?`, - img - ) - } - if ( - img.getAttribute('objectPosition') || - img.getAttribute('objectposition') - ) { - console.error( - `Image has unknown prop "objectPosition". Did you mean to use the "style" prop instead?`, - img - ) - } if (img.getAttribute('alt') === null) { console.error( `Image is missing required "alt" property. Please add Alternative Text to describe the image for screen readers and search engines.` @@ -560,6 +542,21 @@ 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:')) { diff --git a/test/integration/next-image-new/base-path/components/TallImage.js b/test/integration/next-image-new/base-path/components/TallImage.js index c0fbbcfe6d63c8c..cb76a505d5c25f3 100644 --- a/test/integration/next-image-new/base-path/components/TallImage.js +++ b/test/integration/next-image-new/base-path/components/TallImage.js @@ -7,12 +7,7 @@ const Page = () => { return (

Static Image

- +
) } diff --git a/test/integration/next-image-new/default/components/TallImage.js b/test/integration/next-image-new/default/components/TallImage.js index c0fbbcfe6d63c8c..cb76a505d5c25f3 100644 --- a/test/integration/next-image-new/default/components/TallImage.js +++ b/test/integration/next-image-new/default/components/TallImage.js @@ -7,12 +7,7 @@ const Page = () => { return (

Static Image

- +
) } diff --git a/test/integration/next-image-new/default/pages/invalid-objectfit.js b/test/integration/next-image-new/default/pages/invalid-objectfit.js deleted file mode 100644 index 01a575bfc829372..000000000000000 --- a/test/integration/next-image-new/default/pages/invalid-objectfit.js +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react' -import Image from 'next/image' - -const Page = () => { - return ( -
- -
- ) -} - -export default Page diff --git a/test/integration/next-image-new/default/pages/on-loading-complete.js b/test/integration/next-image-new/default/pages/on-loading-complete.js index 2568eccd7cc8c57..8d82c92c86cfece 100644 --- a/test/integration/next-image-new/default/pages/on-loading-complete.js +++ b/test/integration/next-image-new/default/pages/on-loading-complete.js @@ -29,7 +29,6 @@ const Page = () => { { width={200} height={200} src="/test.jpg" - objectFit="cover" + fill + style={{ objectFit: 'cover' }} /> { return ( <>

Warning should print at most once

- +
footer here
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 b0d3938d48081a0..11d610184a19062 100644 --- a/test/integration/next-image-new/default/test/index.test.ts +++ b/test/integration/next-image-new/default/test/index.test.ts @@ -912,20 +912,6 @@ function runTests(mode) { ) expect(warnings.length).toBe(1) }) - - it('should show console error for objectFit and objectPosition', async () => { - const browser = await webdriver(appPort, '/invalid-objectfit') - - expect(await hasRedbox(browser)).toBe(false) - - await check(async () => { - return (await browser.log()).map((log) => log.message).join('\n') - }, /Image has unknown prop "objectFit"/gm) - - await check(async () => { - return (await browser.log()).map((log) => log.message).join('\n') - }, /Image has unknown prop "objectPosition"/gm) - }) } else { //server-only tests it('should not create an image folder in server/chunks', async () => { diff --git a/test/integration/next-image-new/invalid-layout/pages/index.js b/test/integration/next-image-new/invalid-layout/pages/index.js new file mode 100644 index 000000000000000..e728970add15324 --- /dev/null +++ b/test/integration/next-image-new/invalid-layout/pages/index.js @@ -0,0 +1,14 @@ +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 new file mode 100644 index 000000000000000..e14fafc5cf3bc63 Binary files /dev/null and b/test/integration/next-image-new/invalid-layout/public/logo.png 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 new file mode 100644 index 000000000000000..3d79162c0849f48 --- /dev/null +++ b/test/integration/next-image-new/invalid-layout/test/index.test.ts @@ -0,0 +1,66 @@ +/* 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 new file mode 100644 index 000000000000000..ff4d5726a1a92b7 --- /dev/null +++ b/test/integration/next-image-new/invalid-objectfit/pages/index.js @@ -0,0 +1,14 @@ +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 new file mode 100644 index 000000000000000..e14fafc5cf3bc63 Binary files /dev/null and b/test/integration/next-image-new/invalid-objectfit/public/logo.png 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 new file mode 100644 index 000000000000000..4304f5b6a1209f5 --- /dev/null +++ b/test/integration/next-image-new/invalid-objectfit/test/index.test.ts @@ -0,0 +1,66 @@ +/* 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 }) + }) +})