Skip to content

Commit 4a780fb

Browse files
kathmbeckpieh
andauthored
feat: image and file cdn url generator adapter implementation (#38685)
* alternate image url construction * try using image cdn in e2e site * Update netlify.toml * have separate check for dispatching image and file service * fix tests? * try to use images from deploy (so we can avoid using ones hosted externally) * replicate prod-runtime imagecdn tests in adapters * fix import * adjusting remote-file tests * adjusting remote-file tests 2 * cleanup/test * assert naturalWidth/height in image-cdn tests (both adapters and production-runtime) * remove unused * don't use path prefix for alternate image cdn url * _gatsby/file is prefixed * feat: move custom image cdn url generator implementation to adapter (#38715) * feat: move custom image cdn url generator implementation to adapter * provide public types for custom image cdn url generator function signature and individual arguments * use position/cover * update comment * update docs * chore: types/jsdocs shuffle * apply suggestion from https://github.com/gatsbyjs/gatsby/pull/38685\#discussion_r1414135797 * remove docs from feature branch * feat: provide custom FILE_CDN url generator from adapter (#38735) * feat: provide file cdn from adapters * update test * fix tests * use edge function for non-image File CDN * why edge function was not deployed? * bump netlify-cli * ? * bump node for adapters tests * add execa to dev deps * cleanup * some jsdocs updates * add note that generated urls ideally are relative, but can be absolute as well * feat: allow adding remote file allowed url patterns (#38719) * feat: move custom image cdn url generator implementation to adapter * provide public types for custom image cdn url generator function signature and individual arguments * feat: allow adding image cdn allowed url patterns * Module.createRequireFromPath doesn't exist anymore in Node 18, and because package requires at least that version we remove it * fix contentful source image url * fix wordpress source image url * rename ImageCdnAllowed to RemoteFileAllowed as it's not just for image cdn * compare allowed remote urls in netlify.toml with ones generated by gatsby * url testing in filecdn * jsdocs * print warnings for netlify.toml about missing remote_images patterns * test if any existing pattern in netlify.toml allow needed remote url instead of just string comparison * chore: update adapter README about imageCDN * use correct remote_images for adapters e2e site --------- Co-authored-by: Michal Piechowiak <misiek.piechowiak@gmail.com>
1 parent f8c207b commit 4a780fb

49 files changed

Lines changed: 2177 additions & 80 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.circleci/config.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,8 @@ jobs:
455455

456456
e2e_tests_adapters:
457457
<<: *e2e-executor
458+
docker:
459+
- image: cypress/browsers:node-18.16.1-chrome-114.0.5735.133-1-ff-114.0.2-edge-114.0.1823.51-1
458460
steps:
459461
- run: echo 'export CYPRESS_RECORD_KEY="${CY_CLOUD_ADAPTERS}"' >> "$BASH_ENV"
460462
- e2e-test:
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
Cypress.on("uncaught:exception", err => {
2+
if (
3+
(err.message.includes("Minified React error #418") ||
4+
err.message.includes("Minified React error #423") ||
5+
err.message.includes("Minified React error #425")) &&
6+
Cypress.env(`TEST_PLUGIN_OFFLINE`)
7+
) {
8+
return false
9+
}
10+
})
11+
12+
const PATH_PREFIX = Cypress.env(`PATH_PREFIX`) || ``
13+
14+
// there are multiple scenarios we want to test and ensure that custom image cdn url is used:
15+
// - child build process (SSG, Page Query)
16+
// - main build process (SSG, Page Context)
17+
// - query engine (SSR, Page Query)
18+
const configs = [
19+
{
20+
title: `remote-file (SSG, Page Query)`,
21+
pagePath: `/routes/remote-file/`,
22+
placeholders: true,
23+
},
24+
{
25+
title: `remote-file (SSG, Page Context)`,
26+
pagePath: `/routes/remote-file-data-from-context/`,
27+
placeholders: true,
28+
},
29+
{
30+
title: `remote-file (SSR, Page Query)`,
31+
pagePath: `/routes/ssr/remote-file/`,
32+
placeholders: false,
33+
},
34+
]
35+
36+
for (const config of configs) {
37+
describe(
38+
config.title,
39+
{
40+
retries: {
41+
runMode: 4,
42+
},
43+
},
44+
() => {
45+
beforeEach(() => {
46+
cy.visit(config.pagePath).waitForRouteChange()
47+
48+
// trigger intersection observer
49+
cy.scrollTo("top")
50+
cy.wait(200)
51+
cy.scrollTo("bottom", {
52+
duration: 600,
53+
})
54+
cy.wait(600)
55+
})
56+
57+
describe(`Image CDN`, () => {
58+
async function testImages(images, expectations) {
59+
for (let i = 0; i < images.length; i++) {
60+
const expectation = expectations[i]
61+
62+
const url = images[i].currentSrc
63+
64+
const { href, origin } = new URL(url)
65+
const urlWithoutOrigin = href.replace(origin, ``)
66+
67+
// using Netlify Image CDN
68+
expect(urlWithoutOrigin).to.match(/^\/.netlify\/images/)
69+
70+
const res = await fetch(url, {
71+
method: "HEAD",
72+
})
73+
expect(res.ok).to.be.true
74+
75+
const expectedNaturalWidth =
76+
expectation.naturalWidth ?? expectation.width
77+
const expectedNaturalHeight =
78+
expectation.naturalHeight ?? expectation.height
79+
80+
if (expectation.width) {
81+
expect(
82+
Math.ceil(images[i].getBoundingClientRect().width)
83+
).to.be.equal(expectation.width)
84+
}
85+
if (expectation.height) {
86+
expect(
87+
Math.ceil(images[i].getBoundingClientRect().height)
88+
).to.be.equal(expectation.height)
89+
}
90+
if (expectedNaturalWidth) {
91+
expect(Math.ceil(images[i].naturalWidth)).to.be.equal(
92+
expectedNaturalWidth
93+
)
94+
}
95+
if (expectedNaturalHeight) {
96+
expect(Math.ceil(images[i].naturalHeight)).to.be.equal(
97+
expectedNaturalHeight
98+
)
99+
}
100+
}
101+
}
102+
103+
it(`should render correct dimensions`, () => {
104+
cy.get('[data-testid="image-public"]').then(async $urls => {
105+
const urls = Array.from(
106+
$urls.map((_, $url) => $url.getAttribute("href"))
107+
)
108+
109+
// urls is array of href attribute, not absolute urls, so it already is stripped of origin
110+
for (const urlWithoutOrigin of urls) {
111+
// using Netlify Image CDN
112+
expect(urlWithoutOrigin).to.match(/^\/.netlify\/images/)
113+
const res = await fetch(urlWithoutOrigin, {
114+
method: "HEAD",
115+
})
116+
expect(res.ok).to.be.true
117+
}
118+
})
119+
120+
cy.get(".resize").then({ timeout: 60000 }, async $imgs => {
121+
await testImages(Array.from($imgs), [
122+
{
123+
width: 100,
124+
height: 133,
125+
},
126+
{
127+
width: 100,
128+
height: 160,
129+
},
130+
{
131+
width: 100,
132+
height: 67,
133+
},
134+
])
135+
})
136+
137+
cy.get(".fixed img:not([aria-hidden=true])").then(
138+
{ timeout: 60000 },
139+
async $imgs => {
140+
await testImages(Array.from($imgs), [
141+
{
142+
width: 100,
143+
height: 133,
144+
},
145+
{
146+
width: 100,
147+
height: 160,
148+
},
149+
{
150+
width: 100,
151+
height: 67,
152+
},
153+
])
154+
}
155+
)
156+
157+
cy.get(".constrained img:not([aria-hidden=true])").then(
158+
{ timeout: 60000 },
159+
async $imgs => {
160+
await testImages(Array.from($imgs), [
161+
{
162+
width: 300,
163+
height: 400,
164+
},
165+
{
166+
width: 300,
167+
height: 481,
168+
},
169+
{
170+
width: 300,
171+
height: 200,
172+
},
173+
])
174+
}
175+
)
176+
177+
cy.get(".full img:not([aria-hidden=true])").then(
178+
{ timeout: 60000 },
179+
async $imgs => {
180+
await testImages(Array.from($imgs), [
181+
{
182+
naturalHeight: 1333,
183+
},
184+
{
185+
naturalHeight: 1603,
186+
},
187+
{
188+
naturalHeight: 666,
189+
},
190+
])
191+
}
192+
)
193+
})
194+
195+
it(`should render a placeholder`, () => {
196+
if (config.placeholders) {
197+
cy.get(".fixed [data-placeholder-image]")
198+
.first()
199+
.should("have.css", "background-color", "rgb(232, 184, 8)")
200+
cy.get(".constrained [data-placeholder-image]")
201+
.first()
202+
.should($el => {
203+
expect($el.prop("tagName")).to.be.equal("IMG")
204+
expect($el.prop("src")).to.contain("data:image/jpg;base64")
205+
})
206+
cy.get(".constrained_traced [data-placeholder-image]")
207+
.first()
208+
.should($el => {
209+
// traced falls back to DOMINANT_COLOR
210+
expect($el.prop("tagName")).to.be.equal("DIV")
211+
expect($el).to.be.empty
212+
})
213+
}
214+
cy.get(".full [data-placeholder-image]")
215+
.first()
216+
.should($el => {
217+
expect($el.prop("tagName")).to.be.equal("DIV")
218+
expect($el).to.be.empty
219+
})
220+
})
221+
})
222+
223+
it(`File CDN`, () => {
224+
cy.get('[data-testid="file-public"]').then(async $urls => {
225+
const fileCdnFixtures = Array.from(
226+
$urls.map((_, $url) => {
227+
return {
228+
urlWithoutOrigin: $url.getAttribute("href"),
229+
allowed: $url.getAttribute("data-allowed") === "true",
230+
}
231+
})
232+
)
233+
234+
// urls is array of href attribute, not absolute urls, so it already is stripped of origin
235+
for (const { urlWithoutOrigin, allowed } of fileCdnFixtures) {
236+
// using Netlify Image CDN
237+
expect(urlWithoutOrigin).to.match(
238+
new RegExp(`^${PATH_PREFIX}/_gatsby/file`)
239+
)
240+
const res = await fetch(urlWithoutOrigin, {
241+
method: "HEAD",
242+
})
243+
if (allowed) {
244+
expect(res.ok).to.be.true
245+
} else {
246+
expect(res.ok).to.be.false
247+
expect(res.status).to.be.equal(500)
248+
}
249+
}
250+
})
251+
})
252+
}
253+
)
254+
}

e2e-tests/adapters/gatsby-config.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ const config: GatsbyConfig = {
2424
},
2525
trailingSlash,
2626
pathPrefix,
27-
plugins: [],
27+
plugins: [
28+
`gatsby-plugin-image`,
29+
`gatsby-plugin-sharp`,
30+
`gatsby-transformer-sharp`,
31+
],
2832
headers: [
2933
{
3034
source: `/*`,

0 commit comments

Comments
 (0)