diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 437d73e3cc64a7bb4ce614ccf348544fbc4cd58e..2a496da5a0614287f5d54d18e88c490018ad2560 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -229,13 +229,6 @@ export default async function getBaseWebpackConfig( } } - // Normalize defined image host to end in slash - if (config.images?.path) { - if (config.images.path[config.images.path.length - 1] !== '/') { - config.images.path += '/' - } - } - const reactVersion = await getPackageVersion({ cwd: dir, name: 'react' }) const hasReactRefresh: boolean = dev && !isServer const hasJsxRuntime: boolean = @@ -997,7 +990,12 @@ export default async function getBaseWebpackConfig( 'process.env.__NEXT_SCROLL_RESTORATION': JSON.stringify( config.experimental.scrollRestoration ), - 'process.env.__NEXT_IMAGE_OPTS': JSON.stringify(config.images), + 'process.env.__NEXT_IMAGE_OPTS': JSON.stringify({ + sizes: config.images.sizes, + path: config.images.path, + loader: config.images.loader, + autoOptimize: config.images.autoOptimize, + }), 'process.env.__NEXT_ROUTER_BASEPATH': JSON.stringify(config.basePath), 'process.env.__NEXT_HAS_REWRITES': JSON.stringify(hasRewrites), 'process.env.__NEXT_I18N_SUPPORT': JSON.stringify( diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx index 1c68d75785442d0d62af876dbf60b047382e568d..dd35cbbfdbaf2efa01352f4e2939bd6307d46d75 100644 --- a/packages/next/client/image.tsx +++ b/packages/next/client/image.tsx @@ -9,6 +9,9 @@ const loaders: { [key: string]: (props: LoaderProps) => string } = { type ImageData = { sizes?: number[] + loader?: string + path?: string + autoOptimize?: boolean } type ImageProps = Omit< @@ -16,13 +19,15 @@ type ImageProps = Omit< 'src' | 'srcSet' | 'ref' > & { src: string + width: number + height: number quality?: string priority?: boolean lazy?: boolean unoptimized?: boolean } -let imageData: any = process.env.__NEXT_IMAGE_OPTS +const imageData: ImageData = process.env.__NEXT_IMAGE_OPTS as any const breakpoints = imageData.sizes || [640, 1024, 1600] // Auto optimize defaults to on if not specified if (imageData.autoOptimize === undefined) { @@ -83,7 +88,7 @@ type CallLoaderProps = { function callLoader(loaderProps: CallLoaderProps) { let loader = loaders[imageData.loader || 'default'] - return loader({ root: imageData.path, ...loaderProps }) + return loader({ root: imageData.path || '/_next/image', ...loaderProps }) } type SrcSetData = { @@ -136,6 +141,8 @@ function generatePreload({ export default function Image({ src, sizes, + width, + height, unoptimized = false, priority = false, lazy = false, @@ -156,11 +163,6 @@ export default function Image({ lazy = false } - // Normalize provided src - if (src[0] === '/') { - src = src.slice(1) - } - useEffect(() => { const target = thisEl.current @@ -217,8 +219,11 @@ export default function Image({ // it's too late for preloads const shouldPreload = priority && typeof window === 'undefined' + const ratio = (height / width) * 100 + const paddingBottom = `${isNaN(ratio) ? 1 : ratio}%` + return ( -
+
{shouldPreload ? generatePreload({ src, @@ -233,6 +238,13 @@ export default function Image({ className={className} sizes={sizes} ref={thisEl} + style={{ + height: '100%', + left: '0', + position: 'absolute', + top: '0', + width: '100%', + }} />
) @@ -242,6 +254,10 @@ export default function Image({ type LoaderProps = CallLoaderProps & { root: string } +function normalizeSrc(src: string) { + return src[0] === '/' ? src.slice(1) : src +} + function imgixLoader({ root, src, width, quality }: LoaderProps): string { const params = [] let paramsString = '' @@ -257,7 +273,7 @@ function imgixLoader({ root, src, width, quality }: LoaderProps): string { if (params.length) { paramsString = '?' + params.join('&') } - return `${root}${src}${paramsString}` + return `${root}${normalizeSrc(src)}${paramsString}` } function cloudinaryLoader({ root, src, width, quality }: LoaderProps): string { @@ -275,7 +291,7 @@ function cloudinaryLoader({ root, src, width, quality }: LoaderProps): string { if (params.length) { paramsString = params.join(',') + '/' } - return `${root}${paramsString}${src}` + return `${root}${paramsString}${normalizeSrc(src)}` } function defaultLoader({ root, src, width, quality }: LoaderProps): string { diff --git a/packages/next/next-server/server/config.ts b/packages/next/next-server/server/config.ts index 753b0ea0242c8be30926ddbfdf3048491e62f9df..c65065db610f08d22e4776971a8d4ab65ff9e577 100644 --- a/packages/next/next-server/server/config.ts +++ b/packages/next/next-server/server/config.ts @@ -215,6 +215,14 @@ function assignDefaults(userConfig: { [key: string]: any }) { if (result?.images) { const { images } = result + + // Normalize defined image host to end in slash + if (images?.path) { + if (images.path[images.path.length - 1] !== '/') { + images.path += '/' + } + } + if (typeof images !== 'object') { throw new Error( `Specified images should be an object received ${typeof images}` diff --git a/test/integration/image-component/basic/pages/client-side.js b/test/integration/image-component/basic/pages/client-side.js index b4f7f9da18d5bb88511b9bb2200852d13ea22112..3b6f3ac8792c9cd2276cdac67f00c540e3604ef0 100644 --- a/test/integration/image-component/basic/pages/client-side.js +++ b/test/integration/image-component/basic/pages/client-side.js @@ -6,21 +6,49 @@ const ClientSide = () => { return (

This is a client side page

- - + + + + - - Errors diff --git a/test/integration/image-component/basic/pages/errors.js b/test/integration/image-component/basic/pages/errors.js index 20d70cfb4bc7c25ae93fe3291ad8abfe42d8011d..552a5168285e45f1f33fd0bdb98048bbe8476070 100644 --- a/test/integration/image-component/basic/pages/errors.js +++ b/test/integration/image-component/basic/pages/errors.js @@ -5,7 +5,13 @@ const Errors = () => { return (

This is a page with errors

- +
) } diff --git a/test/integration/image-component/basic/pages/index.js b/test/integration/image-component/basic/pages/index.js index 6c9f777ca4a794618eb028f6ac23dd1291ab6122..de450b29ce67df4909584822794f9438b1c996f5 100644 --- a/test/integration/image-component/basic/pages/index.js +++ b/test/integration/image-component/basic/pages/index.js @@ -6,18 +6,34 @@ const Page = () => { return (

Hello World

- - + + { priority host="secondary" src="withpriority2.png" + width={300} + height={400} /> + - Client Side diff --git a/test/integration/image-component/basic/pages/lazy.js b/test/integration/image-component/basic/pages/lazy.js index f352e6bdce413eb32ff5274f4e7ec3014986b988..4d9c223ab356c328245d82ef991396ea0c554249 100644 --- a/test/integration/image-component/basic/pages/lazy.js +++ b/test/integration/image-component/basic/pages/lazy.js @@ -5,28 +5,22 @@ const Lazy = () => { return (

This is a page with lazy-loaded images

- +
diff --git a/test/integration/image-component/basic/test/index.test.js b/test/integration/image-component/basic/test/index.test.js index c47a17bfafb8aa4a0ee8d4d5f8e0182ae46893f2..a358202ee2232c2b0c0ea3da1ac14377aa0f35e6 100644 --- a/test/integration/image-component/basic/test/index.test.js +++ b/test/integration/image-component/basic/test/index.test.js @@ -7,6 +7,7 @@ import { nextStart, nextBuild, waitFor, + check, } from 'next-test-utils' import webdriver from 'next-webdriver' @@ -91,19 +92,20 @@ function lazyLoadingTests() { it('should load the second image after scrolling down', async () => { let viewportHeight = await browser.eval(`window.innerHeight`) let topOfMidImage = await browser.eval( - `document.getElementById('lazy-mid').offsetTop` + `document.getElementById('lazy-mid').parentElement.offsetTop` ) let buffer = 150 await browser.eval( `window.scrollTo(0, ${topOfMidImage - (viewportHeight + buffer)})` ) - await waitFor(200) - expect(await browser.elementById('lazy-mid').getAttribute('src')).toBe( - 'https://example.com/myaccount/foo2.jpg' - ) - expect(await browser.elementById('lazy-mid').getAttribute('srcset')).toBe( - 'https://example.com/myaccount/foo2.jpg?w=480 480w, https://example.com/myaccount/foo2.jpg?w=1024 1024w, https://example.com/myaccount/foo2.jpg?w=1600 1600w' - ) + + await check(() => { + return browser.elementById('lazy-mid').getAttribute('src') + }, 'https://example.com/myaccount/foo2.jpg') + + await check(() => { + return browser.elementById('lazy-mid').getAttribute('srcset') + }, 'https://example.com/myaccount/foo2.jpg?w=480 480w, https://example.com/myaccount/foo2.jpg?w=1024 1024w, https://example.com/myaccount/foo2.jpg?w=1600 1600w') }) it('should not have loaded the third image after scrolling down', async () => { expect( @@ -116,7 +118,7 @@ function lazyLoadingTests() { it('should load the third image, which is unoptimized, after scrolling further down', async () => { let viewportHeight = await browser.eval(`window.innerHeight`) let topOfBottomImage = await browser.eval( - `document.getElementById('lazy-bottom').offsetTop` + `document.getElementById('lazy-bottom').parentElement.offsetTop` ) let buffer = 150 await browser.eval( diff --git a/test/integration/image-component/default/pages/index.js b/test/integration/image-component/default/pages/index.js new file mode 100644 index 0000000000000000000000000000000000000000..be82b9c08b181854b47b8af8e3aab16b12a331cf --- /dev/null +++ b/test/integration/image-component/default/pages/index.js @@ -0,0 +1,14 @@ +import React from 'react' +import Image from 'next/image' + +const Page = () => { + return ( +
+

Hello World

+ +

This is the index page

+
+ ) +} + +export default Page diff --git a/test/integration/image-component/default/public/test.bmp b/test/integration/image-component/default/public/test.bmp new file mode 100644 index 0000000000000000000000000000000000000000..f33feda8616b735b81ededeb14244f820342f24b Binary files /dev/null and b/test/integration/image-component/default/public/test.bmp differ diff --git a/test/integration/image-component/default/public/test.gif b/test/integration/image-component/default/public/test.gif new file mode 100644 index 0000000000000000000000000000000000000000..6bbbd315e9fe876cbdbd61261aceabd359efb49f Binary files /dev/null and b/test/integration/image-component/default/public/test.gif differ diff --git a/test/integration/image-component/default/public/test.jpg b/test/integration/image-component/default/public/test.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d536c882412ed3df0dc162823ca5146bcc033499 Binary files /dev/null and b/test/integration/image-component/default/public/test.jpg differ diff --git a/test/integration/image-component/default/public/test.png b/test/integration/image-component/default/public/test.png new file mode 100644 index 0000000000000000000000000000000000000000..e14fafc5cf3bc63b70914ad20467f40f7fecd572 Binary files /dev/null and b/test/integration/image-component/default/public/test.png differ diff --git a/test/integration/image-component/default/public/test.svg b/test/integration/image-component/default/public/test.svg new file mode 100644 index 0000000000000000000000000000000000000000..025d874f92e6a3cc8ee9de39b8dd8c9fd247073c --- /dev/null +++ b/test/integration/image-component/default/public/test.svg @@ -0,0 +1,13 @@ + + + + + + + diff --git a/test/integration/image-component/default/public/test.tiff b/test/integration/image-component/default/public/test.tiff new file mode 100644 index 0000000000000000000000000000000000000000..c2cc3e203bb3fdb5d828597623a630e6b0e59bfb Binary files /dev/null and b/test/integration/image-component/default/public/test.tiff differ diff --git a/test/integration/image-component/default/test/index.test.js b/test/integration/image-component/default/test/index.test.js new file mode 100644 index 0000000000000000000000000000000000000000..e09e9808ffb516756d9dd5eecd909fae2c5ee4a0 --- /dev/null +++ b/test/integration/image-component/default/test/index.test.js @@ -0,0 +1,89 @@ +/* eslint-env jest */ + +import { join } from 'path' +import { + killApp, + findPort, + launchApp, + nextStart, + nextBuild, + check, +} from 'next-test-utils' +import webdriver from 'next-webdriver' +import fs from 'fs-extra' + +jest.setTimeout(1000 * 30) + +const appDir = join(__dirname, '../') +const nextConfig = join(appDir, 'next.config.js') + +let appPort +let app + +function runTests() { + it('should load the image', 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/) + } finally { + if (browser) { + await browser.close() + } + } + }) +} + +describe('Image Component Tests', () => { + describe('dev mode', () => { + beforeAll(async () => { + appPort = await findPort() + app = await launchApp(appDir, appPort) + }) + afterAll(() => killApp(app)) + + runTests('dev') + }) + + describe('server mode', () => { + beforeAll(async () => { + await nextBuild(appDir) + appPort = await findPort() + app = await nextStart(appDir, appPort) + }) + afterAll(() => killApp(app)) + + runTests('server') + }) + + describe('serverless mode', () => { + beforeAll(async () => { + await fs.writeFile( + nextConfig, + ` + module.exports = { + target: 'serverless' + } + ` + ) + await nextBuild(appDir) + appPort = await findPort() + app = await nextStart(appDir, appPort) + }) + afterAll(async () => { + await fs.unlink(nextConfig) + await killApp(app) + }) + + runTests('serverless') + }) +})