2020-11-07 00:03:15 +01:00
import React , { ReactElement } from 'react'
2020-10-14 11:57:10 +02:00
import Head from '../next-server/lib/head'
2020-11-07 18:39:14 +01:00
import { toBase64 } from '../next-server/lib/to-base-64'
2020-11-07 00:03:15 +01:00
import { useIntersection } from './use-intersection'
2020-10-14 11:57:10 +02:00
2020-11-11 16:46:48 +01:00
if ( typeof window === 'undefined' ) {
; ( global as any ) . __NEXT_IMAGE_IMPORTED = true
}
2020-10-22 20:59:42 +02:00
const VALID_LOADING_VALUES = [ 'lazy' , 'eager' , undefined ] as const
type LoadingValue = typeof VALID_LOADING_VALUES [ number ]
2020-10-21 21:39:12 +02:00
const loaders = new Map < LoaderKey , ( props : LoaderProps ) = > string > ( [
[ 'imgix' , imgixLoader ] ,
[ 'cloudinary' , cloudinaryLoader ] ,
2020-10-21 21:55:02 +02:00
[ 'akamai' , akamaiLoader ] ,
2020-10-21 21:39:12 +02:00
[ 'default' , defaultLoader ] ,
] )
type LoaderKey = 'imgix' | 'cloudinary' | 'akamai' | 'default'
2020-10-18 12:41:25 +02:00
2020-10-30 15:33:34 +01:00
const VALID_LAYOUT_VALUES = [
2020-10-31 23:34:06 +01:00
'fill' ,
2020-10-30 15:33:34 +01:00
'fixed' ,
'intrinsic' ,
'responsive' ,
undefined ,
] as const
type LayoutValue = typeof VALID_LAYOUT_VALUES [ number ]
2020-10-14 11:57:10 +02:00
type ImageData = {
2020-10-26 21:07:52 +01:00
deviceSizes : number [ ]
2020-10-27 14:19:23 +01:00
imageSizes : number [ ]
2020-10-21 21:39:12 +02:00
loader : LoaderKey
path : string
2020-10-25 06:22:47 +01:00
domains? : string [ ]
2020-10-14 11:57:10 +02:00
}
2020-11-05 20:42:55 +01:00
type ImgElementStyle = NonNullable < JSX.IntrinsicElements [ ' img ' ] [ ' style ' ] >
2020-11-09 07:20:54 +01:00
export type ImageProps = Omit <
2020-10-18 12:41:25 +02:00
JSX . IntrinsicElements [ 'img' ] ,
2020-11-04 17:13:07 +01:00
'src' | 'srcSet' | 'ref' | 'width' | 'height' | 'loading' | 'style'
2020-10-18 12:41:25 +02:00
> & {
2020-10-14 11:57:10 +02:00
src : string
2020-10-27 12:37:55 +01:00
quality? : number | string
2020-10-16 22:04:39 +02:00
priority? : boolean
2020-10-22 20:59:42 +02:00
loading? : LoadingValue
2020-10-16 22:04:39 +02:00
unoptimized? : boolean
2020-11-05 20:42:55 +01:00
objectFit? : ImgElementStyle [ 'objectFit' ]
objectPosition? : ImgElementStyle [ 'objectPosition' ]
2020-10-21 12:03:31 +02:00
} & (
2020-10-31 23:34:06 +01:00
| {
width? : never
height? : never
/** @deprecated Use `layout="fill"` instead */
unsized : true
}
| { width? : never ; height? : never ; layout : 'fill' }
| {
width : number | string
height : number | string
layout? : Exclude < LayoutValue , ' fill ' >
}
2020-10-21 12:03:31 +02:00
)
2020-10-14 11:57:10 +02:00
2020-10-20 18:43:24 +02:00
const imageData : ImageData = process . env . __NEXT_IMAGE_OPTS as any
2020-10-25 06:22:47 +01:00
const {
2020-10-26 21:07:52 +01:00
deviceSizes : configDeviceSizes ,
2020-10-27 14:19:23 +01:00
imageSizes : configImageSizes ,
2020-10-25 06:22:47 +01:00
loader : configLoader ,
path : configPath ,
domains : configDomains ,
} = imageData
2020-10-26 21:07:52 +01:00
// sort smallest to largest
2020-11-03 03:12:46 +01:00
const allSizes = [ . . . configDeviceSizes , . . . configImageSizes ]
2020-10-26 21:07:52 +01:00
configDeviceSizes . sort ( ( a , b ) = > a - b )
2020-11-03 03:12:46 +01:00
allSizes . sort ( ( a , b ) = > a - b )
2020-10-14 11:57:10 +02:00
2020-11-03 03:12:46 +01:00
function getSizes (
2020-11-01 01:26:57 +01:00
width : number | undefined ,
layout : LayoutValue
2020-11-03 03:12:46 +01:00
) : { sizes : number [ ] ; kind : 'w' | 'x' } {
2020-11-01 01:26:57 +01:00
if (
typeof width !== 'number' ||
layout === 'fill' ||
layout === 'responsive'
) {
2020-11-03 03:12:46 +01:00
return { sizes : configDeviceSizes , kind : 'w' }
2020-10-26 21:07:52 +01:00
}
2020-11-03 03:12:46 +01:00
const sizes = [
. . . new Set (
[ width , width * 2 , width * 3 ] . map (
( w ) = > allSizes . find ( ( p ) = > p >= w ) || allSizes [ allSizes . length - 1 ]
)
) ,
]
return { sizes , kind : 'x' }
2020-10-26 15:29:52 +01:00
}
2020-10-20 16:28:01 +02:00
function computeSrc (
src : string ,
unoptimized : boolean ,
2020-11-01 01:26:57 +01:00
layout : LayoutValue ,
2020-10-27 12:37:55 +01:00
width? : number ,
quality? : number
2020-10-20 16:28:01 +02:00
) : string {
2020-10-14 11:57:10 +02:00
if ( unoptimized ) {
return src
}
2020-11-03 03:12:46 +01:00
const { sizes } = getSizes ( width , layout )
const largest = sizes [ sizes . length - 1 ]
2020-10-26 15:29:52 +01:00
return callLoader ( { src , width : largest , quality } )
2020-10-14 11:57:10 +02:00
}
2020-10-20 16:28:01 +02:00
type CallLoaderProps = {
src : string
2020-10-23 16:23:16 +02:00
width : number
2020-10-27 12:37:55 +01:00
quality? : number
2020-10-20 16:28:01 +02:00
}
function callLoader ( loaderProps : CallLoaderProps ) {
2020-10-23 16:23:16 +02:00
const load = loaders . get ( configLoader ) || defaultLoader
2020-10-21 21:39:12 +02:00
return load ( { root : configPath , . . . loaderProps } )
2020-10-14 11:57:10 +02:00
}
type SrcSetData = {
src : string
2020-10-26 15:29:52 +01:00
unoptimized : boolean
2020-11-01 01:26:57 +01:00
layout : LayoutValue
2020-10-27 12:37:55 +01:00
width? : number
quality? : number
2020-10-14 11:57:10 +02:00
}
2020-10-26 15:29:52 +01:00
function generateSrcSet ( {
src ,
unoptimized ,
2020-11-01 01:26:57 +01:00
layout ,
2020-10-26 15:29:52 +01:00
width ,
quality ,
} : SrcSetData ) : string | undefined {
2020-10-14 11:57:10 +02:00
// At each breakpoint, generate an image url using the loader, such as:
// ' www.example.com/foo.jpg?w=480 480w, '
2020-10-26 15:29:52 +01:00
if ( unoptimized ) {
return undefined
}
2020-10-26 21:07:52 +01:00
2020-11-03 03:12:46 +01:00
const { sizes , kind } = getSizes ( width , layout )
return sizes
. map (
( size , i ) = >
` ${ callLoader ( { src , width : size , quality } )} ${
kind === 'w' ? size : i + 1
} $ { kind } `
)
2020-10-14 11:57:10 +02:00
. join ( ', ' )
}
type PreloadData = {
src : string
2020-10-26 15:29:52 +01:00
unoptimized : boolean
2020-11-01 01:26:57 +01:00
layout : LayoutValue
2020-10-26 15:29:52 +01:00
width : number | undefined
2020-10-16 22:04:39 +02:00
sizes? : string
2020-10-27 12:37:55 +01:00
quality? : number
2020-10-14 11:57:10 +02:00
}
function generatePreload ( {
src ,
2020-10-16 22:04:39 +02:00
unoptimized = false ,
2020-11-01 01:26:57 +01:00
layout ,
width ,
2020-10-14 11:57:10 +02:00
sizes ,
2020-10-20 16:28:01 +02:00
quality ,
2020-10-14 11:57:10 +02:00
} : 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"
2020-11-01 01:26:57 +01:00
href = { computeSrc ( src , unoptimized , layout , width , quality ) }
2020-10-14 11:57:10 +02:00
// @ts-ignore: imagesrcset and imagesizes not yet in the link element type
2020-11-01 01:26:57 +01:00
imagesrcset = { generateSrcSet ( {
src ,
unoptimized ,
layout ,
width ,
quality ,
} ) }
2020-10-14 11:57:10 +02:00
imagesizes = { sizes }
/ >
< / Head >
)
}
2020-10-26 15:29:52 +01:00
function getInt ( x : unknown ) : number | undefined {
if ( typeof x === 'number' ) {
return x
}
if ( typeof x === 'string' ) {
return parseInt ( x , 10 )
}
return undefined
}
2020-10-14 11:57:10 +02:00
export default function Image ( {
src ,
sizes ,
2020-10-16 22:04:39 +02:00
unoptimized = false ,
priority = false ,
2020-10-22 20:59:42 +02:00
loading ,
2020-10-17 20:55:29 +02:00
className ,
2020-10-20 16:28:01 +02:00
quality ,
2020-10-21 12:03:31 +02:00
width ,
height ,
2020-11-05 20:42:55 +01:00
objectFit ,
objectPosition ,
2020-10-31 23:34:06 +01:00
. . . all
2020-10-14 11:57:10 +02:00
} : ImageProps ) {
2020-10-31 23:34:06 +01:00
let rest : Partial < ImageProps > = all
let layout : NonNullable < LayoutValue > = sizes ? 'responsive' : 'intrinsic'
let unsized = false
if ( 'unsized' in rest ) {
unsized = Boolean ( rest . unsized )
// Remove property so it's not spread into image:
delete rest [ 'unsized' ]
} else if ( 'layout' in rest ) {
// Override default layout if the user specified one:
if ( rest . layout ) layout = rest . layout
// Remove property so it's not spread into image:
delete rest [ 'layout' ]
}
2020-10-22 20:59:42 +02:00
if ( process . env . NODE_ENV !== 'production' ) {
2020-10-26 15:29:52 +01:00
if ( ! src ) {
throw new Error (
` Image is missing required "src" property. Make sure you pass "src" in props to the \` next/image \` component. Received: ${ JSON . stringify (
2020-10-31 23:34:06 +01:00
{ width , height , quality }
2020-10-26 15:29:52 +01:00
) } `
)
}
2020-10-30 15:33:34 +01:00
if ( ! VALID_LAYOUT_VALUES . includes ( layout ) ) {
throw new Error (
` Image with src " ${ src } " has invalid "layout" property. Provided " ${ layout } " should be one of ${ VALID_LAYOUT_VALUES . map (
String
) . join ( ',' ) } . `
)
}
2020-10-22 20:59:42 +02:00
if ( ! VALID_LOADING_VALUES . includes ( loading ) ) {
throw new Error (
` Image with src " ${ src } " has invalid "loading" property. Provided " ${ loading } " should be one of ${ VALID_LOADING_VALUES . map (
String
) . join ( ',' ) } . `
)
}
if ( priority && loading === 'lazy' ) {
2020-10-22 16:55:51 +02:00
throw new Error (
2020-10-31 23:34:06 +01:00
` Image with src " ${ src } " has both "priority" and "loading='lazy'" properties. Only one should be used. `
2020-10-17 20:55:29 +02:00
)
}
2020-10-31 23:34:06 +01:00
if ( unsized ) {
throw new Error (
` Image with src " ${ src } " has deprecated "unsized" property, which was removed in favor of the "layout='fill'" property `
)
2020-10-30 15:33:34 +01:00
}
}
2020-11-07 00:03:15 +01:00
let isLazy =
! priority && ( loading === 'lazy' || typeof loading === 'undefined' )
2020-11-04 22:14:55 +01:00
if ( src && src . startsWith ( 'data:' ) ) {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
unoptimized = true
2020-11-07 00:03:15 +01:00
isLazy = false
2020-11-04 22:14:55 +01:00
}
2020-11-07 00:03:15 +01:00
const [ setRef , isIntersected ] = useIntersection < HTMLImageElement > ( {
rootMargin : '200px' ,
disabled : ! isLazy ,
} )
const isVisible = ! isLazy || isIntersected
2020-10-17 20:55:29 +02:00
2020-10-27 12:37:55 +01:00
const widthInt = getInt ( width )
const heightInt = getInt ( height )
const qualityInt = getInt ( quality )
2020-10-31 23:34:06 +01:00
let wrapperStyle : JSX.IntrinsicElements [ 'div' ] [ 'style' ] | undefined
let sizerStyle : JSX.IntrinsicElements [ 'div' ] [ 'style' ] | undefined
2020-10-30 15:33:34 +01:00
let sizerSvg : string | undefined
2020-11-05 20:42:55 +01:00
let imgStyle : ImgElementStyle | undefined = {
2020-11-07 00:03:15 +01:00
visibility : isVisible ? 'visible' : 'hidden' ,
2020-10-31 23:34:06 +01:00
position : 'absolute' ,
top : 0 ,
left : 0 ,
bottom : 0 ,
right : 0 ,
boxSizing : 'border-box' ,
padding : 0 ,
border : 'none' ,
margin : 'auto' ,
display : 'block' ,
width : 0 ,
height : 0 ,
minWidth : '100%' ,
maxWidth : '100%' ,
minHeight : '100%' ,
maxHeight : '100%' ,
2020-11-05 20:42:55 +01:00
objectFit ,
objectPosition ,
2020-10-31 23:34:06 +01:00
}
2020-10-24 04:01:15 +02:00
if (
2020-10-26 15:29:52 +01:00
typeof widthInt !== 'undefined' &&
typeof heightInt !== 'undefined' &&
2020-10-31 23:34:06 +01:00
layout !== 'fill'
2020-10-24 04:01:15 +02:00
) {
// <Image src="i.png" width="100" height="100" />
2020-10-26 15:29:52 +01:00
const quotient = heightInt / widthInt
2020-10-30 15:33:34 +01:00
const paddingTop = isNaN ( quotient ) ? '100%' : ` ${ quotient * 100 } % `
if ( layout === 'responsive' ) {
// <Image src="i.png" width="100" height="100" layout="responsive" />
2020-10-31 23:34:06 +01:00
wrapperStyle = {
display : 'block' ,
overflow : 'hidden' ,
position : 'relative' ,
boxSizing : 'border-box' ,
margin : 0 ,
}
sizerStyle = { display : 'block' , boxSizing : 'border-box' , paddingTop }
2020-10-30 15:33:34 +01:00
} else if ( layout === 'intrinsic' ) {
// <Image src="i.png" width="100" height="100" layout="intrinsic" />
wrapperStyle = {
display : 'inline-block' ,
maxWidth : '100%' ,
2020-10-31 23:34:06 +01:00
overflow : 'hidden' ,
position : 'relative' ,
boxSizing : 'border-box' ,
margin : 0 ,
2020-10-30 15:33:34 +01:00
}
sizerStyle = {
2020-10-31 23:34:06 +01:00
boxSizing : 'border-box' ,
display : 'block' ,
2020-10-30 15:33:34 +01:00
maxWidth : '100%' ,
}
sizerSvg = ` <svg width=" ${ widthInt } " height=" ${ heightInt } " xmlns="http://www.w3.org/2000/svg" version="1.1"/> `
} else if ( layout === 'fixed' ) {
// <Image src="i.png" width="100" height="100" layout="fixed" />
wrapperStyle = {
2020-10-31 23:34:06 +01:00
overflow : 'hidden' ,
boxSizing : 'border-box' ,
2020-10-30 15:33:34 +01:00
display : 'inline-block' ,
position : 'relative' ,
width : widthInt ,
height : heightInt ,
}
2020-10-21 12:03:31 +02:00
}
} else if (
2020-10-26 15:29:52 +01:00
typeof widthInt === 'undefined' &&
typeof heightInt === 'undefined' &&
2020-10-31 23:34:06 +01:00
layout === 'fill'
2020-10-21 12:03:31 +02:00
) {
2020-10-31 23:34:06 +01:00
// <Image src="i.png" layout="fill" />
wrapperStyle = {
display : 'block' ,
overflow : 'hidden' ,
position : 'absolute' ,
top : 0 ,
left : 0 ,
bottom : 0 ,
right : 0 ,
boxSizing : 'border-box' ,
margin : 0 ,
2020-10-21 12:03:31 +02:00
}
} else {
2020-10-22 16:55:51 +02:00
// <Image src="i.png" />
2020-10-21 12:03:31 +02:00
if ( process . env . NODE_ENV !== 'production' ) {
2020-10-22 16:55:51 +02:00
throw new Error (
2020-10-31 23:34:06 +01:00
` Image with src " ${ src } " must use "width" and "height" properties or "layout='fill'" property. `
2020-10-21 12:03:31 +02:00
)
}
}
2020-10-20 18:43:24 +02:00
2020-10-26 15:29:52 +01:00
// Generate attribute values
2020-11-01 01:26:57 +01:00
const imgSrc = computeSrc ( src , unoptimized , layout , widthInt , qualityInt )
2020-10-26 15:29:52 +01:00
const imgSrcSet = generateSrcSet ( {
src ,
unoptimized ,
2020-11-01 01:26:57 +01:00
layout ,
width : widthInt ,
2020-10-27 12:37:55 +01:00
quality : qualityInt ,
2020-10-26 15:29:52 +01:00
} )
2020-11-07 18:39:14 +01:00
const imgAttributes : Pick < JSX.IntrinsicElements [ ' img ' ] , ' src ' | ' srcSet ' > = {
src :
'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' ,
}
2020-11-07 00:03:15 +01:00
if ( isVisible ) {
2020-11-07 18:39:14 +01:00
imgAttributes . src = imgSrc
imgAttributes . srcSet = imgSrcSet
2020-10-26 15:29:52 +01:00
}
2020-10-24 04:01:15 +02:00
// 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'
2020-10-31 23:34:06 +01:00
if ( unsized ) {
wrapperStyle = undefined
sizerStyle = undefined
imgStyle = undefined
}
2020-10-14 11:57:10 +02:00
return (
2020-10-22 16:55:51 +02:00
< div style = { wrapperStyle } >
2020-10-30 15:33:34 +01:00
{ shouldPreload
? generatePreload ( {
src ,
2020-11-01 01:26:57 +01:00
layout ,
2020-10-30 15:33:34 +01:00
unoptimized ,
2020-11-01 01:26:57 +01:00
width : widthInt ,
2020-10-30 15:33:34 +01:00
sizes ,
quality : qualityInt ,
} )
: null }
{ sizerStyle ? (
< div style = { sizerStyle } >
{ sizerSvg ? (
< img
style = { { maxWidth : '100%' , display : 'block' } }
alt = ""
aria - hidden = { true }
role = "presentation"
2020-11-07 18:39:14 +01:00
src = { ` data:image/svg+xml;base64, ${ toBase64 ( sizerSvg ) } ` }
2020-10-30 15:33:34 +01:00
/ >
) : null }
< / div >
) : null }
< img
{ . . . rest }
{ . . . imgAttributes }
decoding = "async"
className = { className }
sizes = { sizes }
2020-11-07 00:03:15 +01:00
ref = { setRef }
2020-10-30 15:33:34 +01:00
style = { imgStyle }
/ >
2020-10-14 11:57:10 +02:00
< / div >
)
}
//BUILT IN LOADERS
2020-10-20 16:28:01 +02:00
type LoaderProps = CallLoaderProps & { root : string }
2020-10-14 11:57:10 +02:00
2020-10-20 18:43:24 +02:00
function normalizeSrc ( src : string ) {
return src [ 0 ] === '/' ? src . slice ( 1 ) : src
}
2020-10-20 16:28:01 +02:00
function imgixLoader ( { root , src , width , quality } : LoaderProps ) : string {
2020-11-02 01:30:17 +01:00
// Demo: https://static.imgix.net/daisy.png?format=auto&fit=max&w=300
const params = [ 'auto=format' , 'fit=max' , 'w=' + width ]
2020-10-20 16:28:01 +02:00
let paramsString = ''
if ( quality ) {
params . push ( 'q=' + quality )
}
2020-10-21 21:39:12 +02:00
2020-10-20 16:28:01 +02:00
if ( params . length ) {
paramsString = '?' + params . join ( '&' )
}
2020-10-20 18:43:24 +02:00
return ` ${ root } ${ normalizeSrc ( src ) } ${ paramsString } `
2020-10-14 11:57:10 +02:00
}
2020-10-21 21:31:28 +02:00
function akamaiLoader ( { root , src , width } : LoaderProps ) : string {
2020-10-23 16:23:16 +02:00
return ` ${ root } ${ normalizeSrc ( src ) } ?imwidth= ${ width } `
2020-10-21 21:31:28 +02:00
}
2020-10-20 16:28:01 +02:00
function cloudinaryLoader ( { root , src , width , quality } : LoaderProps ) : string {
2020-11-02 01:30:17 +01:00
// Demo: https://res.cloudinary.com/demo/image/upload/w_300,c_limit/turtles.jpg
const params = [ 'f_auto' , 'c_limit' , 'w_' + width ]
2020-10-20 16:28:01 +02:00
let paramsString = ''
if ( quality ) {
params . push ( 'q_' + quality )
}
if ( params . length ) {
paramsString = params . join ( ',' ) + '/'
}
2020-10-20 18:43:24 +02:00
return ` ${ root } ${ paramsString } ${ normalizeSrc ( src ) } `
2020-10-14 11:57:10 +02:00
}
2020-10-20 16:28:01 +02:00
function defaultLoader ( { root , src , width , quality } : LoaderProps ) : string {
2020-10-25 06:22:47 +01:00
if ( process . env . NODE_ENV !== 'production' ) {
const missingValues = [ ]
// these should always be provided but make sure they are
if ( ! src ) missingValues . push ( 'src' )
if ( ! width ) missingValues . push ( 'width' )
if ( missingValues . length > 0 ) {
throw new Error (
` Next Image Optimization requires ${ missingValues . join (
', '
) } to be provided . Make sure you pass them as props to the \ ` next/image \` component. Received: ${ JSON . stringify (
{ src , width , quality }
) } `
)
}
2020-11-04 21:47:49 +01:00
if ( src . startsWith ( '//' ) ) {
throw new Error (
` Failed to parse src " ${ src } " on \` next/image \` , protocol-relative URL (//) must be changed to an absolute URL (http:// or https://) `
)
}
if ( ! src . startsWith ( '/' ) && configDomains ) {
2020-10-25 06:22:47 +01:00
let parsedSrc : URL
try {
parsedSrc = new URL ( src )
} catch ( err ) {
console . error ( err )
throw new Error (
2020-11-04 21:47:49 +01:00
` Failed to parse src " ${ src } " on \` next/image \` , if using relative image it must start with a leading slash "/" or be an absolute URL (http:// or https://) `
2020-10-25 06:22:47 +01:00
)
}
if ( ! configDomains . includes ( parsedSrc . hostname ) ) {
throw new Error (
2020-10-27 22:55:21 +01:00
` Invalid src prop ( ${ src } ) on \` next/image \` , hostname " ${ parsedSrc . hostname } " is not configured under images in your \` next.config.js \` \ n ` +
2020-11-04 14:03:16 +01:00
` See more info: https://err.sh/next.js/next-image-unconfigured-host `
2020-10-25 06:22:47 +01:00
)
}
}
}
2020-10-27 12:37:55 +01:00
return ` ${ root } ?url= ${ encodeURIComponent ( src ) } &w= ${ width } &q= ${ quality || 75 } `
2020-10-14 11:57:10 +02:00
}