Skip to content

Commit 6ed397f

Browse files
ascorbicgatsbybot
and
gatsbybot
authored
feat(gatsby-plugin-image): Add image plugin helpers (#28110)
* Add image helper * Fix type * Fix size calculation * Update test * Fix package.json * Add support for empty metadata * Add CdnImage component * Hooks are nicer * Add resolver utils * Quality shouldn't be a default * Add tests * Move resolver utils into gatsby-plugin-image/graphql * Change export to /graphql-utils Co-authored-by: gatsbybot <[email protected]>
1 parent 911d5e3 commit 6ed397f

16 files changed

+1024
-43
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./dist/resolver-utils"

packages/gatsby-plugin-image/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"version": "0.3.0-next.1",
44
"scripts": {
55
"build": "npm-run-all -s clean -p build:*",
6-
"build:gatsby-node": "tsc --jsx react --downlevelIteration true --skipLibCheck true --esModuleInterop true --outDir dist/ src/gatsby-node.ts src/babel-plugin-parse-static-images.ts src/types.d.ts",
6+
"build:gatsby-node": "tsc --jsx react --downlevelIteration true --skipLibCheck true --esModuleInterop true --outDir dist/ src/gatsby-node.ts src/babel-plugin-parse-static-images.ts src/resolver-utils.ts src/types.d.ts",
77
"build:gatsby-ssr": "microbundle -i src/gatsby-ssr.tsx -f cjs -o ./[name].js --no-pkg-main --jsx React.createElement --no-compress --external=common-tags,react --no-sourcemap",
88
"build:server": "microbundle -f cjs,es --jsx React.createElement --define SERVER=true",
99
"build:browser": "microbundle -i src/index.browser.ts -f cjs,modern,es --jsx React.createElement -o dist/gatsby-image.browser --define SERVER=false",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
import {
2+
formatFromFilename,
3+
generateImageData,
4+
IGatsbyImageHelperArgs,
5+
IImage,
6+
} from "../image-utils"
7+
8+
const generateImageSource = (
9+
file: string,
10+
width: number,
11+
height: number,
12+
format
13+
): IImage => {
14+
return {
15+
src: `https://example.com/${file}/${width}/${height}/image.${format}`,
16+
width,
17+
height,
18+
format,
19+
}
20+
}
21+
22+
const args: IGatsbyImageHelperArgs = {
23+
pluginName: `gatsby-plugin-fake`,
24+
filename: `afile.jpg`,
25+
generateImageSource,
26+
width: 400,
27+
sourceMetadata: {
28+
width: 800,
29+
height: 600,
30+
format: `jpg`,
31+
},
32+
reporter: {
33+
warn: jest.fn(),
34+
},
35+
}
36+
37+
const fluidArgs: IGatsbyImageHelperArgs = {
38+
...args,
39+
width: undefined,
40+
maxWidth: 400,
41+
layout: `fluid`,
42+
}
43+
44+
const constrainedArgs: IGatsbyImageHelperArgs = {
45+
...fluidArgs,
46+
layout: `constrained`,
47+
}
48+
49+
describe(`the image data helper`, () => {
50+
beforeEach(() => {
51+
jest.resetAllMocks()
52+
})
53+
it(`throws if there's not a valid generateImageData function`, () => {
54+
const generateImageSource = `this should be a function`
55+
56+
expect(() =>
57+
generateImageData(({
58+
...args,
59+
generateImageSource,
60+
} as any) as IGatsbyImageHelperArgs)
61+
).toThrow()
62+
})
63+
64+
it(`warns if generateImageSource function returns invalid values`, () => {
65+
const generateImageSource = jest
66+
.fn()
67+
.mockReturnValue({ width: 100, height: 200, src: undefined })
68+
69+
const myArgs = {
70+
...args,
71+
generateImageSource,
72+
}
73+
74+
generateImageData(myArgs)
75+
76+
expect(args.reporter?.warn).toHaveBeenCalledWith(
77+
`[gatsby-plugin-fake] The resolver for image afile.jpg returned an invalid value.`
78+
)
79+
;(args.reporter?.warn as jest.Mock).mockReset()
80+
81+
generateImageSource.mockReturnValue({
82+
width: 100,
83+
height: undefined,
84+
src: `example`,
85+
format: `jpg`,
86+
})
87+
generateImageData(myArgs)
88+
89+
expect(args.reporter?.warn).toHaveBeenCalledWith(
90+
`[gatsby-plugin-fake] The resolver for image afile.jpg returned an invalid value.`
91+
)
92+
;(args.reporter?.warn as jest.Mock).mockReset()
93+
94+
generateImageSource.mockReturnValue({
95+
width: undefined,
96+
height: 100,
97+
src: `example`,
98+
format: `jpg`,
99+
})
100+
generateImageData(myArgs)
101+
102+
expect(args.reporter?.warn).toHaveBeenCalledWith(
103+
`[gatsby-plugin-fake] The resolver for image afile.jpg returned an invalid value.`
104+
)
105+
;(args.reporter?.warn as jest.Mock).mockReset()
106+
107+
generateImageSource.mockReturnValue({
108+
width: 100,
109+
height: 100,
110+
src: `example`,
111+
format: undefined,
112+
})
113+
generateImageData(myArgs)
114+
115+
expect(args.reporter?.warn).toHaveBeenCalledWith(
116+
`[gatsby-plugin-fake] The resolver for image afile.jpg returned an invalid value.`
117+
)
118+
;(args.reporter?.warn as jest.Mock).mockReset()
119+
generateImageSource.mockReturnValue({
120+
width: 100,
121+
height: 100,
122+
src: `example`,
123+
format: `jpg`,
124+
})
125+
generateImageData(myArgs)
126+
expect(args.reporter?.warn).not.toHaveBeenCalled()
127+
})
128+
129+
it(`warns if there's no plugin name`, () => {
130+
generateImageData(({
131+
...args,
132+
pluginName: undefined,
133+
} as any) as IGatsbyImageHelperArgs)
134+
expect(args.reporter?.warn).toHaveBeenCalledWith(
135+
`[gatsby-plugin-image] "generateImageData" was not passed a plugin name`
136+
)
137+
})
138+
139+
it(`calls the generateImageSource function`, () => {
140+
const generateImageSource = jest.fn()
141+
generateImageData({ ...args, generateImageSource })
142+
expect(generateImageSource).toHaveBeenCalledWith(
143+
`afile.jpg`,
144+
800,
145+
600,
146+
`jpg`,
147+
undefined,
148+
undefined
149+
)
150+
})
151+
152+
it(`calculates sizes for fixed`, () => {
153+
const data = generateImageData(args)
154+
expect(data.images.fallback?.sizes).toEqual(`400px`)
155+
})
156+
157+
it(`calculates sizes for fluid`, () => {
158+
const data = generateImageData(fluidArgs)
159+
expect(data.images.fallback?.sizes).toEqual(`100vw`)
160+
})
161+
162+
it(`calculates sizes for constrained`, () => {
163+
const data = generateImageData(constrainedArgs)
164+
expect(data.images.fallback?.sizes).toEqual(
165+
`(min-width: 400px) 400px, 100vw`
166+
)
167+
})
168+
169+
it(`returns URLs for fixed`, () => {
170+
const data = generateImageData(args)
171+
expect(data?.images?.fallback?.src).toEqual(
172+
`https://example.com/afile.jpg/400/300/image.jpg`
173+
)
174+
175+
expect(data.images?.sources?.[0].srcSet).toEqual(
176+
`https://example.com/afile.jpg/400/300/image.webp 400w,\nhttps://example.com/afile.jpg/800/600/image.webp 800w`
177+
)
178+
})
179+
180+
it(`returns URLs for fluid`, () => {
181+
const data = generateImageData(fluidArgs)
182+
expect(data?.images?.fallback?.src).toEqual(
183+
`https://example.com/afile.jpg/400/300/image.jpg`
184+
)
185+
186+
expect(data.images?.sources?.[0].srcSet).toEqual(
187+
`https://example.com/afile.jpg/100/75/image.webp 100w,\nhttps://example.com/afile.jpg/200/150/image.webp 200w,\nhttps://example.com/afile.jpg/400/300/image.webp 400w,\nhttps://example.com/afile.jpg/800/600/image.webp 800w`
188+
)
189+
})
190+
191+
it(`converts to PNG if requested`, () => {
192+
const data = generateImageData({ ...args, formats: [`png`] })
193+
expect(data?.images?.fallback?.src).toEqual(
194+
`https://example.com/afile.jpg/400/300/image.png`
195+
)
196+
})
197+
198+
it(`does not include sources if only jpg or png format is specified`, () => {
199+
let data = generateImageData({ ...args, formats: [`auto`] })
200+
expect(data.images?.sources?.length).toBe(0)
201+
202+
data = generateImageData({ ...args, formats: [`png`] })
203+
expect(data.images?.sources?.length).toBe(0)
204+
205+
data = generateImageData({ ...args, formats: [`jpg`] })
206+
expect(data.images?.sources?.length).toBe(0)
207+
})
208+
209+
it(`does not include fallback if only webp format is specified`, () => {
210+
const data = generateImageData({ ...args, formats: [`webp`] })
211+
expect(data.images?.sources?.length).toBe(1)
212+
expect(data.images?.fallback).toBeUndefined()
213+
})
214+
215+
it(`does not include fallback if only avif format is specified`, () => {
216+
const data = generateImageData({ ...args, formats: [`avif`] })
217+
expect(data.images?.sources?.length).toBe(1)
218+
expect(data.images?.fallback).toBeUndefined()
219+
})
220+
221+
it(`generates the same output as the input format if output is auto`, () => {
222+
const sourceMetadata = {
223+
width: 800,
224+
height: 600,
225+
format: `jpg`,
226+
}
227+
228+
let data = generateImageData({ ...args, formats: [`auto`] })
229+
expect(data?.images?.fallback?.src).toEqual(
230+
`https://example.com/afile.jpg/400/300/image.jpg`
231+
)
232+
expect(data.images?.sources?.length).toBe(0)
233+
234+
data = generateImageData({
235+
...args,
236+
sourceMetadata: { ...sourceMetadata, format: `png` },
237+
formats: [`auto`],
238+
})
239+
expect(data?.images?.fallback?.src).toEqual(
240+
`https://example.com/afile.jpg/400/300/image.png`
241+
)
242+
expect(data.images?.sources?.length).toBe(0)
243+
})
244+
245+
it(`treats empty formats or empty string as auto`, () => {
246+
let data = generateImageData({ ...args, formats: [``] })
247+
expect(data?.images?.fallback?.src).toEqual(
248+
`https://example.com/afile.jpg/400/300/image.jpg`
249+
)
250+
expect(data.images?.sources?.length).toBe(0)
251+
252+
data = generateImageData({ ...args, formats: [] })
253+
expect(data?.images?.fallback?.src).toEqual(
254+
`https://example.com/afile.jpg/400/300/image.jpg`
255+
)
256+
expect(data.images?.sources?.length).toBe(0)
257+
})
258+
})
259+
260+
describe(`the helper utils`, () => {
261+
it(`gets file format from filename`, () => {
262+
const names = [
263+
`filename.jpg`,
264+
`filename.jpeg`,
265+
`filename.png`,
266+
`filename.heic`,
267+
`filename.jp`,
268+
`filename.jpgjpg`,
269+
`file.name.jpg`,
270+
`file.name.`,
271+
`filenamejpg`,
272+
`.jpg`,
273+
]
274+
const expected = [
275+
`jpg`,
276+
`jpg`,
277+
`png`,
278+
`heic`,
279+
undefined,
280+
undefined,
281+
`jpg`,
282+
undefined,
283+
undefined,
284+
`jpg`,
285+
]
286+
for (const idx in names) {
287+
const ext = formatFromFilename(names[idx])
288+
expect(ext).toBe(expected[idx])
289+
}
290+
})
291+
})

0 commit comments

Comments
 (0)