Skip to content

Commit

Permalink
feat: image and file cdn url generator adapter implementation (#38685)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
kathmbeck and pieh authored Dec 15, 2023
1 parent f8c207b commit 4a780fb
Show file tree
Hide file tree
Showing 49 changed files with 2,177 additions and 80 deletions.
2 changes: 2 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,8 @@ jobs:

e2e_tests_adapters:
<<: *e2e-executor
docker:
- image: cypress/browsers:node-18.16.1-chrome-114.0.5735.133-1-ff-114.0.2-edge-114.0.1823.51-1
steps:
- run: echo 'export CYPRESS_RECORD_KEY="${CY_CLOUD_ADAPTERS}"' >> "$BASH_ENV"
- e2e-test:
Expand Down
254 changes: 254 additions & 0 deletions e2e-tests/adapters/cypress/e2e/remote-file.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
Cypress.on("uncaught:exception", err => {
if (
(err.message.includes("Minified React error #418") ||
err.message.includes("Minified React error #423") ||
err.message.includes("Minified React error #425")) &&
Cypress.env(`TEST_PLUGIN_OFFLINE`)
) {
return false
}
})

const PATH_PREFIX = Cypress.env(`PATH_PREFIX`) || ``

// there are multiple scenarios we want to test and ensure that custom image cdn url is used:
// - child build process (SSG, Page Query)
// - main build process (SSG, Page Context)
// - query engine (SSR, Page Query)
const configs = [
{
title: `remote-file (SSG, Page Query)`,
pagePath: `/routes/remote-file/`,
placeholders: true,
},
{
title: `remote-file (SSG, Page Context)`,
pagePath: `/routes/remote-file-data-from-context/`,
placeholders: true,
},
{
title: `remote-file (SSR, Page Query)`,
pagePath: `/routes/ssr/remote-file/`,
placeholders: false,
},
]

for (const config of configs) {
describe(
config.title,
{
retries: {
runMode: 4,
},
},
() => {
beforeEach(() => {
cy.visit(config.pagePath).waitForRouteChange()

// trigger intersection observer
cy.scrollTo("top")
cy.wait(200)
cy.scrollTo("bottom", {
duration: 600,
})
cy.wait(600)
})

describe(`Image CDN`, () => {
async function testImages(images, expectations) {
for (let i = 0; i < images.length; i++) {
const expectation = expectations[i]

const url = images[i].currentSrc

const { href, origin } = new URL(url)
const urlWithoutOrigin = href.replace(origin, ``)

// using Netlify Image CDN
expect(urlWithoutOrigin).to.match(/^\/.netlify\/images/)

const res = await fetch(url, {
method: "HEAD",
})
expect(res.ok).to.be.true

const expectedNaturalWidth =
expectation.naturalWidth ?? expectation.width
const expectedNaturalHeight =
expectation.naturalHeight ?? expectation.height

if (expectation.width) {
expect(
Math.ceil(images[i].getBoundingClientRect().width)
).to.be.equal(expectation.width)
}
if (expectation.height) {
expect(
Math.ceil(images[i].getBoundingClientRect().height)
).to.be.equal(expectation.height)
}
if (expectedNaturalWidth) {
expect(Math.ceil(images[i].naturalWidth)).to.be.equal(
expectedNaturalWidth
)
}
if (expectedNaturalHeight) {
expect(Math.ceil(images[i].naturalHeight)).to.be.equal(
expectedNaturalHeight
)
}
}
}

it(`should render correct dimensions`, () => {
cy.get('[data-testid="image-public"]').then(async $urls => {
const urls = Array.from(
$urls.map((_, $url) => $url.getAttribute("href"))
)

// urls is array of href attribute, not absolute urls, so it already is stripped of origin
for (const urlWithoutOrigin of urls) {
// using Netlify Image CDN
expect(urlWithoutOrigin).to.match(/^\/.netlify\/images/)
const res = await fetch(urlWithoutOrigin, {
method: "HEAD",
})
expect(res.ok).to.be.true
}
})

cy.get(".resize").then({ timeout: 60000 }, async $imgs => {
await testImages(Array.from($imgs), [
{
width: 100,
height: 133,
},
{
width: 100,
height: 160,
},
{
width: 100,
height: 67,
},
])
})

cy.get(".fixed img:not([aria-hidden=true])").then(
{ timeout: 60000 },
async $imgs => {
await testImages(Array.from($imgs), [
{
width: 100,
height: 133,
},
{
width: 100,
height: 160,
},
{
width: 100,
height: 67,
},
])
}
)

cy.get(".constrained img:not([aria-hidden=true])").then(
{ timeout: 60000 },
async $imgs => {
await testImages(Array.from($imgs), [
{
width: 300,
height: 400,
},
{
width: 300,
height: 481,
},
{
width: 300,
height: 200,
},
])
}
)

cy.get(".full img:not([aria-hidden=true])").then(
{ timeout: 60000 },
async $imgs => {
await testImages(Array.from($imgs), [
{
naturalHeight: 1333,
},
{
naturalHeight: 1603,
},
{
naturalHeight: 666,
},
])
}
)
})

it(`should render a placeholder`, () => {
if (config.placeholders) {
cy.get(".fixed [data-placeholder-image]")
.first()
.should("have.css", "background-color", "rgb(232, 184, 8)")
cy.get(".constrained [data-placeholder-image]")
.first()
.should($el => {
expect($el.prop("tagName")).to.be.equal("IMG")
expect($el.prop("src")).to.contain("data:image/jpg;base64")
})
cy.get(".constrained_traced [data-placeholder-image]")
.first()
.should($el => {
// traced falls back to DOMINANT_COLOR
expect($el.prop("tagName")).to.be.equal("DIV")
expect($el).to.be.empty
})
}
cy.get(".full [data-placeholder-image]")
.first()
.should($el => {
expect($el.prop("tagName")).to.be.equal("DIV")
expect($el).to.be.empty
})
})
})

it(`File CDN`, () => {
cy.get('[data-testid="file-public"]').then(async $urls => {
const fileCdnFixtures = Array.from(
$urls.map((_, $url) => {
return {
urlWithoutOrigin: $url.getAttribute("href"),
allowed: $url.getAttribute("data-allowed") === "true",
}
})
)

// urls is array of href attribute, not absolute urls, so it already is stripped of origin
for (const { urlWithoutOrigin, allowed } of fileCdnFixtures) {
// using Netlify Image CDN
expect(urlWithoutOrigin).to.match(
new RegExp(`^${PATH_PREFIX}/_gatsby/file`)
)
const res = await fetch(urlWithoutOrigin, {
method: "HEAD",
})
if (allowed) {
expect(res.ok).to.be.true
} else {
expect(res.ok).to.be.false
expect(res.status).to.be.equal(500)
}
}
})
})
}
)
}
6 changes: 5 additions & 1 deletion e2e-tests/adapters/gatsby-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ const config: GatsbyConfig = {
},
trailingSlash,
pathPrefix,
plugins: [],
plugins: [
`gatsby-plugin-image`,
`gatsby-plugin-sharp`,
`gatsby-transformer-sharp`,
],
headers: [
{
source: `/*`,
Expand Down
Loading

0 comments on commit 4a780fb

Please sign in to comment.