Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Co-authored-by: Tim Neutkens <timneutkens@me.com> Co-authored-by: Tim Neutkens <tim@timneutkens.nl>
- Loading branch information
1 parent
9a5a152
commit 87175fe
Showing
14 changed files
with
564 additions
and
1 deletion.
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 : ''}` | ||
} | ||
|
||
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' | ||
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.