Skip to content

Commit

Permalink
fix: next/image usage from node_modules (vercel#33559)
Browse files Browse the repository at this point in the history
fix: image config not work for `node_modules`

Co-authored-by: Steven <steven@ceriously.com>
  • Loading branch information
2 people authored and natew committed Feb 16, 2022
1 parent 69ab49b commit 02accdd
Show file tree
Hide file tree
Showing 9 changed files with 204 additions and 49 deletions.
91 changes: 46 additions & 45 deletions packages/next/client/image.tsx
@@ -1,4 +1,4 @@
import React, { useRef, useEffect } from 'react'
import React, { useRef, useEffect, useContext, useMemo } from 'react'
import Head from '../shared/lib/head'
import {
ImageConfigComplete,
Expand All @@ -7,7 +7,9 @@ import {
VALID_LOADERS,
} from '../server/image-config'
import { useIntersection } from './use-intersection'
import { ImageConfigContext } from '../shared/lib/image-config-context'

const configEnv = process.env.__NEXT_IMAGE_OPTS as any as ImageConfigComplete
const loadedImageURLs = new Set<string>()
const allImgs = new Map<
string,
Expand All @@ -23,21 +25,17 @@ if (typeof window === 'undefined') {

const VALID_LOADING_VALUES = ['lazy', 'eager', undefined] as const
type LoadingValue = typeof VALID_LOADING_VALUES[number]

type ImageConfig = ImageConfigComplete & { allSizes: number[] }
export type ImageLoader = (resolverProps: ImageLoaderProps) => string

export type ImageLoaderProps = {
config: Readonly<ImageConfig>
src: string
width: number
quality?: number
}

type DefaultImageLoaderProps = ImageLoaderProps & { root: string }

const loaders = new Map<
LoaderValue,
(props: DefaultImageLoaderProps) => string
>([
const loaders = new Map<LoaderValue, (props: ImageLoaderProps) => string>([
['default', defaultLoader],
['imgix', imgixLoader],
['cloudinary', cloudinaryLoader],
Expand Down Expand Up @@ -111,20 +109,8 @@ export type ImageProps = Omit<
onLoadingComplete?: OnLoadingComplete
}

const {
deviceSizes: configDeviceSizes,
imageSizes: configImageSizes,
loader: configLoader,
path: configPath,
domains: configDomains,
} = (process.env.__NEXT_IMAGE_OPTS as any as ImageConfigComplete) ||
imageConfigDefault
// sort smallest to largest
const allSizes = [...configDeviceSizes, ...configImageSizes]
configDeviceSizes.sort((a, b) => a - b)
allSizes.sort((a, b) => a - b)

function getWidths(
{ deviceSizes, allSizes }: ImageConfig,
width: number | undefined,
layout: LayoutValue,
sizes: string | undefined
Expand All @@ -139,9 +125,7 @@ function getWidths(
if (percentSizes.length) {
const smallestRatio = Math.min(...percentSizes) * 0.01
return {
widths: allSizes.filter(
(s) => s >= configDeviceSizes[0] * smallestRatio
),
widths: allSizes.filter((s) => s >= deviceSizes[0] * smallestRatio),
kind: 'w',
}
}
Expand All @@ -152,7 +136,7 @@ function getWidths(
layout === 'fill' ||
layout === 'responsive'
) {
return { widths: configDeviceSizes, kind: 'w' }
return { widths: deviceSizes, kind: 'w' }
}

const widths = [
Expand All @@ -174,6 +158,7 @@ function getWidths(
}

type GenImgAttrsData = {
config: ImageConfig
src: string
unoptimized: boolean
layout: LayoutValue
Expand All @@ -190,6 +175,7 @@ type GenImgAttrsResult = {
}

function generateImgAttrs({
config,
src,
unoptimized,
layout,
Expand All @@ -202,15 +188,15 @@ function generateImgAttrs({
return { src, srcSet: undefined, sizes: undefined }
}

const { widths, kind } = getWidths(width, layout, sizes)
const { widths, kind } = getWidths(config, width, layout, sizes)
const last = widths.length - 1

return {
sizes: !sizes && kind === 'w' ? '100vw' : sizes,
srcSet: widths
.map(
(w, i) =>
`${loader({ src, quality, width: w })} ${
`${loader({ config, src, quality, width: w })} ${
kind === 'w' ? w : i + 1
}${kind}`
)
Expand All @@ -222,7 +208,7 @@ function generateImgAttrs({
// updated by React. That causes multiple unnecessary requests if `srcSet`
// and `sizes` are defined.
// This bug cannot be reproduced in Chrome or Firefox.
src: loader({ src, quality, width: widths[last] }),
src: loader({ config, src, quality, width: widths[last] }),
}
}

Expand All @@ -237,14 +223,15 @@ function getInt(x: unknown): number | undefined {
}

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

Expand Down Expand Up @@ -337,6 +324,15 @@ export default function Image({
...all
}: ImageProps) {
const imgRef = useRef<HTMLImageElement>(null)

const configContext = useContext(ImageConfigContext)
const config: ImageConfig = useMemo(() => {
const c = configEnv || configContext || imageConfigDefault
const allSizes = [...c.deviceSizes, ...c.imageSizes].sort((a, b) => a - b)
const deviceSizes = c.deviceSizes.sort((a, b) => a - b)
return { ...c, allSizes, deviceSizes }
}, [configContext])

let rest: Partial<ImageProps> = all
let layout: NonNullable<LayoutValue> = sizes ? 'responsive' : 'intrinsic'
if ('layout' in rest) {
Expand Down Expand Up @@ -468,6 +464,7 @@ export default function Image({

if (!unoptimized) {
const urlStr = loader({
config,
src,
width: widthInt || 400,
quality: qualityInt || 75,
Expand Down Expand Up @@ -630,6 +627,7 @@ export default function Image({

if (isVisible) {
imgAttributes = generateImgAttrs({
config,
src,
unoptimized,
layout,
Expand Down Expand Up @@ -720,6 +718,7 @@ export default function Image({
<img
{...rest}
{...generateImgAttrs({
config,
src,
unoptimized,
layout,
Expand Down Expand Up @@ -768,13 +767,13 @@ function normalizeSrc(src: string): string {
}

function imgixLoader({
root,
config,
src,
width,
quality,
}: DefaultImageLoaderProps): string {
}: ImageLoaderProps): string {
// Demo: https://static.imgix.net/daisy.png?auto=format&fit=max&w=300
const url = new URL(`${root}${normalizeSrc(src)}`)
const url = new URL(`${config.path}${normalizeSrc(src)}`)
const params = url.searchParams

params.set('auto', params.get('auto') || 'format')
Expand All @@ -788,35 +787,35 @@ function imgixLoader({
return url.href
}

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

function cloudinaryLoader({
root,
config,
src,
width,
quality,
}: DefaultImageLoaderProps): string {
}: ImageLoaderProps): 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')]
const paramsString = params.join(',') + '/'
return `${root}${paramsString}${normalizeSrc(src)}`
return `${config.path}${paramsString}${normalizeSrc(src)}`
}

function customLoader({ src }: DefaultImageLoaderProps): string {
function customLoader({ src }: ImageLoaderProps): string {
throw new Error(
`Image with src "${src}" is missing "loader" prop.` +
`\nRead more: https://nextjs.org/docs/messages/next-image-missing-loader`
)
}

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

Expand All @@ -840,7 +839,7 @@ function defaultLoader({
)
}

if (!src.startsWith('/') && configDomains) {
if (!src.startsWith('/') && config.domains) {
let parsedSrc: URL
try {
parsedSrc = new URL(src)
Expand All @@ -853,7 +852,7 @@ function defaultLoader({

if (
process.env.NODE_ENV !== 'test' &&
!configDomains.includes(parsedSrc.hostname)
!config.domains.includes(parsedSrc.hostname)
) {
throw new Error(
`Invalid src prop (${src}) on \`next/image\`, hostname "${parsedSrc.hostname}" is not configured under images in your \`next.config.js\`\n` +
Expand All @@ -863,5 +862,7 @@ function defaultLoader({
}
}

return `${root}?url=${encodeURIComponent(src)}&w=${width}&q=${quality || 75}`
return `${config.path}?url=${encodeURIComponent(src)}&w=${width}&q=${
quality || 75
}`
}
8 changes: 7 additions & 1 deletion packages/next/client/index.tsx
Expand Up @@ -38,6 +38,8 @@ import {
trackWebVitalMetric,
} from './streaming/vitals'
import { RefreshContext } from './streaming/refresh'
import { ImageConfigContext } from '../shared/lib/image-config-context'
import { ImageConfigComplete } from '../server/image-config'

/// <reference types="react-dom/experimental" />

Expand Down Expand Up @@ -626,7 +628,11 @@ function AppContainer({
>
<RouterContext.Provider value={makePublicRouterInstance(router)}>
<HeadManagerContext.Provider value={headManager}>
{children}
<ImageConfigContext.Provider
value={process.env.__NEXT_IMAGE_OPTS as any as ImageConfigComplete}
>
{children}
</ImageConfigContext.Provider>
</HeadManagerContext.Provider>
</RouterContext.Provider>
</Container>
Expand Down
5 changes: 3 additions & 2 deletions packages/next/server/base-server.ts
Expand Up @@ -58,6 +58,7 @@ import { MIDDLEWARE_ROUTE } from '../lib/constants'
import { addRequestMeta, getRequestMeta } from './request-meta'
import { createHeaderRoute, createRedirectRoute } from './server-route-utils'
import { PrerenderManifest } from '../build'
import { ImageConfigComplete } from './image-config'
import { checkIsManualRevalidate } from '../server/api-utils'

export type FindComponentsResult = {
Expand Down Expand Up @@ -146,7 +147,7 @@ export default abstract class Server {
ampOptimizerConfig?: { [key: string]: any }
basePath: string
optimizeFonts: boolean
images: string
images: ImageConfigComplete
fontManifest?: FontManifest
optimizeImages: boolean
disableOptimizedLoading?: boolean
Expand Down Expand Up @@ -304,7 +305,7 @@ export default abstract class Server {
customServer: customServer === true ? true : undefined,
ampOptimizerConfig: this.nextConfig.experimental.amp?.optimizer,
basePath: this.nextConfig.basePath,
images: JSON.stringify(this.nextConfig.images),
images: this.nextConfig.images,
optimizeFonts: !!this.nextConfig.optimizeFonts && !dev,
fontManifest:
this.nextConfig.optimizeFonts && !dev
Expand Down
8 changes: 7 additions & 1 deletion packages/next/server/render.tsx
Expand Up @@ -61,6 +61,8 @@ import { DomainLocale } from './config'
import RenderResult from './render-result'
import isError from '../lib/is-error'
import { readableStreamTee } from './web/utils'
import { ImageConfigContext } from '../shared/lib/image-config-context'
import { ImageConfigComplete } from './image-config'

let optimizeAmp: typeof import('./optimize-amp').default
let getFontDefinitionFromManifest: typeof import('./font-utils').getFontDefinitionFromManifest
Expand Down Expand Up @@ -232,6 +234,7 @@ export type RenderOptsPartial = {
serverComponents?: boolean
customServer?: boolean
crossOrigin?: string
images: ImageConfigComplete
reactRoot: boolean
}

Expand Down Expand Up @@ -457,6 +460,7 @@ export async function renderToHTML(
basePath,
devOnlyCacheBusterQueryString,
supportsDynamicHTML,
images,
reactRoot,
runtime,
} = renderOpts
Expand Down Expand Up @@ -740,7 +744,9 @@ export async function renderToHTML(
value={(moduleName) => reactLoadableModules.push(moduleName)}
>
<StyleRegistry registry={jsxStyleRegistry}>
{children}
<ImageConfigContext.Provider value={images}>
{children}
</ImageConfigContext.Provider>
</StyleRegistry>
</LoadableContext.Provider>
</HeadManagerContext.Provider>
Expand Down
12 changes: 12 additions & 0 deletions packages/next/shared/lib/image-config-context.ts
@@ -0,0 +1,12 @@
import React from 'react'
import {
ImageConfigComplete,
imageConfigDefault,
} from '../../server/image-config'

export const ImageConfigContext =
React.createContext<ImageConfigComplete>(imageConfigDefault)

if (process.env.NODE_ENV !== 'production') {
ImageConfigContext.displayName = 'ImageConfigContext'
}
@@ -0,0 +1,5 @@
module.exports = {
images: {
domains: ['i.imgur.com'],
},
}

0 comments on commit 02accdd

Please sign in to comment.