Skip to content

Commit e567aa8

Browse files
ascorbicgatsbybotLB
authored
feat(gatsby-plugin-image): Add resolver helper and improve custom hook (#29342)
* feat(gatsby-plugin-image): Add url builder helper hook * Unify API * Refactor * Fix types * Add resolver * Add breakpoints * Add background color * lint * Fix default handling * Fix import * Apply suggestions from code review Co-authored-by: LB <[email protected]> Co-authored-by: gatsbybot <[email protected]> Co-authored-by: LB <[email protected]>
1 parent 01b6123 commit e567aa8

File tree

6 files changed

+271
-23
lines changed

6 files changed

+271
-23
lines changed

packages/gatsby-plugin-image/src/components/hooks.ts

+98-6
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import {
1919
generateImageData,
2020
Layout,
2121
EVERY_BREAKPOINT,
22+
IImage,
23+
ImageFormat,
2224
} from "../image-utils"
2325
const imageCache = new Set<string>()
2426

@@ -88,24 +90,114 @@ export async function applyPolyfill(
8890
;(window as any).objectFitPolyfill(ref.current)
8991
}
9092

91-
export function useGatsbyImage({
93+
export interface IUrlBuilderArgs<OptionsType> {
94+
width: number
95+
height: number
96+
baseUrl: string
97+
format: ImageFormat
98+
options: OptionsType
99+
}
100+
export interface IUseGatsbyImageArgs<OptionsType = {}> {
101+
baseUrl: string
102+
/**
103+
* For constrained and fixed images, the size of the image element
104+
*/
105+
width?: number
106+
height?: number
107+
/**
108+
* If available, pass the source image width and height
109+
*/
110+
sourceWidth?: number
111+
sourceHeight?: number
112+
/**
113+
* If only one dimension is passed, then this will be used to calculate the other.
114+
*/
115+
aspectRatio?: number
116+
layout?: Layout
117+
/**
118+
* Returns a URL based on the passed arguments. Should be a pure function
119+
*/
120+
urlBuilder: (args: IUrlBuilderArgs<OptionsType>) => string
121+
122+
/**
123+
* Should be a data URI
124+
*/
125+
placeholderURL?: string
126+
backgroundColor?: string
127+
/**
128+
* Used in error messages etc
129+
*/
130+
pluginName?: string
131+
132+
/**
133+
* If you do not support auto-format, pass an array of image types here
134+
*/
135+
formats?: Array<ImageFormat>
136+
137+
breakpoints?: Array<number>
138+
139+
/**
140+
* Passed to the urlBuilder function
141+
*/
142+
options?: OptionsType
143+
}
144+
145+
/**
146+
* Use this hook to generate gatsby-plugin-image data in the browser.
147+
*/
148+
export function useGatsbyImage<OptionsType>({
149+
baseUrl,
150+
urlBuilder,
151+
sourceWidth,
152+
sourceHeight,
92153
pluginName = `useGatsbyImage`,
154+
formats = [`auto`],
93155
breakpoints = EVERY_BREAKPOINT,
94-
...args
95-
}: IGatsbyImageHelperArgs): IGatsbyImageData {
96-
return generateImageData({ pluginName, breakpoints, ...args })
156+
options,
157+
...props
158+
}: IUseGatsbyImageArgs<OptionsType>): IGatsbyImageData {
159+
const generateImageSource = (
160+
baseUrl: string,
161+
width: number,
162+
height?: number,
163+
format?: ImageFormat
164+
): IImage => {
165+
return {
166+
width,
167+
height,
168+
format,
169+
src: urlBuilder({ baseUrl, width, height, options, format }),
170+
}
171+
}
172+
173+
const sourceMetadata: IGatsbyImageHelperArgs["sourceMetadata"] = {
174+
width: sourceWidth,
175+
height: sourceHeight,
176+
format: `auto`,
177+
}
178+
179+
const args: IGatsbyImageHelperArgs = {
180+
...props,
181+
pluginName,
182+
generateImageSource,
183+
filename: baseUrl,
184+
formats,
185+
breakpoints,
186+
sourceMetadata,
187+
}
188+
return generateImageData(args)
97189
}
98190

99191
export function getMainProps(
100192
isLoading: boolean,
101193
isLoaded: boolean,
102-
images: any,
194+
images: IGatsbyImageData["images"],
103195
loading?: "eager" | "lazy",
104196
toggleLoaded?: (loaded: boolean) => void,
105197
cacheKey?: string,
106198
ref?: RefObject<HTMLImageElement>,
107199
style: CSSProperties = {}
108-
): MainImageProps {
200+
): Partial<MainImageProps> {
109201
const onLoad: ReactEventHandler<HTMLImageElement> = function (e) {
110202
if (isLoaded) {
111203
return

packages/gatsby-plugin-image/src/image-utils.ts

+64-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/* eslint-disable no-unused-expressions */
22
import { stripIndent } from "common-tags"
3+
import camelCase from "camelcase"
34
import { IGatsbyImageData } from "."
45

56
const DEFAULT_PIXEL_DENSITIES = [0.25, 0.5, 1, 2]
@@ -19,7 +20,8 @@ export const EVERY_BREAKPOINT = [
1920
4096,
2021
]
2122
const DEFAULT_FLUID_WIDTH = 800
22-
const DEFAULT_FIXED_WIDTH = 400
23+
const DEFAULT_FIXED_WIDTH = 800
24+
const DEFAULT_ASPECT_RATIO = 4 / 3
2325

2426
export type Fit = "cover" | "fill" | "inside" | "outside" | "contain"
2527

@@ -107,6 +109,8 @@ export interface IGatsbyImageHelperArgs {
107109
fit?: Fit
108110
options?: Record<string, unknown>
109111
breakpoints?: Array<number>
112+
backgroundColor?: string
113+
aspectRatio?: number
110114
}
111115

112116
const warn = (message: string): void => console.warn(message)
@@ -150,20 +154,68 @@ export function formatFromFilename(filename: string): ImageFormat | undefined {
150154
return undefined
151155
}
152156

157+
export function setDefaultDimensions(
158+
args: IGatsbyImageHelperArgs
159+
): IGatsbyImageHelperArgs {
160+
let {
161+
layout = `constrained`,
162+
width,
163+
height,
164+
sourceMetadata,
165+
breakpoints,
166+
aspectRatio,
167+
formats = [`auto`, `webp`],
168+
} = args
169+
formats = formats.map(format => format.toLowerCase() as ImageFormat)
170+
layout = camelCase(layout) as Layout
171+
172+
if (width && height) {
173+
return args
174+
}
175+
if (sourceMetadata.width && sourceMetadata.height && !aspectRatio) {
176+
aspectRatio = sourceMetadata.width / sourceMetadata.height
177+
}
178+
179+
if (layout === `fullWidth`) {
180+
width = width || sourceMetadata.width || breakpoints[breakpoints.length - 1]
181+
height = height || Math.round(width / (aspectRatio || DEFAULT_ASPECT_RATIO))
182+
} else {
183+
if (!width) {
184+
if (height && aspectRatio) {
185+
width = height * aspectRatio
186+
} else if (sourceMetadata.width) {
187+
width = sourceMetadata.width
188+
} else if (height) {
189+
width = Math.round(height / DEFAULT_ASPECT_RATIO)
190+
} else {
191+
width = DEFAULT_FIXED_WIDTH
192+
}
193+
}
194+
195+
if (aspectRatio && !height) {
196+
height = Math.round(width / aspectRatio)
197+
}
198+
}
199+
return { ...args, width, height, aspectRatio, layout, formats }
200+
}
201+
153202
export function generateImageData(
154203
args: IGatsbyImageHelperArgs
155204
): IGatsbyImageData {
205+
args = setDefaultDimensions(args)
206+
156207
let {
157208
pluginName,
158209
sourceMetadata,
159210
generateImageSource,
160-
layout = `constrained`,
211+
layout,
161212
fit,
162213
options,
163214
width,
164215
height,
165216
filename,
166217
reporter = { warn },
218+
backgroundColor,
167219
} = args
168220

169221
if (!pluginName) {
@@ -175,18 +227,19 @@ export function generateImageData(
175227
if (typeof generateImageSource !== `function`) {
176228
throw new Error(`generateImageSource must be a function`)
177229
}
230+
178231
if (!sourceMetadata || (!sourceMetadata.width && !sourceMetadata.height)) {
179232
// No metadata means we let the CDN handle max size etc, aspect ratio etc
180233
sourceMetadata = {
181234
width,
182235
height,
183-
format: formatFromFilename(filename),
236+
format: sourceMetadata?.format || formatFromFilename(filename) || `auto`,
184237
}
185238
} else if (!sourceMetadata.format) {
186239
sourceMetadata.format = formatFromFilename(filename)
187240
}
188-
//
189-
const formats = new Set<ImageFormat>(args.formats || [`auto`, `webp`])
241+
242+
const formats = new Set<ImageFormat>(args.formats)
190243

191244
if (formats.size === 0 || formats.has(`auto`) || formats.has(``)) {
192245
formats.delete(`auto`)
@@ -262,7 +315,11 @@ export function generateImageData(
262315
}
263316
})
264317

265-
const imageProps: Partial<IGatsbyImageData> = { images: result, layout }
318+
const imageProps: Partial<IGatsbyImageData> = {
319+
images: result,
320+
layout,
321+
backgroundColor,
322+
}
266323
switch (layout) {
267324
case `fixed`:
268325
imageProps.width = imageSizes.presentationWidth
@@ -317,7 +374,7 @@ export function calculateImageSizes(args: IImageSizeArgs): IImageSizes {
317374
return responsiveImageSizes({ breakpoints, ...args })
318375
} else {
319376
reporter.warn(
320-
`No valid layout was provided for the image at ${filename}. Valid image layouts are fixed, fullWidth, and constrained.`
377+
`No valid layout was provided for the image at ${filename}. Valid image layouts are fixed, fullWidth, and constrained. Found ${layout}`
321378
)
322379
return {
323380
sizes: [imgDimensions.width],

packages/gatsby-plugin-image/src/index.browser.ts

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export {
1313
useGatsbyImage,
1414
useArtDirection,
1515
IArtDirectedImage,
16+
IUseGatsbyImageArgs,
17+
IUrlBuilderArgs,
1618
} from "./components/hooks"
1719
export {
1820
generateImageData,

packages/gatsby-plugin-image/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export {
1212
useGatsbyImage,
1313
useArtDirection,
1414
IArtDirectedImage,
15+
IUseGatsbyImageArgs,
16+
IUrlBuilderArgs,
1517
} from "./components/hooks"
1618
export {
1719
generateImageData,

0 commit comments

Comments
 (0)