Remove multi-host support for image component and support quality pass-through (#18038)

Co-authored-by: Tim Neutkens <timneutkens@me.com>
This commit is contained in:
Alex Castle 2020-10-20 07:28:01 -07:00 committed by GitHub
parent a6ef24580e
commit 01e6bd1684
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 80 additions and 191 deletions

View file

@ -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 =

View file

@ -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({
<link
rel="preload"
as="image"
href={computeSrc(src, host, unoptimized)}
href={computeSrc(src, unoptimized, quality)}
// @ts-ignore: imagesrcset and imagesizes not yet in the link element type
imagesrcset={generateSrcSet({ src, host, widths })}
imagesrcset={generateSrcSet({ src, widths, quality })}
imagesizes={sizes}
/>
</Head>
@ -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<HTMLImageElement>(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, 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 imgixLoader({ root, src, width }: LoaderProps): string {
return `${root}${src}${width ? '?w=' + width : ''}`
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 cloudinaryLoader({ root, src, width }: LoaderProps): string {
return `${root}${width ? 'w_' + width + '/' : ''}${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'}`
}

View file

@ -1,7 +0,0 @@
import React from 'react'
const page = () => {
return <div>Hello</div>
}
export default page

View file

@ -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'
)
})
})

View file

@ -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',
},
}

View file

@ -6,7 +6,7 @@ const ClientSide = () => {
return (
<div>
<p id="stubtext">This is a client side page</p>
<Image id="basic-image" src="foo.jpg"></Image>
<Image id="basic-image" src="foo.jpg" quality="60"></Image>
<Image id="attribute-test" data-demo="demo-value" src="bar.jpg" />
<Image
id="secondary-image"

View file

@ -6,7 +6,7 @@ const Page = () => {
return (
<div>
<p>Hello World</p>
<Image id="basic-image" src="foo.jpg"></Image>
<Image id="basic-image" src="foo.jpg" quality="60"></Image>
<Image id="attribute-test" data-demo="demo-value" src="bar.jpg" />
<Image
id="secondary-image"

View file

@ -32,7 +32,7 @@ function runTests() {
})
it('should modify src with the loader', async () => {
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(