Skip to content
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

Support for custom image loaders via image component prop #20216

Merged
merged 24 commits into from
Jan 5, 2021
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c78462b
support custom image component url resolvers
atcastle Nov 18, 2020
86e51ed
Merge remote-tracking branch 'origin/canary' into image-component-cus…
atcastle Nov 18, 2020
d464057
Fix inccorect conditional
atcastle Nov 19, 2020
a68391b
Add tests for custom resolver errors
atcastle Nov 19, 2020
83d531f
Add support for resolver alternative to loader property
atcastle Nov 20, 2020
6c3234a
Merge remote-tracking branch 'origin/canary' into image-component-cus…
atcastle Nov 20, 2020
f0e7dc0
Rename registerCustomResolver to registerCustomImageLoader
atcastle Dec 2, 2020
4034c6e
Change to use loader prop instead of _app.js
atcastle Dec 15, 2020
8728227
Merge remote-tracking branch 'origin/canary' into image-component-loa…
atcastle Dec 15, 2020
b98d2d6
Revert obsolete changes
atcastle Dec 15, 2020
e2ade38
One more image-config reversion
atcastle Dec 15, 2020
750e0dc
Update packages/next/client/image.tsx
atcastle Dec 15, 2020
ed8de54
remove redundant 'default' setting
atcastle Dec 15, 2020
cce179e
Merge branch 'image-component-loader-prop' of github.com:azukaru/next…
atcastle Dec 15, 2020
531efe8
Adopt ricokahler's suggestions for refactoring custom loader logic
atcastle Dec 16, 2020
f9cabd2
Merge remote-tracking branch 'origin/canary' into image-component-loa…
atcastle Jan 4, 2021
5301504
Remove support for string identification of built-in loaders
atcastle Jan 4, 2021
7bce34c
Clean up obsolete config destructuring change in image.tsx
atcastle Jan 4, 2021
dc2b6ac
Remove redundant check for invalid default loader
atcastle Jan 4, 2021
60a0415
Export type definition for image loader props
atcastle Jan 4, 2021
d126aad
Fixed mistakenly removed loading prop validation for image component
atcastle Jan 5, 2021
ebb20b8
Update packages/next/client/image.tsx
atcastle Jan 5, 2021
4afaeb5
Add default loaderr validation back into image.tsx
atcastle Jan 5, 2021
5c1563b
Merge branch 'canary' into image-component-loader-prop
Timer Jan 5, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 80 additions & 43 deletions packages/next/client/image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,20 @@ if (typeof window === 'undefined') {
const VALID_LOADING_VALUES = ['lazy', 'eager', undefined] as const
type LoadingValue = typeof VALID_LOADING_VALUES[number]

const loaders = new Map<LoaderValue, (props: LoaderProps) => string>([
export type ImageLoader = (resolverProps: ImageLoaderProps) => string

type ImageLoaderProps = {
atcastle marked this conversation as resolved.
Show resolved Hide resolved
src: string
width: number
quality?: number
}

type DefaultImageLoaderProps = ImageLoaderProps & { root: string }

const loaders = new Map<
LoaderValue,
(props: DefaultImageLoaderProps) => string
>([
['imgix', imgixLoader],
['cloudinary', cloudinaryLoader],
['akamai', akamaiLoader],
Expand All @@ -38,6 +51,7 @@ export type ImageProps = Omit<
'src' | 'srcSet' | 'ref' | 'width' | 'height' | 'loading' | 'style'
> & {
src: string
loader?: ImageLoader | string
atcastle marked this conversation as resolved.
Show resolved Hide resolved
quality?: number | string
priority?: boolean
loading?: LoadingValue
Expand All @@ -59,14 +73,15 @@ export type ImageProps = Omit<
}
)

const config =
((process.env.__NEXT_IMAGE_OPTS as any) as ImageConfig) || imageConfigDefault
const configLoader = config.loader
const {
deviceSizes: configDeviceSizes,
imageSizes: configImageSizes,
loader: configLoader,
path: configPath,
domains: configDomains,
} =
((process.env.__NEXT_IMAGE_OPTS as any) as ImageConfig) || imageConfigDefault
} = config
atcastle marked this conversation as resolved.
Show resolved Hide resolved
// sort smallest to largest
const allSizes = [...configDeviceSizes, ...configImageSizes]
configDeviceSizes.sort((a, b) => a - b)
Expand Down Expand Up @@ -94,28 +109,11 @@ function getWidths(
return { widths, kind: 'x' }
}

type CallLoaderProps = {
src: string
width: number
quality?: number
}

function callLoader(loaderProps: CallLoaderProps) {
const load = loaders.get(configLoader)
if (load) {
return load({ root: configPath, ...loaderProps })
}
throw new Error(
`Unknown "loader" found in "next.config.js". Expected: ${VALID_LOADERS.join(
', '
)}. Received: ${configLoader}`
)
}

type GenImgAttrsData = {
src: string
unoptimized: boolean
layout: LayoutValue
loader: ImageLoader
width?: number
quality?: number
sizes?: string
Expand All @@ -133,6 +131,7 @@ function generateImgAttrs({
width,
quality,
sizes,
loader,
styfle marked this conversation as resolved.
Show resolved Hide resolved
}: GenImgAttrsData): GenImgAttrsResult {
if (unoptimized) {
return { src }
Expand All @@ -141,22 +140,18 @@ function generateImgAttrs({
const { widths, kind } = getWidths(width, layout)
const last = widths.length - 1

const srcSet = widths
.map(
(w, i) =>
`${callLoader({ src, quality, width: w })} ${
kind === 'w' ? w : i + 1
}${kind}`
)
.join(', ')

if (!sizes && kind === 'w') {
sizes = '100vw'
return {
src: loader({ src, quality, width: widths[last] }),
sizes: !sizes && kind === 'w' ? '100vw' : sizes,
srcSet: widths
.map(
(w, i) =>
`${loader({ src, quality, width: w })} ${
kind === 'w' ? w : i + 1
}${kind}`
)
.join(', '),
}

src = callLoader({ src, quality, width: widths[last] })

return { src, sizes, srcSet }
}

function getInt(x: unknown): number | undefined {
Expand All @@ -169,6 +164,19 @@ function getInt(x: unknown): number | undefined {
return undefined
}

function defaultImageLoader(loaderProps: ImageLoaderProps) {
const load = loaders.get(configLoader)
if (!load) {
throw new Error(
`Unknown "loader" found in "next.config.js". Expected: ${VALID_LOADERS.join(
', '
)}. Received: ${configLoader}`
)
}
atcastle marked this conversation as resolved.
Show resolved Hide resolved

return load({ root: configPath, ...loaderProps })
}

export default function Image({
src,
sizes,
Expand All @@ -181,6 +189,7 @@ export default function Image({
height,
objectFit,
objectPosition,
loader = defaultImageLoader,
...all
}: ImageProps) {
let rest: Partial<ImageProps> = all
Expand All @@ -197,6 +206,20 @@ export default function Image({
// Remove property so it's not spread into image:
delete rest['layout']
}
// Handle string specifying a different built-in loader
if (typeof loader === 'string') {
if (!(VALID_LOADERS as ReadonlyArray<string>).includes(loader)) {
throw new Error(
`Unknown "loader" specified as a parameter. Expected a function or one of the following: ${VALID_LOADERS.join(
', '
)}. Received: ${configLoader}`
)
}
const load = loaders.get(loader as LoaderValue) as (
props: DefaultImageLoaderProps
) => string
loader = (props: ImageLoaderProps) => load({ root: configPath, ...props })
}

if (process.env.NODE_ENV !== 'production') {
if (!src) {
Expand Down Expand Up @@ -365,6 +388,7 @@ export default function Image({
width: widthInt,
quality: qualityInt,
sizes,
loader,
})
}

Expand Down Expand Up @@ -402,13 +426,16 @@ export default function Image({

//BUILT IN LOADERS

type LoaderProps = CallLoaderProps & { root: string }

function normalizeSrc(src: string) {
return src[0] === '/' ? src.slice(1) : src
}

function imgixLoader({ root, src, width, quality }: LoaderProps): string {
function imgixLoader({
root,
src,
width,
quality,
}: DefaultImageLoaderProps): string {
// Demo: https://static.imgix.net/daisy.png?format=auto&fit=max&w=300
const params = ['auto=format', 'fit=max', 'w=' + width]
let paramsString = ''
Expand All @@ -422,18 +449,28 @@ function imgixLoader({ root, src, width, quality }: LoaderProps): string {
return `${root}${normalizeSrc(src)}${paramsString}`
}

function akamaiLoader({ root, src, width }: LoaderProps): string {
function akamaiLoader({ root, src, width }: DefaultImageLoaderProps): string {
return `${root}${normalizeSrc(src)}?imwidth=${width}`
}

function cloudinaryLoader({ root, src, width, quality }: LoaderProps): string {
function cloudinaryLoader({
root,
src,
width,
quality,
}: DefaultImageLoaderProps): string {
// Demo: https://res.cloudinary.com/demo/image/upload/w_300,c_limit,q_auto/turtles.jpg
const params = ['f_auto', 'c_limit', 'w_' + width, 'q_' + (quality || 'auto')]
let paramsString = params.join(',') + '/'
return `${root}${paramsString}${normalizeSrc(src)}`
}

function defaultLoader({ root, src, width, quality }: LoaderProps): string {
function defaultLoader({
root,
src,
width,
quality,
}: DefaultImageLoaderProps): string {
if (process.env.NODE_ENV !== 'production') {
const missingValues = []

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
images: {
deviceSizes: [480, 1024, 1600, 2000],
imageSizes: [16, 32, 48, 64],
path: 'https://globalresolver.com/myaccount/',
loader: 'imgix',
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react'
import Image from 'next/image'

const myLoader = ({ src, width, quality }) => {
return `https://customresolver.com/${src}?w~~${width},q~~${quality}`
}

const MyImage = (props) => {
return <Image loader={myLoader} {...props}></Image>
}

const Page = () => {
return (
<div>
<p>Image Client Side Test</p>
<MyImage
id="basic-image"
src="foo.jpg"
loading="eager"
width={300}
height={400}
quality={60}
/>
<Image
id="different-default-image"
src="foo.jpg"
loading="eager"
loader="cloudinary"
width={300}
height={400}
quality={60}
/>
<Image
id="unoptimized-image"
unoptimized
src="https://arbitraryurl.com/foo.jpg"
loading="eager"
width={300}
height={400}
/>
</div>
)
}

export default Page
49 changes: 49 additions & 0 deletions test/integration/image-component/custom-resolver/pages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React from 'react'
import Image from 'next/image'
import Link from 'next/link'

const myLoader = ({ src, width, quality }) => {
return `https://customresolver.com/${src}?w~~${width},q~~${quality}`
}

const MyImage = (props) => {
return <Image loader={myLoader} {...props}></Image>
}

const Page = () => {
return (
<div>
<p>Image SSR Test</p>
<MyImage
id="basic-image"
src="foo.jpg"
loading="eager"
width={300}
height={400}
quality={60}
/>
<Image
id="different-default-image"
src="foo.jpg"
loading="eager"
loader="cloudinary"
width={300}
height={400}
quality={60}
/>
<Image
id="unoptimized-image"
unoptimized
src="https://arbitraryurl.com/foo.jpg"
loading="eager"
width={300}
height={400}
/>
<Link href="/client-side">
<a id="clientlink">Client Side</a>
</Link>
</div>
)
}

export default Page
Loading