-
Notifications
You must be signed in to change notification settings - Fork 27.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Image component foundation #17343
Merged
Merged
Image component foundation #17343
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
5cdd37a
Create stub of image component
atcastle 03e3b96
Expand test coverage to include client-side component usage
atcastle 4a7243a
Basic pass through src on image component
atcastle 354e0ab
Data pipeline from config to browser and server on all targets
atcastle 22e1ddf
Add support for multiple hosts
atcastle 144812a
Add support for the 'unoptimized' attribute to the image component
atcastle 3090223
Add basic loader support and srcsets
atcastle 6749745
Minor cleanups
atcastle 241e835
Simplify image config loading
timneutkens 016aef7
Add priority attribute
atcastle 2aee017
Handle unregistered host
atcastle f02580b
Simplify image config loading
timneutkens bcf5464
Add checking for malformed images property in next.config.js
atcastle 68f7e71
Merge branch 'image-component' of github.com:azukaru/next.js into ima…
atcastle a76af8a
Update packages/next/build/webpack-config.ts
timneutkens f7b832d
fix obsolete test and filepath issues
atcastle 87d650e
Merge branch 'image-component' of github.com:azukaru/next.js into ima…
atcastle 60e057b
Normalize host and src paths
atcastle 2f0098b
Merge remote-tracking branch 'origin/canary' into image-component
atcastle File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,183 @@ | ||
import React, { ReactElement } from 'react' | ||
import Head from '../next-server/lib/head' | ||
|
||
const loaders: { [key: string]: (props: LoaderProps) => string } = { | ||
imgix: imgixLoader, | ||
cloudinary: cloudinaryLoader, | ||
default: defaultLoader, | ||
} | ||
type ImageData = { | ||
hosts: { | ||
[key: string]: { | ||
path: string | ||
loader: string | ||
} | ||
} | ||
breakpoints?: number[] | ||
} | ||
|
||
type ImageProps = { | ||
src: string | ||
host: string | ||
sizes: string | ||
breakpoints: number[] | ||
priority: boolean | ||
unoptimized: boolean | ||
rest: any[] | ||
} | ||
|
||
let imageData: any = process.env.__NEXT_IMAGE_OPTS | ||
const breakpoints = imageData.breakpoints || [640, 1024, 1600] | ||
|
||
function computeSrc(src: string, host: string, unoptimized: boolean): 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) | ||
} | ||
} | ||
|
||
function callLoader(src: string, host: string, width?: number): string { | ||
let loader = loaders[imageData.hosts[host].loader || 'default'] | ||
return loader({ root: imageData.hosts[host].path, filename: src, width }) | ||
} | ||
|
||
type SrcSetData = { | ||
src: string | ||
host: string | ||
widths: number[] | ||
} | ||
|
||
function generateSrcSet({ src, host, widths }: 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`) | ||
.join(', ') | ||
} | ||
|
||
type PreloadData = { | ||
src: string | ||
host: string | ||
widths: number[] | ||
sizes: string | ||
unoptimized: boolean | ||
} | ||
|
||
function generatePreload({ | ||
src, | ||
host, | ||
widths, | ||
unoptimized, | ||
sizes, | ||
}: 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 | ||
// compatible, as the link tag includes all necessary attributes, even if the final two are ignored. | ||
// See: https://web.dev/preload-responsive-images/ | ||
return ( | ||
<Head> | ||
<link | ||
rel="preload" | ||
as="image" | ||
href={computeSrc(src, host, unoptimized)} | ||
// @ts-ignore: imagesrcset and imagesizes not yet in the link element type | ||
imagesrcset={generateSrcSet({ src, host, widths })} | ||
imagesizes={sizes} | ||
/> | ||
</Head> | ||
) | ||
} | ||
|
||
export default function Image({ | ||
src, | ||
host, | ||
sizes, | ||
unoptimized, | ||
priority, | ||
...rest | ||
}: ImageProps) { | ||
// 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' | ||
} | ||
|
||
host = host || 'default' | ||
|
||
// Normalize provided src | ||
if (src[0] === '/') { | ||
src = src.slice(1) | ||
} | ||
|
||
// Generate attribute values | ||
const imgSrc = computeSrc(src, host, unoptimized) | ||
const imgAttributes: { src: string; srcSet?: string } = { src: imgSrc } | ||
if (!unoptimized) { | ||
imgAttributes.srcSet = generateSrcSet({ | ||
src, | ||
host: host, | ||
widths: breakpoints, | ||
}) | ||
} | ||
// No need to add preloads on the client side--by the time the application is hydrated, | ||
// it's too late for preloads | ||
const shouldPreload = priority && typeof window === 'undefined' | ||
|
||
return ( | ||
<div> | ||
{shouldPreload | ||
? generatePreload({ | ||
src, | ||
host, | ||
widths: breakpoints, | ||
unoptimized, | ||
sizes, | ||
}) | ||
: ''} | ||
<img {...rest} {...imgAttributes} sizes={sizes} /> | ||
</div> | ||
) | ||
} | ||
|
||
//BUILT IN LOADERS | ||
|
||
type LoaderProps = { | ||
root: string | ||
filename: string | ||
width?: number | ||
} | ||
|
||
function imgixLoader({ root, filename, width }: LoaderProps): string { | ||
return `${root}${filename}${width ? '?w=' + width : ''}` | ||
timneutkens marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
function cloudinaryLoader({ root, filename, width }: LoaderProps): string { | ||
return `${root}${width ? 'w_' + width + '/' : ''}${filename}` | ||
} | ||
|
||
function defaultLoader({ root, filename }: LoaderProps): string { | ||
return `${root}${filename}` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './dist/client/image' | ||
timneutkens marked this conversation as resolved.
Show resolved
Hide resolved
|
||
export { default } from './dist/client/image' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
module.exports = require('./dist/client/image') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
7 changes: 7 additions & 0 deletions
7
test/integration/image-component/bad-next-config/pages/index.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import React from 'react' | ||
|
||
const page = () => { | ||
return <div>Hello</div> | ||
} | ||
|
||
export default page |
76 changes: 76 additions & 0 deletions
76
test/integration/image-component/bad-next-config/test/index.test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
/* 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: { | ||
hosts: { | ||
secondary: { | ||
path: 'https://examplesecondary.com/images/', | ||
loader: 'cloudinary', | ||
}, | ||
}, | ||
breakpoints: [480, 1024, 1600], | ||
}, | ||
}`, | ||
'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: { | ||
hosts: { | ||
default: { | ||
path: 'https://examplesecondary.com/images/', | ||
loader: 'cloudinary', | ||
}, | ||
secondary: { | ||
loader: 'cloudinary', | ||
}, | ||
}, | ||
breakpoints: [480, 1024, 1600], | ||
}, | ||
}`, | ||
'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' | ||
) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
module.exports = { | ||
images: { | ||
hosts: { | ||
default: { | ||
path: 'https://example.com/myaccount/', | ||
loader: 'imgix', | ||
}, | ||
secondary: { | ||
path: 'https://examplesecondary.com/images/', | ||
loader: 'cloudinary', | ||
}, | ||
}, | ||
breakpoints: [480, 1024, 1600], | ||
}, | ||
} |
31 changes: 31 additions & 0 deletions
31
test/integration/image-component/basic/pages/client-side.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import React from 'react' | ||
import Image from 'next/image' | ||
import Link from 'next/link' | ||
|
||
const ClientSide = () => { | ||
return ( | ||
<div> | ||
<p id="stubtext">This is a client side page</p> | ||
<Image id="basic-image" src="foo.jpg"></Image> | ||
<Image id="attribute-test" data-demo="demo-value" src="bar.jpg" /> | ||
<Image | ||
id="secondary-image" | ||
data-demo="demo-value" | ||
host="secondary" | ||
src="foo2.jpg" | ||
/> | ||
<Image | ||
id="unoptimized-image" | ||
unoptimized | ||
src="https://arbitraryurl.com/foo.jpg" | ||
/> | ||
<Image id="priority-image-client" priority src="withpriorityclient.png" /> | ||
<Image id="preceding-slash-image" src="/fooslash.jpg" priority /> | ||
<Link href="/errors"> | ||
<a id="errorslink">Errors</a> | ||
</Link> | ||
</div> | ||
) | ||
} | ||
|
||
export default ClientSide |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can pass imgSrc instead and avoid recomputing the src?