diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index f9df1470e32b01c..437d73e3cc64a7b 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -229,25 +229,13 @@ export default async function getBaseWebpackConfig( } } - if (config.images?.hosts) { - if (!config.images.hosts.default) { - // If the image component is being used, a default host must be provided - throw new Error( - 'If the image configuration property is present in next.config.js, it must have a host named "default"' - ) + // Normalize defined image host to end in slash + if (config.images?.path) { + if (config.images.path[config.images.path.length - 1] !== '/') { + config.images.path += '/' } - Object.values(config.images.hosts).forEach((host: any) => { - if (!host.path) { - throw new Error( - 'All hosts defined in the image configuration property of next.config.js must define a path' - ) - } - // Normalize hosts so all paths have trailing slash - if (host.path[host.path.length - 1] !== '/') { - host.path += '/' - } - }) } + const reactVersion = await getPackageVersion({ cwd: dir, name: 'react' }) const hasReactRefresh: boolean = dev && !isServer const hasJsxRuntime: boolean = diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx index 28e5f13579a7044..1c68d75785442d0 100644 --- a/packages/next/client/image.tsx +++ b/packages/next/client/image.tsx @@ -9,12 +9,6 @@ const loaders: { [key: string]: (props: LoaderProps) => string } = { type ImageData = { sizes?: number[] - hosts: { - [key: string]: { - path: string - loader: string - } - } } type ImageProps = Omit< @@ -22,7 +16,7 @@ type ImageProps = Omit< 'src' | 'srcSet' | 'ref' > & { src: string - host?: string + quality?: string priority?: boolean lazy?: boolean unoptimized?: boolean @@ -30,6 +24,10 @@ type ImageProps = Omit< let imageData: any = process.env.__NEXT_IMAGE_OPTS const breakpoints = imageData.sizes || [640, 1024, 1600] +// Auto optimize defaults to on if not specified +if (imageData.autoOptimize === undefined) { + imageData.autoOptimize = true +} let cachedObserver: IntersectionObserver const IntersectionObserver = @@ -66,60 +64,56 @@ function getObserver(): IntersectionObserver | undefined { )) } -function computeSrc(src: string, host: string, unoptimized: boolean): string { +function computeSrc( + src: string, + unoptimized: boolean, + quality?: string +): string { if (unoptimized) { return src } - if (!host) { - // No host provided, use default - return callLoader(src, 'default') - } else { - let selectedHost = imageData.hosts[host] - if (!selectedHost) { - if (process.env.NODE_ENV !== 'production') { - console.error( - `Image tag is used specifying host ${host}, but that host is not defined in next.config` - ) - } - return src - } - return callLoader(src, host) - } + return callLoader({ src, quality }) } -function callLoader(src: string, host: string, width?: number): string { - let loader = loaders[imageData.hosts[host].loader || 'default'] - return loader({ root: imageData.hosts[host].path, src, width }) +type CallLoaderProps = { + src: string + width?: number + quality?: string +} + +function callLoader(loaderProps: CallLoaderProps) { + let loader = loaders[imageData.loader || 'default'] + return loader({ root: imageData.path, ...loaderProps }) } type SrcSetData = { src: string - host: string widths: number[] + quality?: string } -function generateSrcSet({ src, host, widths }: SrcSetData): string { +function generateSrcSet({ src, widths, quality }: SrcSetData): string { // At each breakpoint, generate an image url using the loader, such as: // ' www.example.com/foo.jpg?w=480 480w, ' return widths - .map((width: number) => `${callLoader(src, host, width)} ${width}w`) + .map((width: number) => `${callLoader({ src, width, quality })} ${width}w`) .join(', ') } type PreloadData = { src: string - host: string widths: number[] sizes?: string unoptimized?: boolean + quality?: string } function generatePreload({ src, - host, widths, unoptimized = false, sizes, + quality, }: PreloadData): ReactElement { // This function generates an image preload that makes use of the "imagesrcset" and "imagesizes" // attributes for preloading responsive images. They're still experimental, but fully backward @@ -130,9 +124,9 @@ function generatePreload({ @@ -141,30 +135,17 @@ function generatePreload({ export default function Image({ src, - host, sizes, unoptimized = false, priority = false, lazy = false, className, + quality, ...rest }: ImageProps) { const thisEl = useRef(null) // Sanity Checks: - if (process.env.NODE_ENV !== 'production') { - if (unoptimized && host) { - console.error(`Image tag used specifying both a host and the unoptimized attribute--these are mutually exclusive. - With the unoptimized attribute, no host will be used, so specify an absolute URL.`) - } - } - if (host && !imageData.hosts[host]) { - // If unregistered host is selected, log an error and use the default instead - if (process.env.NODE_ENV !== 'production') { - console.error(`Image host identifier ${host} could not be resolved.`) - } - host = 'default' - } // If priority and lazy are present, log an error and use priority only. if (priority && lazy) { if (process.env.NODE_ENV !== 'production') { @@ -175,8 +156,6 @@ export default function Image({ lazy = false } - host = host || 'default' - // Normalize provided src if (src[0] === '/') { src = src.slice(1) @@ -199,12 +178,12 @@ export default function Image({ }, [thisEl, lazy]) // Generate attribute values - const imgSrc = computeSrc(src, host, unoptimized) + const imgSrc = computeSrc(src, unoptimized, quality) const imgSrcSet = !unoptimized ? generateSrcSet({ src, - host: host, widths: breakpoints, + quality, }) : undefined @@ -243,7 +222,6 @@ export default function Image({ {shouldPreload ? generatePreload({ src, - host, widths: breakpoints, unoptimized, sizes, @@ -262,23 +240,46 @@ export default function Image({ //BUILT IN LOADERS -type LoaderProps = { - root: string - src: string - width?: number -} +type LoaderProps = CallLoaderProps & { root: string } -function imgixLoader({ root, src, width }: LoaderProps): string { - return `${root}${src}${width ? '?w=' + width : ''}` +function imgixLoader({ root, src, width, quality }: LoaderProps): string { + const params = [] + let paramsString = '' + if (width) { + params.push('w=' + width) + } + if (quality) { + params.push('q=' + quality) + } + if (imageData.autoOptimize) { + params.push('auto=compress') + } + if (params.length) { + paramsString = '?' + params.join('&') + } + return `${root}${src}${paramsString}` } -function cloudinaryLoader({ root, src, width }: LoaderProps): string { - return `${root}${width ? 'w_' + width + '/' : ''}${src}` +function cloudinaryLoader({ root, src, width, quality }: LoaderProps): string { + const params = [] + let paramsString = '' + if (!quality && imageData.autoOptimize) { + quality = 'auto' + } + if (width) { + params.push('w_' + width) + } + if (quality) { + params.push('q_' + quality) + } + if (params.length) { + paramsString = params.join(',') + '/' + } + return `${root}${paramsString}${src}` } -function defaultLoader({ root, src, width }: LoaderProps): string { - // TODO: change quality parameter to be configurable +function defaultLoader({ root, src, width, quality }: LoaderProps): string { return `${root}?url=${encodeURIComponent(src)}&${ width ? `w=${width}&` : '' - }q=100` + }q=${quality || '100'}` } diff --git a/test/integration/image-component/bad-next-config/pages/index.js b/test/integration/image-component/bad-next-config/pages/index.js deleted file mode 100644 index ff2547f9c51e922..000000000000000 --- a/test/integration/image-component/bad-next-config/pages/index.js +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react' - -const page = () => { - return
Hello
-} - -export default page diff --git a/test/integration/image-component/bad-next-config/test/index.test.js b/test/integration/image-component/bad-next-config/test/index.test.js deleted file mode 100644 index da8c8963c6fbd47..000000000000000 --- a/test/integration/image-component/bad-next-config/test/index.test.js +++ /dev/null @@ -1,76 +0,0 @@ -/* eslint-env jest */ - -import { join } from 'path' -import { nextBuild } from 'next-test-utils' -import fs from 'fs-extra' - -jest.setTimeout(1000 * 30) - -const appDir = join(__dirname, '../') -const nextConfig = join(appDir, 'next.config.js') - -describe('Next.config.js images prop without default host', () => { - let build - beforeAll(async () => { - await fs.writeFile( - nextConfig, - `module.exports = { - images: { - sizes: [480, 1024, 1600], - hosts: { - secondary: { - path: 'https://examplesecondary.com/images/', - loader: 'cloudinary', - }, - }, - }, - }`, - 'utf8' - ) - build = await nextBuild(appDir, [], { - stdout: true, - stderr: true, - }) - }) - it('Should error during build if images prop in next.config is malformed', () => { - expect(build.stderr).toContain( - 'If the image configuration property is present in next.config.js, it must have a host named "default"' - ) - }) -}) - -describe('Next.config.js images prop without path', () => { - let build - beforeAll(async () => { - await fs.writeFile( - nextConfig, - `module.exports = { - images: { - sizes: [480, 1024, 1600], - hosts: { - default: { - path: 'https://examplesecondary.com/images/', - loader: 'cloudinary', - }, - secondary: { - loader: 'cloudinary', - }, - }, - }, - }`, - 'utf8' - ) - build = await nextBuild(appDir, [], { - stdout: true, - stderr: true, - }) - }) - afterAll(async () => { - await fs.remove(nextConfig) - }) - it('Should error during build if images prop in next.config is malformed', () => { - expect(build.stderr).toContain( - 'All hosts defined in the image configuration property of next.config.js must define a path' - ) - }) -}) diff --git a/test/integration/image-component/basic/next.config.js b/test/integration/image-component/basic/next.config.js index 8ada091227bd4a1..45c304157512976 100644 --- a/test/integration/image-component/basic/next.config.js +++ b/test/integration/image-component/basic/next.config.js @@ -1,15 +1,8 @@ module.exports = { images: { sizes: [480, 1024, 1600], - hosts: { - default: { - path: 'https://example.com/myaccount/', - loader: 'imgix', - }, - secondary: { - path: 'https://examplesecondary.com/images/', - loader: 'cloudinary', - }, - }, + autoOptimize: false, + path: 'https://example.com/myaccount/', + loader: 'imgix', }, } diff --git a/test/integration/image-component/basic/pages/client-side.js b/test/integration/image-component/basic/pages/client-side.js index 0615e65d175aa3a..b4f7f9da18d5bb8 100644 --- a/test/integration/image-component/basic/pages/client-side.js +++ b/test/integration/image-component/basic/pages/client-side.js @@ -6,7 +6,7 @@ const ClientSide = () => { return (

This is a client side page

- + { return (

Hello World

- + { expect(await browser.elementById('basic-image').getAttribute('src')).toBe( - 'https://example.com/myaccount/foo.jpg' + 'https://example.com/myaccount/foo.jpg?q=60' ) }) it('should correctly generate src even if preceding slash is included in prop', async () => { @@ -40,16 +40,11 @@ function runTests() { await browser.elementById('preceding-slash-image').getAttribute('src') ).toBe('https://example.com/myaccount/fooslash.jpg') }) - it('should support manually selecting a different host', async () => { - expect( - await browser.elementById('secondary-image').getAttribute('src') - ).toBe('https://examplesecondary.com/images/foo2.jpg') - }) it('should add a srcset based on the loader', async () => { expect( await browser.elementById('basic-image').getAttribute('srcset') ).toBe( - 'https://example.com/myaccount/foo.jpg?w=480 480w, https://example.com/myaccount/foo.jpg?w=1024 1024w, https://example.com/myaccount/foo.jpg?w=1600 1600w' + 'https://example.com/myaccount/foo.jpg?w=480&q=60 480w, https://example.com/myaccount/foo.jpg?w=1024&q=60 1024w, https://example.com/myaccount/foo.jpg?w=1600&q=60 1600w' ) }) it('should add a srcset even with preceding slash in prop', async () => { @@ -102,6 +97,7 @@ function lazyLoadingTests() { 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' ) @@ -126,6 +122,7 @@ function lazyLoadingTests() { await browser.eval( `window.scrollTo(0, ${topOfBottomImage - (viewportHeight + buffer)})` ) + await waitFor(200) expect(await browser.elementById('lazy-bottom').getAttribute('src')).toBe( 'https://www.otherhost.com/foo3.jpg' ) @@ -178,13 +175,6 @@ describe('Image Component Tests', () => { ) ).toBe(true) }) - it('should add a preload tag for a priority image, with secondary host', async () => { - expect( - await hasPreloadLinkMatchingUrl( - 'https://examplesecondary.com/images/withpriority2.png' - ) - ).toBe(true) - }) it('should add a preload tag for a priority image, with arbitrary host', async () => { expect( await hasPreloadLinkMatchingUrl(