From a9fceea9408a08d8a8a1bd7b6f3c8730c8fcc75a Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Tue, 28 Nov 2023 11:14:15 +0100 Subject: [PATCH 01/12] feat: move custom image cdn url generator implementation to adapter --- .../adapters/cypress/e2e/remote-file.cy.ts | 331 ++++++++++-------- e2e-tests/adapters/gatsby-node.ts | 48 ++- e2e-tests/adapters/src/pages/index.jsx | 12 + .../adapters/src/pages/routes/remote-file.jsx | 2 +- .../src/pages/routes/ssr/remote-file.jsx | 87 +++++ .../templates/remote-file-from-context.jsx | 45 +++ .../src/__tests__/image-cdn-url-generator.ts | 116 ++++++ .../src/image-cdn-url-generator.ts | 60 ++++ packages/gatsby-adapter-netlify/src/index.ts | 14 + .../polyfill-remote-file/jobs/dispatchers.ts | 4 +- .../src/polyfill-remote-file/types.ts | 6 + .../utils/__tests__/url-generator.ts | 123 ------- .../utils/url-generator.ts | 96 ++--- packages/gatsby/index.d.ts | 1 + packages/gatsby/src/utils/adapter/manager.ts | 5 + packages/gatsby/src/utils/adapter/types.ts | 4 + .../utils/page-ssr-module/bundle-webpack.ts | 17 +- .../src/utils/page-ssr-module/lambda.ts | 17 +- types/gatsby-monorepo/global.d.ts | 1 + 19 files changed, 635 insertions(+), 354 deletions(-) create mode 100644 e2e-tests/adapters/src/pages/routes/ssr/remote-file.jsx create mode 100644 e2e-tests/adapters/src/templates/remote-file-from-context.jsx create mode 100644 packages/gatsby-adapter-netlify/src/__tests__/image-cdn-url-generator.ts create mode 100644 packages/gatsby-adapter-netlify/src/image-cdn-url-generator.ts diff --git a/e2e-tests/adapters/cypress/e2e/remote-file.cy.ts b/e2e-tests/adapters/cypress/e2e/remote-file.cy.ts index 1f814544b02a2..2a2f347365e50 100644 --- a/e2e-tests/adapters/cypress/e2e/remote-file.cy.ts +++ b/e2e-tests/adapters/cypress/e2e/remote-file.cy.ts @@ -11,107 +11,116 @@ Cypress.on("uncaught:exception", err => { const PATH_PREFIX = Cypress.env(`PATH_PREFIX`) || `` -describe( - `remote-file`, +// 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 = [ { - retries: { - runMode: 4, - }, + title: `remote-file (SSG, Page Query)`, + pagePath: `/routes/remote-file/`, + fileCDN: true, + placeholders: true, + }, + { + title: `remote-file (SSG, Page Context)`, + pagePath: `/routes/remote-file-data-from-context/`, + fileCDN: true, + placeholders: true, + }, + { + title: `remote-file (SSR, Page Query)`, + pagePath: `/routes/ssr/remote-file/`, + fileCDN: false, + placeholders: false, }, - () => { - beforeEach(() => { - cy.visit(`/routes/remote-file/`).waitForRouteChange() - - // trigger intersection observer - cy.scrollTo("top") - cy.wait(200) - cy.scrollTo("bottom", { - duration: 600, +] + +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) }) - cy.wait(600) - }) - async function testImages(images, expectations) { - for (let i = 0; i < images.length; i++) { - const expectation = expectations[i] + async function testImages(images, expectations) { + for (let i = 0; i < images.length; i++) { + const expectation = expectations[i] - const url = images[i].currentSrc + const url = images[i].currentSrc - const { href, origin } = new URL(url) - const urlWithoutOrigin = href.replace(origin, ``) + const { href, origin } = new URL(url) + const urlWithoutOrigin = href.replace(origin, ``) - // using Netlify Image CDN - expect(urlWithoutOrigin).to.match(/^\/.netlify\/images/) + // 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="public"]').then(async $urls => { - const urls = Array.from( - $urls.map((_, $url) => $url.getAttribute("href")) - ) - - for (const url of urls) { - // using OSS implementation for publicURL for now - expect(url).to.match(new RegExp(`^${PATH_PREFIX}/_gatsby/file`)) 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 + ) + } } - }) + } - 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, - }, - ]) - }) + it(`should render correct dimensions`, () => { + if (config.fileCDN) { + cy.get('[data-testid="public"]').then(async $urls => { + const urls = Array.from( + $urls.map((_, $url) => $url.getAttribute("href")) + ) + + for (const url of urls) { + // using OSS implementation for publicURL for now + expect(url).to.match(new RegExp(`^${PATH_PREFIX}/_gatsby/file`)) + const res = await fetch(url, { + method: "HEAD", + }) + expect(res.ok).to.be.true + } + }) + } - cy.get(".fixed img:not([aria-hidden=true])").then( - { timeout: 60000 }, - async $imgs => { + cy.get(".resize").then({ timeout: 60000 }, async $imgs => { await testImages(Array.from($imgs), [ { width: 100, @@ -126,70 +135,92 @@ describe( 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(".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(".full img:not([aria-hidden=true])").then( - { timeout: 60000 }, - async $imgs => { - await testImages(Array.from($imgs), [ - { - naturalHeight: 1333, - }, - { - naturalHeight: 1603, - }, - { - naturalHeight: 666, - }, - ]) + 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 + }) } - ) - }) - - it(`should render a placeholder`, () => { - 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 - }) - }) - } -) + cy.get(".full [data-placeholder-image]") + .first() + .should($el => { + expect($el.prop("tagName")).to.be.equal("DIV") + expect($el).to.be.empty + }) + }) + } + ) +} diff --git a/e2e-tests/adapters/gatsby-node.ts b/e2e-tests/adapters/gatsby-node.ts index 656dfac22953f..3c876c4f8efe7 100644 --- a/e2e-tests/adapters/gatsby-node.ts +++ b/e2e-tests/adapters/gatsby-node.ts @@ -6,9 +6,53 @@ import { applyTrailingSlashOption } from "./utils" const TRAILING_SLASH = (process.env.TRAILING_SLASH || `never`) as GatsbyConfig["trailingSlash"] -export const createPages: GatsbyNode["createPages"] = ({ - actions: { createRedirect, createSlice }, +export const createPages: GatsbyNode["createPages"] = async ({ + actions: { createPage, createRedirect, createSlice }, + graphql, }) => { + const { data: ImageCDNRemoteFileFromPageContextData } = await graphql(` + query ImageCDNGatsbyNode { + allMyRemoteFile { + nodes { + id + url + filename + publicUrl + resize(width: 100) { + height + width + src + } + fixed: gatsbyImage( + layout: FIXED + width: 100 + placeholder: DOMINANT_COLOR + ) + constrained: gatsbyImage( + layout: CONSTRAINED + width: 300 + placeholder: BLURRED + ) + constrained_traced: gatsbyImage( + layout: CONSTRAINED + width: 300 + placeholder: TRACED_SVG + ) + full: gatsbyImage(layout: FULL_WIDTH, width: 500, placeholder: NONE) + } + } + } + `) + + createPage({ + path: applyTrailingSlashOption( + `/routes/remote-file-data-from-context/`, + TRAILING_SLASH + ), + component: path.resolve(`./src/templates/remote-file-from-context.jsx`), + context: ImageCDNRemoteFileFromPageContextData, + }) + createRedirect({ fromPath: applyTrailingSlashOption("/redirect", TRAILING_SLASH), toPath: applyTrailingSlashOption("/routes/redirect/hit", TRAILING_SLASH), diff --git a/e2e-tests/adapters/src/pages/index.jsx b/e2e-tests/adapters/src/pages/index.jsx index 9cbcccbe6ac45..d0a2cad54df27 100644 --- a/e2e-tests/adapters/src/pages/index.jsx +++ b/e2e-tests/adapters/src/pages/index.jsx @@ -39,6 +39,18 @@ const routes = [ text: "Client-Only Named Wildcard", url: "/routes/client-only/named-wildcard/corinno/fenring", }, + { + text: "RemoteFile (ImageCDN) (SSG, Page Query)", + url: "/routes/remote-file", + }, + { + text: "RemoteFile (ImageCDN) (SSG, Page Context)", + url: "/routes/remote-file-data-from-context", + }, + { + text: "RemoteFile (ImageCDN) (SSR, Page Query)", + url: "/routes/ssr/remote-file", + }, ] const functions = [ diff --git a/e2e-tests/adapters/src/pages/routes/remote-file.jsx b/e2e-tests/adapters/src/pages/routes/remote-file.jsx index f9f35966e4ddf..d82c8c5030651 100644 --- a/e2e-tests/adapters/src/pages/routes/remote-file.jsx +++ b/e2e-tests/adapters/src/pages/routes/remote-file.jsx @@ -44,7 +44,7 @@ const RemoteFile = ({ data }) => { } export const pageQuery = graphql` - { + query SSGImageCDNPageQuery { allMyRemoteFile { nodes { id diff --git a/e2e-tests/adapters/src/pages/routes/ssr/remote-file.jsx b/e2e-tests/adapters/src/pages/routes/ssr/remote-file.jsx new file mode 100644 index 0000000000000..a838f7948b5ee --- /dev/null +++ b/e2e-tests/adapters/src/pages/routes/ssr/remote-file.jsx @@ -0,0 +1,87 @@ +import { graphql } from "gatsby" +import React from "react" + +import { GatsbyImage } from "gatsby-plugin-image" +import Layout from "../../../components/layout" + +const RemoteFile = ({ data }) => { + return ( + + {data.allMyRemoteFile.nodes.map(node => { + return ( +
+

+ + {node.filename} + +

+ +
+ + + + +
+
+ ) + })} +
+ ) +} + +export const pageQuery = graphql` + query SSRImageCDNPageQuery { + allMyRemoteFile { + nodes { + id + url + filename + # FILE_CDN is not supported in SSR/DSG yet + # publicUrl + resize(width: 100) { + height + width + src + } + fixed: gatsbyImage( + layout: FIXED + width: 100 + # only NONE placeholder is supported in SSR/DSG + # placeholder: DOMINANT_COLOR + placeholder: NONE + ) + constrained: gatsbyImage( + layout: CONSTRAINED + width: 300 + # only NONE placeholder is supported in SSR/DSG + # placeholder: DOMINANT_COLOR + placeholder: NONE + ) + constrained_traced: gatsbyImage( + layout: CONSTRAINED + width: 300 + # only NONE placeholder is supported in SSR/DSG + # placeholder: DOMINANT_COLOR + placeholder: NONE + ) + full: gatsbyImage(layout: FULL_WIDTH, width: 500, placeholder: NONE) + } + } + } +` + +export default RemoteFile diff --git a/e2e-tests/adapters/src/templates/remote-file-from-context.jsx b/e2e-tests/adapters/src/templates/remote-file-from-context.jsx new file mode 100644 index 0000000000000..2e2d8af24496f --- /dev/null +++ b/e2e-tests/adapters/src/templates/remote-file-from-context.jsx @@ -0,0 +1,45 @@ +import React from "react" + +import { GatsbyImage } from "gatsby-plugin-image" +import Layout from "../components/layout" + +const RemoteFile = ({ pageContext: data }) => { + return ( + + {data.allMyRemoteFile.nodes.map(node => { + return ( +
+

+ + {node.filename} + +

+ +
+ + + + +
+
+ ) + })} +
+ ) +} + +export default RemoteFile diff --git a/packages/gatsby-adapter-netlify/src/__tests__/image-cdn-url-generator.ts b/packages/gatsby-adapter-netlify/src/__tests__/image-cdn-url-generator.ts new file mode 100644 index 0000000000000..957b8cc86c718 --- /dev/null +++ b/packages/gatsby-adapter-netlify/src/__tests__/image-cdn-url-generator.ts @@ -0,0 +1,116 @@ +import { generateImageUrl, generateImageArgs } from "../image-cdn-url-generator" + +describe(`generateImageUrl`, () => { + const source = { + url: `https://example.com/image.jpg`, + filename: `image.jpg`, + mimeType: `image/jpeg`, + internal: { + contentDigest: `1234`, + }, + } + + it(`should return an image based url`, () => { + expect( + generateImageUrl(source, { + width: 100, + height: 100, + cropFocus: `top`, + format: `webp`, + quality: 80, + }) + ).toMatchInlineSnapshot( + `"/.netlify/images?w=100&h=100&fit=crop&crop=top&fm=webp&q=80&url=https%3A%2F%2Fexample.com%2Fimage.jpg&cd=1234"` + ) + }) + + it(`should handle special characters`, () => { + const source = { + url: `https://example.com/image-éà.jpg`, + filename: `image-éà.jpg`, + mimeType: `image/jpeg`, + internal: { + contentDigest: `1234`, + }, + } + + expect( + generateImageUrl(source, { + width: 100, + height: 100, + cropFocus: `top`, + format: `webp`, + quality: 80, + }) + ).toMatchInlineSnapshot( + `"/.netlify/images?w=100&h=100&fit=crop&crop=top&fm=webp&q=80&url=https%3A%2F%2Fexample.com%2Fimage-%C3%A9%C3%A0.jpg&cd=1234"` + ) + }) + + it(`should handle spaces`, () => { + const source = { + url: `https://example.com/image test.jpg`, + filename: `image test.jpg`, + mimeType: `image/jpeg`, + internal: { + contentDigest: `1234`, + }, + } + + expect( + generateImageUrl(source, { + width: 100, + height: 100, + cropFocus: `top`, + format: `webp`, + quality: 80, + }) + ).toMatchInlineSnapshot( + `"/.netlify/images?w=100&h=100&fit=crop&crop=top&fm=webp&q=80&url=https%3A%2F%2Fexample.com%2Fimage+test.jpg&cd=1234"` + ) + }) + + it(`should handle encoded urls`, () => { + const source = { + url: `https://example.com/image%20test.jpg`, + filename: `image test.jpg`, + mimeType: `image/jpeg`, + internal: { + contentDigest: `1234`, + }, + } + + expect( + generateImageUrl(source, { + width: 100, + height: 100, + cropFocus: `top`, + format: `webp`, + quality: 80, + }) + ).toMatchInlineSnapshot( + `"/.netlify/images?w=100&h=100&fit=crop&crop=top&fm=webp&q=80&url=https%3A%2F%2Fexample.com%2Fimage%2520test.jpg&cd=1234"` + ) + }) + + it.each([ + [`width`, `w`, 100], + [`height`, `h`, 50], + [`cropFocus`, `crop`, `center,right`], + [`format`, `fm`, `webp`], + [`quality`, `q`, 60], + ] as Array<[keyof ImageArgs, string, ImageArgs[keyof ImageArgs]]>)( + `should set %s in image args`, + (key, queryKey, value) => { + const url = new URL( + // @ts-ignore remove typings + `https://netlify.com${generateImageUrl(source, { + format: `webp`, + [key]: value, + })}` + ) + + expect(url.searchParams.get(queryKey)).toEqual(value.toString()) + } + ) +}) diff --git a/packages/gatsby-adapter-netlify/src/image-cdn-url-generator.ts b/packages/gatsby-adapter-netlify/src/image-cdn-url-generator.ts new file mode 100644 index 0000000000000..cb5b9d5e38ed2 --- /dev/null +++ b/packages/gatsby-adapter-netlify/src/image-cdn-url-generator.ts @@ -0,0 +1,60 @@ +import type { ImageCdnTransformArgs } from "gatsby" + +export function generateImageUrl( + source: { + url: string + filename: string + mimeType: string + internal: { contentDigest: string } + }, + imageArgs: ImageCdnTransformArgs +): string { + const placeholderOrigin = `http://netlify.com` + const imageParams = generateImageArgs(imageArgs) + + const baseURL = new URL(`${placeholderOrigin}/.netlify/images`) + + baseURL.search = imageParams.toString() + baseURL.searchParams.append(`url`, source.url) + baseURL.searchParams.append(`cd`, source.internal.contentDigest) + + return `${baseURL.pathname}${baseURL.search}` +} + +export function generateImageArgs({ + width, + height, + format, + cropFocus, + quality, +}: ImageCdnTransformArgs): URLSearchParams { + const params = new URLSearchParams() + + if (width) { + params.append(`w`, width.toString()) + } + if (height) { + params.append(`h`, height.toString()) + } + if (cropFocus) { + params.append(`fit`, `crop`) + if (Array.isArray(cropFocus)) { + // For array of cropFocus values, append them as comma-separated string + params.append(`crop`, cropFocus.join(`,`)) + } else { + params.append(`crop`, cropFocus) + } + } + + if (format) { + params.append(`fm`, format) + } + + if (quality) { + params.append(`q`, quality.toString()) + } + + return params +} + +export default generateImageUrl diff --git a/packages/gatsby-adapter-netlify/src/index.ts b/packages/gatsby-adapter-netlify/src/index.ts index 2e20a63a35508..e741758167612 100644 --- a/packages/gatsby-adapter-netlify/src/index.ts +++ b/packages/gatsby-adapter-netlify/src/index.ts @@ -11,6 +11,7 @@ interface INetlifyCacheUtils { interface INetlifyAdapterOptions { excludeDatastoreFromEngineFunction?: boolean + imageCDN?: boolean } let _cacheUtils: INetlifyCacheUtils | undefined @@ -117,6 +118,16 @@ const createNetlifyAdapter: AdapterInit = options => { excludeDatastoreFromEngineFunction = false } + let useNetlifyImageCDN = options?.imageCDN + if ( + typeof useNetlifyImageCDN === `undefined` && + typeof process.env.NETLIFY_IMAGE_CDN !== `undefined` + ) { + useNetlifyImageCDN = + process.env.NETLIFY_IMAGE_CDN === `true` || + process.env.NETLIFY_IMAGE_CDN === `1` + } + return { excludeDatastoreFromEngineFunction, deployURL, @@ -128,6 +139,9 @@ const createNetlifyAdapter: AdapterInit = options => { `gatsby-plugin-netlify-cache`, `gatsby-plugin-netlify`, ], + imageCDNUrlGeneratorModulePath: useNetlifyImageCDN + ? require.resolve(`./image-cdn-url-generator`) + : undefined, } }, } diff --git a/packages/gatsby-plugin-utils/src/polyfill-remote-file/jobs/dispatchers.ts b/packages/gatsby-plugin-utils/src/polyfill-remote-file/jobs/dispatchers.ts index d6afdb8ebc078..858ea8f9bcbdd 100644 --- a/packages/gatsby-plugin-utils/src/polyfill-remote-file/jobs/dispatchers.ts +++ b/packages/gatsby-plugin-utils/src/polyfill-remote-file/jobs/dispatchers.ts @@ -16,9 +16,9 @@ export function shouldDispatchLocalFileServiceJob(): boolean { export function shouldDispatchLocalImageServiceJob(): boolean { return ( !( + global.__GATSBY?.imageCDNUrlGeneratorModulePath || process.env.GATSBY_CLOUD_IMAGE_CDN === `1` || - process.env.GATSBY_CLOUD_IMAGE_CDN === `true` || - process.env.NETLIFY_IMAGE_CDN === `true` + process.env.GATSBY_CLOUD_IMAGE_CDN === `true` ) && process.env.NODE_ENV === `production` ) } diff --git a/packages/gatsby-plugin-utils/src/polyfill-remote-file/types.ts b/packages/gatsby-plugin-utils/src/polyfill-remote-file/types.ts index 99cc86e69f7aa..8f064d33e9d5a 100644 --- a/packages/gatsby-plugin-utils/src/polyfill-remote-file/types.ts +++ b/packages/gatsby-plugin-utils/src/polyfill-remote-file/types.ts @@ -93,3 +93,9 @@ export function isImage(node: { return node.mimeType.startsWith(`image/`) && node.mimeType !== `image/svg+xml` } + +export type ImageCdnTransformArgs = WidthOrHeight & { + format: string + cropFocus?: ImageCropFocus | Array + quality: number +} diff --git a/packages/gatsby-plugin-utils/src/polyfill-remote-file/utils/__tests__/url-generator.ts b/packages/gatsby-plugin-utils/src/polyfill-remote-file/utils/__tests__/url-generator.ts index 34448ddd3c24e..c74e778214986 100644 --- a/packages/gatsby-plugin-utils/src/polyfill-remote-file/utils/__tests__/url-generator.ts +++ b/packages/gatsby-plugin-utils/src/polyfill-remote-file/utils/__tests__/url-generator.ts @@ -344,126 +344,3 @@ describe(`url-generator`, () => { ) }) }) - -describe(`generateImageUrlAlt`, () => { - beforeEach(() => { - process.env.NETLIFY_IMAGE_CDN = `true` - }) - - afterEach(() => { - delete process.env.NETLIFY_IMAGE_CDN - }) - - const source = { - url: `https://example.com/image.jpg`, - filename: `image.jpg`, - mimeType: `image/jpeg`, - internal: { - contentDigest: `1234`, - }, - } - - it(`should return an image based url`, () => { - expect( - generateImageUrlAlt(source, { - width: 100, - height: 100, - cropFocus: `top`, - format: `webp`, - quality: 80, - }) - ).toMatchInlineSnapshot( - `"/.netlify/images?w=100&h=100&fit=crop&crop=top&fm=webp&q=80&url=https%3A%2F%2Fexample.com%2Fimage.jpg&cd=1234"` - ) - }) - - it(`should handle special characters`, () => { - const source = { - url: `https://example.com/image-éà.jpg`, - filename: `image-éà.jpg`, - mimeType: `image/jpeg`, - internal: { - contentDigest: `1234`, - }, - } - - expect( - generateImageUrlAlt(source, { - width: 100, - height: 100, - cropFocus: `top`, - format: `webp`, - quality: 80, - }) - ).toMatchInlineSnapshot( - `"/.netlify/images?w=100&h=100&fit=crop&crop=top&fm=webp&q=80&url=https%3A%2F%2Fexample.com%2Fimage-%C3%A9%C3%A0.jpg&cd=1234"` - ) - }) - - it(`should handle spaces`, () => { - const source = { - url: `https://example.com/image test.jpg`, - filename: `image test.jpg`, - mimeType: `image/jpeg`, - internal: { - contentDigest: `1234`, - }, - } - - expect( - generateImageUrlAlt(source, { - width: 100, - height: 100, - cropFocus: `top`, - format: `webp`, - quality: 80, - }) - ).toMatchInlineSnapshot( - `"/.netlify/images?w=100&h=100&fit=crop&crop=top&fm=webp&q=80&url=https%3A%2F%2Fexample.com%2Fimage+test.jpg&cd=1234"` - ) - }) - - it(`should handle encoded urls`, () => { - const source = { - url: `https://example.com/image%20test.jpg`, - filename: `image test.jpg`, - mimeType: `image/jpeg`, - internal: { - contentDigest: `1234`, - }, - } - - expect( - generateImageUrlAlt(source, { - width: 100, - height: 100, - cropFocus: `top`, - format: `webp`, - quality: 80, - }) - ).toMatchInlineSnapshot( - `"/.netlify/images?w=100&h=100&fit=crop&crop=top&fm=webp&q=80&url=https%3A%2F%2Fexample.com%2Fimage%2520test.jpg&cd=1234"` - ) - }) - - it.each([ - [`width`, `w`, 100], - [`height`, `h`, 50], - [`cropFocus`, `crop`, `center,right`], - [`format`, `fm`, `webp`], - [`quality`, `q`, 60], - ] as Array<[keyof ImageArgs, string, ImageArgs[keyof ImageArgs]]>)( - `should set %s in image args`, - (key, queryKey, value) => { - const url = new URL( - // @ts-ignore remove typings - `https://netlify.com${generateImageUrlAlt(source, { - format: `webp`, - [key]: value, - })}` - ) - - expect(url.searchParams.get(queryKey)).toEqual(value.toString()) - } - ) -}) diff --git a/packages/gatsby-plugin-utils/src/polyfill-remote-file/utils/url-generator.ts b/packages/gatsby-plugin-utils/src/polyfill-remote-file/utils/url-generator.ts index 79e3ec108db00..eb5d98c626de4 100644 --- a/packages/gatsby-plugin-utils/src/polyfill-remote-file/utils/url-generator.ts +++ b/packages/gatsby-plugin-utils/src/polyfill-remote-file/utils/url-generator.ts @@ -3,7 +3,7 @@ import { basename, extname } from "path" import { URL } from "url" import { createContentDigest } from "gatsby-core-utils/create-content-digest" import { isImage } from "../types" -import type { ImageCropFocus, WidthOrHeight } from "../types" +import type { ImageCdnTransformArgs } from "../types" import type { Store } from "gatsby" // this is an arbitrary origin that we use #branding so we can construct a full url for the URL constructor @@ -83,6 +83,21 @@ export function generateFileUrl( return `${frontendHostName}${parsedURL.pathname}${parsedURL.search}` } +type CustomImageCDNUrlGeneratorFn = ( + source: { + url: string + mimeType: string + filename: string + internal: { contentDigest: string } + }, + imageArgs: ImageCdnTransformArgs +) => string + +let customImageCDNUrlGenerator: CustomImageCDNUrlGeneratorFn | undefined = + undefined + +const preferDefault = (m: any): any => (m && m.default) || m + export function generateImageUrl( source: { url: string @@ -90,12 +105,18 @@ export function generateImageUrl( filename: string internal: { contentDigest: string } }, - imageArgs: Parameters[0], + imageArgs: ImageCdnTransformArgs, store?: Store ): string { - if (process.env.NETLIFY_IMAGE_CDN) { - return generateImageUrlAlt(source, imageArgs) + if (global.__GATSBY?.imageCDNUrlGeneratorModulePath) { + if (!customImageCDNUrlGenerator) { + customImageCDNUrlGenerator = preferDefault( + require(global.__GATSBY.imageCDNUrlGeneratorModulePath) + ) as CustomImageCDNUrlGeneratorFn + } + return customImageCDNUrlGenerator(source, imageArgs) } + const filenameWithoutExt = basename(source.filename, extname(source.filename)) const queryStr = generateImageArgs(imageArgs) @@ -152,11 +173,7 @@ function generateImageArgs({ format, cropFocus, quality, -}: WidthOrHeight & { - format: string - cropFocus?: ImageCropFocus | Array - quality: number -}): string { +}: ImageCdnTransformArgs): string { const args: Array = [] if (width) { args.push(`w=${width}`) @@ -175,64 +192,3 @@ function generateImageArgs({ return args.join(`&`) } - -export function generateImageUrlAlt( - source: { - url: string - filename: string - mimeType: string - internal: { contentDigest: string } - }, - imageArgs: Parameters[0] -): string { - const placeholderOrigin = `http://netlify.com` - const imageParams = generateImageArgsAlt(imageArgs) - - const baseURL = new URL(`${placeholderOrigin}/.netlify/images`) - - baseURL.search = imageParams.toString() - baseURL.searchParams.append(`url`, source.url) - baseURL.searchParams.append(`cd`, source.internal.contentDigest) - - return `${baseURL.pathname}${baseURL.search}` -} - -export function generateImageArgsAlt({ - width, - height, - format, - cropFocus, - quality, -}: WidthOrHeight & { - format: string - cropFocus?: ImageCropFocus | Array - quality: number -}): URLSearchParams { - const params = new URLSearchParams() - - if (width) { - params.append(`w`, width.toString()) - } - if (height) { - params.append(`h`, height.toString()) - } - if (cropFocus) { - params.append(`fit`, `crop`) - if (Array.isArray(cropFocus)) { - // For array of cropFocus values, append them as comma-separated string - params.append(`crop`, cropFocus.join(`,`)) - } else { - params.append(`crop`, cropFocus) - } - } - - if (format) { - params.append(`fm`, format) - } - - if (quality) { - params.append(`q`, quality.toString()) - } - - return params -} diff --git a/packages/gatsby/index.d.ts b/packages/gatsby/index.d.ts index 3adbc7584892e..7c16b4c627dc8 100644 --- a/packages/gatsby/index.d.ts +++ b/packages/gatsby/index.d.ts @@ -45,6 +45,7 @@ export { HeaderRoutes, FunctionsManifest, IAdapterConfig, + ImageCdnTransformArgs, } from "./dist/utils/adapter/types" export const useScrollRestoration: (key: string) => { diff --git a/packages/gatsby/src/utils/adapter/manager.ts b/packages/gatsby/src/utils/adapter/manager.ts index 5c56f16d64c2d..6d6023dee7e4f 100644 --- a/packages/gatsby/src/utils/adapter/manager.ts +++ b/packages/gatsby/src/utils/adapter/manager.ts @@ -251,6 +251,11 @@ export async function initAdapterManager(): Promise { `Can't exclude datastore from engine function without adapter providing deployURL` ) } + + if (configFromAdapter?.imageCDNUrlGeneratorModulePath) { + global.__GATSBY.imageCDNUrlGeneratorModulePath = + configFromAdapter.imageCDNUrlGeneratorModulePath + } } return { diff --git a/packages/gatsby/src/utils/adapter/types.ts b/packages/gatsby/src/utils/adapter/types.ts index 8e004b93d16b1..374b1a2215da2 100644 --- a/packages/gatsby/src/utils/adapter/types.ts +++ b/packages/gatsby/src/utils/adapter/types.ts @@ -149,6 +149,10 @@ export interface IAdapterConfig { * plugin and adapter is used at the same time. */ pluginsToDisable?: Array + /** + * TODO: write description + */ + imageCDNUrlGeneratorModulePath?: string } type WithRequired = T & { [P in K]-?: T[P] } diff --git a/packages/gatsby/src/utils/page-ssr-module/bundle-webpack.ts b/packages/gatsby/src/utils/page-ssr-module/bundle-webpack.ts index 6b253093f6c2a..1590c27cbcf5d 100644 --- a/packages/gatsby/src/utils/page-ssr-module/bundle-webpack.ts +++ b/packages/gatsby/src/utils/page-ssr-module/bundle-webpack.ts @@ -222,19 +222,32 @@ export async function createPageSSRBundle({ ].filter(Boolean) as Array, }) + let IMAGE_CDN_URL_GENERATOR_MODULE_RELATIVE_PATH = `` + if (global.__GATSBY?.imageCDNUrlGeneratorModulePath) { + await fs.copyFile( + global.__GATSBY.imageCDNUrlGeneratorModulePath, + path.join(outputDir, `image-cdn-url-generator.js`) + ) + IMAGE_CDN_URL_GENERATOR_MODULE_RELATIVE_PATH = `./image-cdn-url-generator.js` + } + let functionCode = await fs.readFile( path.join(__dirname, `lambda.js`), `utf-8` ) functionCode = functionCode - .replace( + .replaceAll( `%CDN_DATASTORE_PATH%`, shouldBundleDatastore() ? `` : `${state.adapter.config.deployURL ?? ``}/${LmdbOnCdnPath}` ) - .replace(`%PATH_PREFIX%`, pathPrefix) + .replaceAll(`%PATH_PREFIX%`, pathPrefix) + .replaceAll( + `%IMAGE_CDN_URL_GENERATOR_MODULE_RELATIVE_PATH%`, + IMAGE_CDN_URL_GENERATOR_MODULE_RELATIVE_PATH + ) await fs.outputFile(path.join(outputDir, `lambda.js`), functionCode) diff --git a/packages/gatsby/src/utils/page-ssr-module/lambda.ts b/packages/gatsby/src/utils/page-ssr-module/lambda.ts index c051aff44010b..3b03052f64cc8 100644 --- a/packages/gatsby/src/utils/page-ssr-module/lambda.ts +++ b/packages/gatsby/src/utils/page-ssr-module/lambda.ts @@ -27,10 +27,7 @@ function setupFsWrapper(): string { const TEMP_DIR = path.join(tmpdir(), `gatsby`) const TEMP_CACHE_DIR = path.join(TEMP_DIR, `.cache`) - global.__GATSBY = { - root: TEMP_DIR, - buildId: ``, - } + global.__GATSBY.root = TEMP_DIR // TODO: don't hardcode this const cacheDir = `/var/task/.cache` @@ -90,6 +87,18 @@ function setupFsWrapper(): string { } } +global.__GATSBY = { + root: process.cwd(), + buildId: ``, +} + +// eslint-disable-next-line no-constant-condition +if (`%IMAGE_CDN_URL_GENERATOR_MODULE_RELATIVE_PATH%`) { + global.__GATSBY.imageCDNUrlGeneratorModulePath = require.resolve( + `%IMAGE_CDN_URL_GENERATOR_MODULE_RELATIVE_PATH%` + ) +} + const dbPath = setupFsWrapper() // using require instead of import here for now because of type hell + import path doesn't exist in current context diff --git a/types/gatsby-monorepo/global.d.ts b/types/gatsby-monorepo/global.d.ts index 192414cb53765..1fe3271d47df8 100644 --- a/types/gatsby-monorepo/global.d.ts +++ b/types/gatsby-monorepo/global.d.ts @@ -7,6 +7,7 @@ declare module NodeJS { __GATSBY: { buildId: string root: string + imageCDNUrlGeneratorModulePath?: string } _polyfillRemoteFileCache?: import("gatsby").GatsbyCache From c8aa0c6cd6f2c9b7a6c07b03cee0f9511d5917a4 Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Wed, 29 Nov 2023 12:58:32 +0100 Subject: [PATCH 02/12] provide public types for custom image cdn url generator function signature and individual arguments --- .../src/image-cdn-url-generator.ts | 15 +++--- packages/gatsby-plugin-utils/src/index.ts | 5 ++ .../src/polyfill-remote-file/types.ts | 16 ++++++ .../utils/url-generator.ts | 53 +++++++++---------- packages/gatsby/index.d.ts | 2 + packages/gatsby/src/utils/adapter/types.ts | 6 +++ 6 files changed, 60 insertions(+), 37 deletions(-) diff --git a/packages/gatsby-adapter-netlify/src/image-cdn-url-generator.ts b/packages/gatsby-adapter-netlify/src/image-cdn-url-generator.ts index cb5b9d5e38ed2..3594227bb8616 100644 --- a/packages/gatsby-adapter-netlify/src/image-cdn-url-generator.ts +++ b/packages/gatsby-adapter-netlify/src/image-cdn-url-generator.ts @@ -1,12 +1,11 @@ -import type { ImageCdnTransformArgs } from "gatsby" +import type { + ImageCdnUrlGeneratorFn, + ImageCdnSourceImage, + ImageCdnTransformArgs, +} from "gatsby" export function generateImageUrl( - source: { - url: string - filename: string - mimeType: string - internal: { contentDigest: string } - }, + source: ImageCdnSourceImage, imageArgs: ImageCdnTransformArgs ): string { const placeholderOrigin = `http://netlify.com` @@ -57,4 +56,4 @@ export function generateImageArgs({ return params } -export default generateImageUrl +export default generateImageUrl as ImageCdnUrlGeneratorFn diff --git a/packages/gatsby-plugin-utils/src/index.ts b/packages/gatsby-plugin-utils/src/index.ts index 07d3c8b9418c5..82d3a8cec4b64 100644 --- a/packages/gatsby-plugin-utils/src/index.ts +++ b/packages/gatsby-plugin-utils/src/index.ts @@ -7,4 +7,9 @@ export * from "./has-feature" export type { IRemoteFileNodeInput, IRemoteImageNodeInput, + // CustomImageCDNUrlGeneratorFn is custom to gatsby-plugin-utils + // but should be just ImageCDNUrlGeneratorFn publicly + CustomImageCdnUrlGeneratorFn as ImageCdnUrlGeneratorFn, + ImageCdnSourceImage, + ImageCdnTransformArgs, } from "./polyfill-remote-file/types" diff --git a/packages/gatsby-plugin-utils/src/polyfill-remote-file/types.ts b/packages/gatsby-plugin-utils/src/polyfill-remote-file/types.ts index 8f064d33e9d5a..6c58f2e4ac773 100644 --- a/packages/gatsby-plugin-utils/src/polyfill-remote-file/types.ts +++ b/packages/gatsby-plugin-utils/src/polyfill-remote-file/types.ts @@ -99,3 +99,19 @@ export type ImageCdnTransformArgs = WidthOrHeight & { cropFocus?: ImageCropFocus | Array quality: number } + +interface IImageCdnSourceImage { + url: string + mimeType: string + filename: string + internal: { contentDigest: string } +} + +// drop confusing double `II` from type/interface name +export type ImageCdnSourceImage = IImageCdnSourceImage + +export type CustomImageCdnUrlGeneratorFn = ( + source: ImageCdnSourceImage, + imageArgs: ImageCdnTransformArgs, + pathPrefix: string +) => string diff --git a/packages/gatsby-plugin-utils/src/polyfill-remote-file/utils/url-generator.ts b/packages/gatsby-plugin-utils/src/polyfill-remote-file/utils/url-generator.ts index eb5d98c626de4..a64692bf93e52 100644 --- a/packages/gatsby-plugin-utils/src/polyfill-remote-file/utils/url-generator.ts +++ b/packages/gatsby-plugin-utils/src/polyfill-remote-file/utils/url-generator.ts @@ -3,7 +3,11 @@ import { basename, extname } from "path" import { URL } from "url" import { createContentDigest } from "gatsby-core-utils/create-content-digest" import { isImage } from "../types" -import type { ImageCdnTransformArgs } from "../types" +import type { + CustomImageCdnUrlGeneratorFn, + ImageCdnSourceImage, + ImageCdnTransformArgs, +} from "../types" import type { Store } from "gatsby" // this is an arbitrary origin that we use #branding so we can construct a full url for the URL constructor @@ -66,6 +70,12 @@ export function generateFileUrl( }, store?: Store ): string { + const state = store?.getState() + + const pathPrefix = state?.program?.prefixPaths + ? state?.config?.pathPrefix + : `` + const fileExt = extname(filename) const filenameWithoutExt = basename(filename, fileExt) @@ -74,7 +84,7 @@ export function generateFileUrl( { url, }, - store + pathPrefix )}/${filenameWithoutExt}${fileExt}` ) @@ -83,45 +93,36 @@ export function generateFileUrl( return `${frontendHostName}${parsedURL.pathname}${parsedURL.search}` } -type CustomImageCDNUrlGeneratorFn = ( - source: { - url: string - mimeType: string - filename: string - internal: { contentDigest: string } - }, - imageArgs: ImageCdnTransformArgs -) => string - -let customImageCDNUrlGenerator: CustomImageCDNUrlGeneratorFn | undefined = +let customImageCDNUrlGenerator: CustomImageCdnUrlGeneratorFn | undefined = undefined const preferDefault = (m: any): any => (m && m.default) || m export function generateImageUrl( - source: { - url: string - mimeType: string - filename: string - internal: { contentDigest: string } - }, + source: ImageCdnSourceImage, imageArgs: ImageCdnTransformArgs, store?: Store ): string { + const state = store?.getState() + + const pathPrefix = state?.program?.prefixPaths + ? state?.config?.pathPrefix + : `` + if (global.__GATSBY?.imageCDNUrlGeneratorModulePath) { if (!customImageCDNUrlGenerator) { customImageCDNUrlGenerator = preferDefault( require(global.__GATSBY.imageCDNUrlGeneratorModulePath) - ) as CustomImageCDNUrlGeneratorFn + ) as CustomImageCdnUrlGeneratorFn } - return customImageCDNUrlGenerator(source, imageArgs) + return customImageCDNUrlGenerator(source, imageArgs, pathPrefix) } const filenameWithoutExt = basename(source.filename, extname(source.filename)) const queryStr = generateImageArgs(imageArgs) const parsedURL = new URL( - `${ORIGIN}${generatePublicUrl(source, store)}/${createContentDigest( + `${ORIGIN}${generatePublicUrl(source, pathPrefix)}/${createContentDigest( queryStr )}/${filenameWithoutExt}.${imageArgs.format}` ) @@ -146,14 +147,8 @@ function generatePublicUrl( url: string mimeType?: string }, - store?: Store + pathPrefix: string ): string { - const state = store?.getState() - - const pathPrefix = state?.program?.prefixPaths - ? state?.config?.pathPrefix - : `` - const remoteUrl = createContentDigest(url) let publicUrl = diff --git a/packages/gatsby/index.d.ts b/packages/gatsby/index.d.ts index 7c16b4c627dc8..a450eed930408 100644 --- a/packages/gatsby/index.d.ts +++ b/packages/gatsby/index.d.ts @@ -45,6 +45,8 @@ export { HeaderRoutes, FunctionsManifest, IAdapterConfig, + ImageCdnUrlGeneratorFn, + ImageCdnSourceImage, ImageCdnTransformArgs, } from "./dist/utils/adapter/types" diff --git a/packages/gatsby/src/utils/adapter/types.ts b/packages/gatsby/src/utils/adapter/types.ts index 374b1a2215da2..cacce9295a37b 100644 --- a/packages/gatsby/src/utils/adapter/types.ts +++ b/packages/gatsby/src/utils/adapter/types.ts @@ -2,6 +2,12 @@ import type reporter from "gatsby-cli/lib/reporter" import type { TrailingSlash } from "gatsby-page-utils" import type { IHeader, HttpStatusCode } from "../../redux/types" +export type { + ImageCdnUrlGeneratorFn, + ImageCdnSourceImage, + ImageCdnTransformArgs, +} from "gatsby-plugin-utils" + interface IBaseRoute { /** * Request path that should be matched for this route. From 2dddc0452deafdd592e796fe766bcf3bfc3d1407 Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Thu, 30 Nov 2023 15:05:53 +0100 Subject: [PATCH 03/12] feat: allow adding image cdn allowed url patterns --- package.json | 2 +- packages/gatsby-adapter-netlify/src/index.ts | 3 +++ .../src/gatsby-node.js | 12 ++++++++++- .../gatsby-source-drupal/src/gatsby-node.ts | 6 +++++- .../src/gatsby-node.ts | 1 + .../src/steps/add-image-cdn-allowed-url.ts | 19 +++++++++++++++++ .../src/steps/index.ts | 1 + packages/gatsby/index.d.ts | 10 ++++++++- .../gatsby/src/redux/actions/restricted.ts | 21 +++++++++++++++++++ .../redux/reducers/image-cdn-allowed-urls.ts | 18 ++++++++++++++++ packages/gatsby/src/redux/reducers/index.ts | 2 ++ packages/gatsby/src/redux/types.ts | 11 ++++++++++ packages/gatsby/src/utils/adapter/manager.ts | 10 +++++++++ packages/gatsby/src/utils/adapter/types.ts | 4 ++++ yarn.lock | 8 +++---- 15 files changed, 120 insertions(+), 8 deletions(-) create mode 100644 packages/gatsby-source-wordpress/src/steps/add-image-cdn-allowed-url.ts create mode 100644 packages/gatsby/src/redux/reducers/image-cdn-allowed-urls.ts diff --git a/package.json b/package.json index a858da92766a5..532800061905c 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "@types/jest": "^29.5.2", "@types/joi": "^14.3.4", "@types/lodash": "^4.14.195", - "@types/node": "^12.20.55", + "@types/node": "18.0.0", "@types/node-fetch": "^2.6.4", "@types/normalize-path": "^3.0.0", "@types/react": "^18.2.14", diff --git a/packages/gatsby-adapter-netlify/src/index.ts b/packages/gatsby-adapter-netlify/src/index.ts index e741758167612..ce916ad08c997 100644 --- a/packages/gatsby-adapter-netlify/src/index.ts +++ b/packages/gatsby-adapter-netlify/src/index.ts @@ -72,6 +72,7 @@ const createNetlifyAdapter: AdapterInit = options => { routesManifest, functionsManifest, headerRoutes, + imageCdnAllowedUrls, }): Promise { const { lambdasThatUseCaching } = await handleRoutesManifest( routesManifest, @@ -85,6 +86,8 @@ const createNetlifyAdapter: AdapterInit = options => { lambdasThatUseCaching.get(fun.functionId) ) } + + console.log({ imageCdnAllowedUrls }) }, config: ({ reporter }): IAdapterConfig => { reporter.verbose( diff --git a/packages/gatsby-source-contentful/src/gatsby-node.js b/packages/gatsby-source-contentful/src/gatsby-node.js index 99528f805a6c1..38fb7d011cdc5 100644 --- a/packages/gatsby-source-contentful/src/gatsby-node.js +++ b/packages/gatsby-source-contentful/src/gatsby-node.js @@ -1,4 +1,5 @@ // @ts-check +import { join } from "path/posix" import _ from "lodash" import origFetch from "node-fetch" import fetchRetry from "@vercel/fetch-retry" @@ -43,7 +44,10 @@ const validateContentfulAccess = async pluginOptions => { return undefined } -export const onPreInit = async ({ store, reporter }) => { +export const onPreInit = async ( + { store, reporter, actions }, + pluginOptions +) => { // if gatsby-plugin-image is not installed try { await import(`gatsby-plugin-image/graphql-utils`) @@ -69,6 +73,12 @@ export const onPreInit = async ({ store, reporter }) => { }, }) } + + if (typeof actions?.addImageCdnAllowedUrl === `function`) { + actions.addImageCdnAllowedUrl( + join(`https://images.ctfassets.net/`, pluginOptions.spaceId, `*`) + ) + } } export const pluginOptionsSchema = ({ Joi }) => diff --git a/packages/gatsby-source-drupal/src/gatsby-node.ts b/packages/gatsby-source-drupal/src/gatsby-node.ts index 275aea5c3f949..d72ce08967ec6 100644 --- a/packages/gatsby-source-drupal/src/gatsby-node.ts +++ b/packages/gatsby-source-drupal/src/gatsby-node.ts @@ -132,8 +132,12 @@ function gracefullyRethrow(activity, error) { } } -exports.onPreBootstrap = (_, pluginOptions) => { +exports.onPreBootstrap = ({ actions }, pluginOptions) => { setOptions(pluginOptions) + + if (typeof actions?.addImageCdnAllowedUrl === `function`) { + actions.addImageCdnAllowedUrl(urlJoin(pluginOptions.baseUrl, `*`)) + } } exports.sourceNodes = async ( diff --git a/packages/gatsby-source-wordpress/src/gatsby-node.ts b/packages/gatsby-source-wordpress/src/gatsby-node.ts index a14f388f84c02..b77dacdc5147c 100644 --- a/packages/gatsby-source-wordpress/src/gatsby-node.ts +++ b/packages/gatsby-source-wordpress/src/gatsby-node.ts @@ -6,6 +6,7 @@ exports.onPluginInit = runApiSteps( steps.setGatsbyApiToState, steps.setErrorMap, steps.setRequestHeaders, + steps.addImageCdnAllowedUrl, steps.hideAuthPluginOptions, ], `onPluginInit` diff --git a/packages/gatsby-source-wordpress/src/steps/add-image-cdn-allowed-url.ts b/packages/gatsby-source-wordpress/src/steps/add-image-cdn-allowed-url.ts new file mode 100644 index 0000000000000..bf8cbf6a3bb41 --- /dev/null +++ b/packages/gatsby-source-wordpress/src/steps/add-image-cdn-allowed-url.ts @@ -0,0 +1,19 @@ +import { join } from "path/posix" +import { getPluginOptions } from "~/utils/get-gatsby-api" + +import type { Step } from "~/utils/run-steps" + +export const addImageCdnAllowedUrl: Step = ({ actions }): void => { + if (typeof actions?.addImageCdnAllowedUrl !== `function`) { + return + } + + const pluginOptions = getPluginOptions() + + const { url } = pluginOptions + + // url has `/graphql` at the end, so we need to remove it + const wordpressRootUrl = url.replace(`/graphql`, ``) + + actions.addImageCdnAllowedUrl(join(wordpressRootUrl, `*`)) +} diff --git a/packages/gatsby-source-wordpress/src/steps/index.ts b/packages/gatsby-source-wordpress/src/steps/index.ts index 7f3845abd5510..c4c8882b07cd0 100644 --- a/packages/gatsby-source-wordpress/src/steps/index.ts +++ b/packages/gatsby-source-wordpress/src/steps/index.ts @@ -21,6 +21,7 @@ export { logPostBuildWarnings } from "~/steps/log-post-build-warnings" export { imageRoutes } from "~/steps/image-routes" export { setRequestHeaders } from "./set-request-headers" +export { addImageCdnAllowedUrl } from "./add-image-cdn-allowed-url" export { hideAuthPluginOptions, diff --git a/packages/gatsby/index.d.ts b/packages/gatsby/index.d.ts index a450eed930408..8f60b31327e98 100644 --- a/packages/gatsby/index.d.ts +++ b/packages/gatsby/index.d.ts @@ -1516,7 +1516,15 @@ export interface Actions { /** * Marks the source plugin that called this function as stateful. Gatsby will not check for stale nodes for any plugin that calls this. */ - enableStatefulSourceNodes?(this: void, plugin?: ActionPlugin) + enableStatefulSourceNodes?(this: void, plugin?: ActionPlugin): void + + /** @see https://www.gatsbyjs.com/docs/actions/#addImageCdnAllowedUrl */ + addImageCdnAllowedUrl?( + this: void, + url: string | Array, + plugin?: ActionPlugin, + traceId?: string + ): void } export interface Store { diff --git a/packages/gatsby/src/redux/actions/restricted.ts b/packages/gatsby/src/redux/actions/restricted.ts index d74000d2977a5..adcc2a929bada 100644 --- a/packages/gatsby/src/redux/actions/restricted.ts +++ b/packages/gatsby/src/redux/actions/restricted.ts @@ -22,6 +22,7 @@ import { ICreateResolverContext, IGatsbyPluginContext, ICreateSliceAction, + IAddImageCdnAllowedUrl, } from "../types" import { generateComponentChunkName } from "../../utils/js-chunk-names" import { store } from "../index" @@ -36,6 +37,7 @@ type RestrictionActionNames = | "addThirdPartySchema" | "printTypeDefinitions" | "createSlice" + | "addImageCdnAllowedUrl" type SomeActionCreator = | ActionCreator @@ -533,6 +535,22 @@ export const actions = { throw new Error(`createSlice is only available in Gatsby v5`) } }, + /** + * @todo + */ + addImageCdnAllowedUrl: ( + url: string | Array, + plugin: IGatsbyPlugin, + traceId?: string + ): IAddImageCdnAllowedUrl => { + const urls = Array.isArray(url) ? url : [url] + return { + type: `ADD_IMAGE_CDN_ALLOWED_URL`, + payload: { urls }, + plugin, + traceId, + } + }, } const withDeprecationWarning = @@ -656,4 +674,7 @@ export const availableActionsByAPI = mapAvailableActionsToAPIs({ createSlice: { [ALLOWED_IN]: [`createPages`], }, + addImageCdnAllowedUrl: { + [ALLOWED_IN]: [`onPreInit`, `onPreBootstrap`, `onPluginInit`], + }, }) diff --git a/packages/gatsby/src/redux/reducers/image-cdn-allowed-urls.ts b/packages/gatsby/src/redux/reducers/image-cdn-allowed-urls.ts new file mode 100644 index 0000000000000..2b187dfe026a8 --- /dev/null +++ b/packages/gatsby/src/redux/reducers/image-cdn-allowed-urls.ts @@ -0,0 +1,18 @@ +import { IGatsbyState, ActionsUnion } from "../types" + +export const imageCdnAllowedUrlsReducer = ( + state: IGatsbyState["imageCdnAllowedUrls"] = new Set(), + action: ActionsUnion +): IGatsbyState["imageCdnAllowedUrls"] => { + switch (action.type) { + case `ADD_IMAGE_CDN_ALLOWED_URL`: { + for (const url of action.payload.urls) { + state.add(url) + } + + return state + } + default: + return state + } +} diff --git a/packages/gatsby/src/redux/reducers/index.ts b/packages/gatsby/src/redux/reducers/index.ts index 54e4ef76e47ed..501ef6102f59d 100644 --- a/packages/gatsby/src/redux/reducers/index.ts +++ b/packages/gatsby/src/redux/reducers/index.ts @@ -38,6 +38,7 @@ import { slicesReducer } from "./slices" import { componentsUsingSlicesReducer } from "./components-using-slices" import { slicesByTemplateReducer } from "./slices-by-template" import { adapterReducer } from "./adapter" +import { imageCdnAllowedUrlsReducer } from "./image-cdn-allowed-urls" /** * @property exports.nodesTouched Set @@ -83,4 +84,5 @@ export { slicesByTemplateReducer as slicesByTemplate, telemetryReducer as telemetry, adapterReducer as adapter, + imageCdnAllowedUrlsReducer as imageCdnAllowedUrls, } diff --git a/packages/gatsby/src/redux/types.ts b/packages/gatsby/src/redux/types.ts index 01c409da25475..99900bc06c2a9 100644 --- a/packages/gatsby/src/redux/types.ts +++ b/packages/gatsby/src/redux/types.ts @@ -425,6 +425,7 @@ export interface IGatsbyState { manager: IAdapterManager config: IAdapterFinalConfig } + imageCdnAllowedUrls: Set } export type GatsbyStateKeys = keyof IGatsbyState @@ -539,6 +540,7 @@ export type ActionsUnion = | IClearGatsbyImageSourceUrlAction | ISetAdapterAction | IDisablePluginsByNameAction + | IAddImageCdnAllowedUrl export interface IInitAction { type: `INIT` @@ -1218,6 +1220,15 @@ export interface IDisablePluginsByNameAction { } } +export interface IAddImageCdnAllowedUrl { + type: `ADD_IMAGE_CDN_ALLOWED_URL` + payload: { + urls: Array + } + plugin: IGatsbyPlugin + traceId?: string +} + export interface ITelemetry { gatsbyImageSourceUrls: Set } diff --git a/packages/gatsby/src/utils/adapter/manager.ts b/packages/gatsby/src/utils/adapter/manager.ts index 6d6023dee7e4f..f767891ec0455 100644 --- a/packages/gatsby/src/utils/adapter/manager.ts +++ b/packages/gatsby/src/utils/adapter/manager.ts @@ -204,6 +204,7 @@ export async function initAdapterManager(): Promise { let _routesManifest: RoutesManifest | undefined = undefined let _functionsManifest: FunctionsManifest | undefined = undefined let _headerRoutes: HeaderRoutes | undefined = undefined + let _imageCdnAllowedUrls: Array | undefined = undefined const adaptContext: IAdaptContext = { get routesManifest(): RoutesManifest { if (!_routesManifest) { @@ -230,6 +231,15 @@ export async function initAdapterManager(): Promise { return _headerRoutes }, + get imageCdnAllowedUrls(): Array { + if (!_imageCdnAllowedUrls) { + _imageCdnAllowedUrls = Array.from( + store.getState().imageCdnAllowedUrls + ) + } + + return _imageCdnAllowedUrls + }, reporter, // Our internal Gatsby config allows this to be undefined but for the adapter we should always pass through the default values and correctly show this in the TypeScript types trailingSlash: trailingSlash as TrailingSlash, diff --git a/packages/gatsby/src/utils/adapter/types.ts b/packages/gatsby/src/utils/adapter/types.ts index cacce9295a37b..fc466e90a57cc 100644 --- a/packages/gatsby/src/utils/adapter/types.ts +++ b/packages/gatsby/src/utils/adapter/types.ts @@ -119,6 +119,10 @@ export interface IAdaptContext extends IDefaultContext { * @see https://www.gatsbyjs.com/docs/reference/config-files/gatsby-config/#trailingslash */ trailingSlash: TrailingSlash + /** + * @todo + */ + imageCdnAllowedUrls: Array } export interface ICacheContext extends IDefaultContext { diff --git a/yarn.lock b/yarn.lock index 55d476539b4ee..6b5bd5dc97560 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4848,10 +4848,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.16.19.tgz#cb03fca8910fdeb7595b755126a8a78144714eea" integrity sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA== -"@types/node@^12.20.55": - version "12.20.55" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240" - integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ== +"@types/node@18.0.0": + version "18.0.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.0.tgz#67c7b724e1bcdd7a8821ce0d5ee184d3b4dd525a" + integrity sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA== "@types/node@^14.18.42", "@types/node@^14.18.52": version "14.18.53" From b96cd861766996d4e2772161654b858fbc47dc42 Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Wed, 6 Dec 2023 10:01:51 +0100 Subject: [PATCH 04/12] Module.createRequireFromPath doesn't exist anymore in Node 18, and because package requires at least that version we remove it --- packages/gatsby-core-utils/src/create-require-from-path.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/gatsby-core-utils/src/create-require-from-path.ts b/packages/gatsby-core-utils/src/create-require-from-path.ts index 41def521e04f9..dc5ffdb46195e 100644 --- a/packages/gatsby-core-utils/src/create-require-from-path.ts +++ b/packages/gatsby-core-utils/src/create-require-from-path.ts @@ -21,6 +21,5 @@ const fallback = (filename: string): NodeRequire => { return mod.exports } -// Polyfill Node's `Module.createRequireFromPath` if not present (added in Node v10.12.0) -export const createRequireFromPath = - Module.createRequire || Module.createRequireFromPath || fallback +// Polyfill Node's `Module.createRequire` if not present (added in Node v10.12.0) +export const createRequireFromPath = Module.createRequire || fallback From e4d9b92608016a31f5f7b40cc8b67553b6f394dc Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Wed, 6 Dec 2023 10:34:08 +0100 Subject: [PATCH 05/12] fix contentful source image url --- package.json | 2 +- .../gatsby-core-utils/src/create-require-from-path.ts | 5 +++-- packages/gatsby-source-contentful/src/gatsby-node.js | 3 +-- .../src/steps/add-image-cdn-allowed-url.ts | 5 +++-- yarn.lock | 8 ++++---- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 532800061905c..a858da92766a5 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "@types/jest": "^29.5.2", "@types/joi": "^14.3.4", "@types/lodash": "^4.14.195", - "@types/node": "18.0.0", + "@types/node": "^12.20.55", "@types/node-fetch": "^2.6.4", "@types/normalize-path": "^3.0.0", "@types/react": "^18.2.14", diff --git a/packages/gatsby-core-utils/src/create-require-from-path.ts b/packages/gatsby-core-utils/src/create-require-from-path.ts index dc5ffdb46195e..41def521e04f9 100644 --- a/packages/gatsby-core-utils/src/create-require-from-path.ts +++ b/packages/gatsby-core-utils/src/create-require-from-path.ts @@ -21,5 +21,6 @@ const fallback = (filename: string): NodeRequire => { return mod.exports } -// Polyfill Node's `Module.createRequire` if not present (added in Node v10.12.0) -export const createRequireFromPath = Module.createRequire || fallback +// Polyfill Node's `Module.createRequireFromPath` if not present (added in Node v10.12.0) +export const createRequireFromPath = + Module.createRequire || Module.createRequireFromPath || fallback diff --git a/packages/gatsby-source-contentful/src/gatsby-node.js b/packages/gatsby-source-contentful/src/gatsby-node.js index 38fb7d011cdc5..06c5788c7535b 100644 --- a/packages/gatsby-source-contentful/src/gatsby-node.js +++ b/packages/gatsby-source-contentful/src/gatsby-node.js @@ -1,5 +1,4 @@ // @ts-check -import { join } from "path/posix" import _ from "lodash" import origFetch from "node-fetch" import fetchRetry from "@vercel/fetch-retry" @@ -76,7 +75,7 @@ export const onPreInit = async ( if (typeof actions?.addImageCdnAllowedUrl === `function`) { actions.addImageCdnAllowedUrl( - join(`https://images.ctfassets.net/`, pluginOptions.spaceId, `*`) + `https://images.ctfassets.net/${pluginOptions.spaceId}/*` ) } } diff --git a/packages/gatsby-source-wordpress/src/steps/add-image-cdn-allowed-url.ts b/packages/gatsby-source-wordpress/src/steps/add-image-cdn-allowed-url.ts index bf8cbf6a3bb41..18d876b14e089 100644 --- a/packages/gatsby-source-wordpress/src/steps/add-image-cdn-allowed-url.ts +++ b/packages/gatsby-source-wordpress/src/steps/add-image-cdn-allowed-url.ts @@ -1,4 +1,4 @@ -import { join } from "path/posix" +import nodePath from "path" import { getPluginOptions } from "~/utils/get-gatsby-api" import type { Step } from "~/utils/run-steps" @@ -13,7 +13,8 @@ export const addImageCdnAllowedUrl: Step = ({ actions }): void => { const { url } = pluginOptions // url has `/graphql` at the end, so we need to remove it + // todo: use siteUrl from wpGraphql instead of url and trying to replace graphql const wordpressRootUrl = url.replace(`/graphql`, ``) - actions.addImageCdnAllowedUrl(join(wordpressRootUrl, `*`)) + actions.addImageCdnAllowedUrl(nodePath.posix.join(wordpressRootUrl, `*`)) } diff --git a/yarn.lock b/yarn.lock index ab12cf8cbc74a..edca74a7eaf04 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4853,10 +4853,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.16.19.tgz#cb03fca8910fdeb7595b755126a8a78144714eea" integrity sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA== -"@types/node@18.0.0": - version "18.0.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.0.tgz#67c7b724e1bcdd7a8821ce0d5ee184d3b4dd525a" - integrity sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA== +"@types/node@^12.20.55": + version "12.20.55" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240" + integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ== "@types/node@^14.18.42", "@types/node@^14.18.52": version "14.18.53" From 018eec69afe347fcd42c7f063d616f7b7922df5d Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Wed, 6 Dec 2023 11:15:22 +0100 Subject: [PATCH 06/12] fix wordpress source image url --- .../gatsby-source-wordpress/src/gatsby-node.ts | 2 +- .../src/steps/add-image-cdn-allowed-url.ts | 15 ++++++++------- packages/gatsby/src/redux/actions/restricted.ts | 8 +++++++- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/gatsby-source-wordpress/src/gatsby-node.ts b/packages/gatsby-source-wordpress/src/gatsby-node.ts index b77dacdc5147c..309b36b0303e8 100644 --- a/packages/gatsby-source-wordpress/src/gatsby-node.ts +++ b/packages/gatsby-source-wordpress/src/gatsby-node.ts @@ -6,7 +6,6 @@ exports.onPluginInit = runApiSteps( steps.setGatsbyApiToState, steps.setErrorMap, steps.setRequestHeaders, - steps.addImageCdnAllowedUrl, steps.hideAuthPluginOptions, ], `onPluginInit` @@ -25,6 +24,7 @@ exports.createSchemaCustomization = runApiSteps( steps.ensurePluginRequirementsAreMet, steps.ingestRemoteSchema, steps.createSchemaCustomization, + steps.addImageCdnAllowedUrl, ], `createSchemaCustomization` ) diff --git a/packages/gatsby-source-wordpress/src/steps/add-image-cdn-allowed-url.ts b/packages/gatsby-source-wordpress/src/steps/add-image-cdn-allowed-url.ts index 18d876b14e089..21f9e57d3930b 100644 --- a/packages/gatsby-source-wordpress/src/steps/add-image-cdn-allowed-url.ts +++ b/packages/gatsby-source-wordpress/src/steps/add-image-cdn-allowed-url.ts @@ -1,5 +1,5 @@ import nodePath from "path" -import { getPluginOptions } from "~/utils/get-gatsby-api" +import { getStore } from "~/store" import type { Step } from "~/utils/run-steps" @@ -8,13 +8,14 @@ export const addImageCdnAllowedUrl: Step = ({ actions }): void => { return } - const pluginOptions = getPluginOptions() + const { wpUrl } = getStore().getState().remoteSchema - const { url } = pluginOptions + if (!wpUrl) { + return + } - // url has `/graphql` at the end, so we need to remove it - // todo: use siteUrl from wpGraphql instead of url and trying to replace graphql - const wordpressRootUrl = url.replace(`/graphql`, ``) + const wordpressUrl = new URL(wpUrl) + wordpressUrl.pathname = nodePath.posix.join(wordpressUrl.pathname, `*`) - actions.addImageCdnAllowedUrl(nodePath.posix.join(wordpressRootUrl, `*`)) + actions.addImageCdnAllowedUrl(wordpressUrl.href) } diff --git a/packages/gatsby/src/redux/actions/restricted.ts b/packages/gatsby/src/redux/actions/restricted.ts index adcc2a929bada..a08b82fe2a757 100644 --- a/packages/gatsby/src/redux/actions/restricted.ts +++ b/packages/gatsby/src/redux/actions/restricted.ts @@ -675,6 +675,12 @@ export const availableActionsByAPI = mapAvailableActionsToAPIs({ [ALLOWED_IN]: [`createPages`], }, addImageCdnAllowedUrl: { - [ALLOWED_IN]: [`onPreInit`, `onPreBootstrap`, `onPluginInit`], + [ALLOWED_IN]: [ + `onPreInit`, + `onPreBootstrap`, + `onPluginInit`, + `createSchemaCustomization`, + `sourceNodes`, + ], }, }) From f50a89df35d57ad923d74a22f90a112f775b102d Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Wed, 6 Dec 2023 11:53:13 +0100 Subject: [PATCH 07/12] rename ImageCdnAllowed to RemoteFileAllowed as it's not just for image cdn --- packages/gatsby-adapter-netlify/src/index.ts | 4 ++-- packages/gatsby-source-contentful/src/gatsby-node.js | 4 ++-- packages/gatsby-source-drupal/src/gatsby-node.ts | 4 ++-- packages/gatsby-source-wordpress/src/gatsby-node.ts | 2 +- ...-cdn-allowed-url.ts => add-remote-file-allowed-url.ts} | 6 +++--- packages/gatsby-source-wordpress/src/steps/index.ts | 2 +- packages/gatsby/index.d.ts | 4 ++-- packages/gatsby/src/redux/actions/restricted.ts | 8 ++++---- packages/gatsby/src/redux/reducers/index.ts | 4 ++-- ...ge-cdn-allowed-urls.ts => remote-file-allowed-urls.ts} | 8 ++++---- packages/gatsby/src/redux/types.ts | 4 ++-- packages/gatsby/src/utils/adapter/manager.ts | 4 ++-- packages/gatsby/src/utils/adapter/types.ts | 2 +- 13 files changed, 28 insertions(+), 28 deletions(-) rename packages/gatsby-source-wordpress/src/steps/{add-image-cdn-allowed-url.ts => add-remote-file-allowed-url.ts} (63%) rename packages/gatsby/src/redux/reducers/{image-cdn-allowed-urls.ts => remote-file-allowed-urls.ts} (55%) diff --git a/packages/gatsby-adapter-netlify/src/index.ts b/packages/gatsby-adapter-netlify/src/index.ts index dc2b9e2ef5999..1b5b6a3ee263a 100644 --- a/packages/gatsby-adapter-netlify/src/index.ts +++ b/packages/gatsby-adapter-netlify/src/index.ts @@ -84,7 +84,7 @@ const createNetlifyAdapter: AdapterInit = options => { functionsManifest, headerRoutes, pathPrefix, - imageCdnAllowedUrls, + remoteFileAllowedUrls, }): Promise { if (useNetlifyImageCDN) { await prepareFileCdnHandler({ pathPrefix }) @@ -103,7 +103,7 @@ const createNetlifyAdapter: AdapterInit = options => { ) } - console.log({ imageCdnAllowedUrls }) + console.log({ remoteFileAllowedUrls }) }, config: ({ reporter }): IAdapterConfig => { reporter.verbose( diff --git a/packages/gatsby-source-contentful/src/gatsby-node.js b/packages/gatsby-source-contentful/src/gatsby-node.js index 06c5788c7535b..8f582ab7a9916 100644 --- a/packages/gatsby-source-contentful/src/gatsby-node.js +++ b/packages/gatsby-source-contentful/src/gatsby-node.js @@ -73,8 +73,8 @@ export const onPreInit = async ( }) } - if (typeof actions?.addImageCdnAllowedUrl === `function`) { - actions.addImageCdnAllowedUrl( + if (typeof actions?.addRemoteFileAllowedUrl === `function`) { + actions.addRemoteFileAllowedUrl( `https://images.ctfassets.net/${pluginOptions.spaceId}/*` ) } diff --git a/packages/gatsby-source-drupal/src/gatsby-node.ts b/packages/gatsby-source-drupal/src/gatsby-node.ts index d72ce08967ec6..8317b65758058 100644 --- a/packages/gatsby-source-drupal/src/gatsby-node.ts +++ b/packages/gatsby-source-drupal/src/gatsby-node.ts @@ -135,8 +135,8 @@ function gracefullyRethrow(activity, error) { exports.onPreBootstrap = ({ actions }, pluginOptions) => { setOptions(pluginOptions) - if (typeof actions?.addImageCdnAllowedUrl === `function`) { - actions.addImageCdnAllowedUrl(urlJoin(pluginOptions.baseUrl, `*`)) + if (typeof actions?.addRemoteFileAllowedUrl === `function`) { + actions.addRemoteFileAllowedUrl(urlJoin(pluginOptions.baseUrl, `*`)) } } diff --git a/packages/gatsby-source-wordpress/src/gatsby-node.ts b/packages/gatsby-source-wordpress/src/gatsby-node.ts index 309b36b0303e8..4c6c62f21bf98 100644 --- a/packages/gatsby-source-wordpress/src/gatsby-node.ts +++ b/packages/gatsby-source-wordpress/src/gatsby-node.ts @@ -24,7 +24,7 @@ exports.createSchemaCustomization = runApiSteps( steps.ensurePluginRequirementsAreMet, steps.ingestRemoteSchema, steps.createSchemaCustomization, - steps.addImageCdnAllowedUrl, + steps.addRemoteFileAllowedUrl, ], `createSchemaCustomization` ) diff --git a/packages/gatsby-source-wordpress/src/steps/add-image-cdn-allowed-url.ts b/packages/gatsby-source-wordpress/src/steps/add-remote-file-allowed-url.ts similarity index 63% rename from packages/gatsby-source-wordpress/src/steps/add-image-cdn-allowed-url.ts rename to packages/gatsby-source-wordpress/src/steps/add-remote-file-allowed-url.ts index 21f9e57d3930b..249a01827e041 100644 --- a/packages/gatsby-source-wordpress/src/steps/add-image-cdn-allowed-url.ts +++ b/packages/gatsby-source-wordpress/src/steps/add-remote-file-allowed-url.ts @@ -3,8 +3,8 @@ import { getStore } from "~/store" import type { Step } from "~/utils/run-steps" -export const addImageCdnAllowedUrl: Step = ({ actions }): void => { - if (typeof actions?.addImageCdnAllowedUrl !== `function`) { +export const addRemoteFileAllowedUrl: Step = ({ actions }): void => { + if (typeof actions?.addRemoteFileAllowedUrl !== `function`) { return } @@ -17,5 +17,5 @@ export const addImageCdnAllowedUrl: Step = ({ actions }): void => { const wordpressUrl = new URL(wpUrl) wordpressUrl.pathname = nodePath.posix.join(wordpressUrl.pathname, `*`) - actions.addImageCdnAllowedUrl(wordpressUrl.href) + actions.addRemoteFileAllowedUrl(wordpressUrl.href) } diff --git a/packages/gatsby-source-wordpress/src/steps/index.ts b/packages/gatsby-source-wordpress/src/steps/index.ts index c4c8882b07cd0..e0bfdec0a1c24 100644 --- a/packages/gatsby-source-wordpress/src/steps/index.ts +++ b/packages/gatsby-source-wordpress/src/steps/index.ts @@ -21,7 +21,7 @@ export { logPostBuildWarnings } from "~/steps/log-post-build-warnings" export { imageRoutes } from "~/steps/image-routes" export { setRequestHeaders } from "./set-request-headers" -export { addImageCdnAllowedUrl } from "./add-image-cdn-allowed-url" +export { addRemoteFileAllowedUrl } from "./add-remote-file-allowed-url" export { hideAuthPluginOptions, diff --git a/packages/gatsby/index.d.ts b/packages/gatsby/index.d.ts index 4dcd60fd3c001..118356773d61e 100644 --- a/packages/gatsby/index.d.ts +++ b/packages/gatsby/index.d.ts @@ -1520,8 +1520,8 @@ export interface Actions { */ enableStatefulSourceNodes?(this: void, plugin?: ActionPlugin): void - /** @see https://www.gatsbyjs.com/docs/actions/#addImageCdnAllowedUrl */ - addImageCdnAllowedUrl?( + /** @see https://www.gatsbyjs.com/docs/actions/#addRemoteFileAllowedUrl */ + addRemoteFileAllowedUrl?( this: void, url: string | Array, plugin?: ActionPlugin, diff --git a/packages/gatsby/src/redux/actions/restricted.ts b/packages/gatsby/src/redux/actions/restricted.ts index a08b82fe2a757..18ad234a020ea 100644 --- a/packages/gatsby/src/redux/actions/restricted.ts +++ b/packages/gatsby/src/redux/actions/restricted.ts @@ -37,7 +37,7 @@ type RestrictionActionNames = | "addThirdPartySchema" | "printTypeDefinitions" | "createSlice" - | "addImageCdnAllowedUrl" + | "addRemoteFileAllowedUrl" type SomeActionCreator = | ActionCreator @@ -538,14 +538,14 @@ export const actions = { /** * @todo */ - addImageCdnAllowedUrl: ( + addRemoteFileAllowedUrl: ( url: string | Array, plugin: IGatsbyPlugin, traceId?: string ): IAddImageCdnAllowedUrl => { const urls = Array.isArray(url) ? url : [url] return { - type: `ADD_IMAGE_CDN_ALLOWED_URL`, + type: `ADD_REMOTE_FILE_ALLOWED_URL`, payload: { urls }, plugin, traceId, @@ -674,7 +674,7 @@ export const availableActionsByAPI = mapAvailableActionsToAPIs({ createSlice: { [ALLOWED_IN]: [`createPages`], }, - addImageCdnAllowedUrl: { + addRemoteFileAllowedUrl: { [ALLOWED_IN]: [ `onPreInit`, `onPreBootstrap`, diff --git a/packages/gatsby/src/redux/reducers/index.ts b/packages/gatsby/src/redux/reducers/index.ts index 501ef6102f59d..e2b394a5de986 100644 --- a/packages/gatsby/src/redux/reducers/index.ts +++ b/packages/gatsby/src/redux/reducers/index.ts @@ -38,7 +38,7 @@ import { slicesReducer } from "./slices" import { componentsUsingSlicesReducer } from "./components-using-slices" import { slicesByTemplateReducer } from "./slices-by-template" import { adapterReducer } from "./adapter" -import { imageCdnAllowedUrlsReducer } from "./image-cdn-allowed-urls" +import { remoteFileAllowedUrlsReducer } from "./remote-file-allowed-urls" /** * @property exports.nodesTouched Set @@ -84,5 +84,5 @@ export { slicesByTemplateReducer as slicesByTemplate, telemetryReducer as telemetry, adapterReducer as adapter, - imageCdnAllowedUrlsReducer as imageCdnAllowedUrls, + remoteFileAllowedUrlsReducer as remoteFileAllowedUrls, } diff --git a/packages/gatsby/src/redux/reducers/image-cdn-allowed-urls.ts b/packages/gatsby/src/redux/reducers/remote-file-allowed-urls.ts similarity index 55% rename from packages/gatsby/src/redux/reducers/image-cdn-allowed-urls.ts rename to packages/gatsby/src/redux/reducers/remote-file-allowed-urls.ts index 2b187dfe026a8..b076cdea80642 100644 --- a/packages/gatsby/src/redux/reducers/image-cdn-allowed-urls.ts +++ b/packages/gatsby/src/redux/reducers/remote-file-allowed-urls.ts @@ -1,11 +1,11 @@ import { IGatsbyState, ActionsUnion } from "../types" -export const imageCdnAllowedUrlsReducer = ( - state: IGatsbyState["imageCdnAllowedUrls"] = new Set(), +export const remoteFileAllowedUrlsReducer = ( + state: IGatsbyState["remoteFileAllowedUrls"] = new Set(), action: ActionsUnion -): IGatsbyState["imageCdnAllowedUrls"] => { +): IGatsbyState["remoteFileAllowedUrls"] => { switch (action.type) { - case `ADD_IMAGE_CDN_ALLOWED_URL`: { + case `ADD_REMOTE_FILE_ALLOWED_URL`: { for (const url of action.payload.urls) { state.add(url) } diff --git a/packages/gatsby/src/redux/types.ts b/packages/gatsby/src/redux/types.ts index 99900bc06c2a9..44e8abaafcfa2 100644 --- a/packages/gatsby/src/redux/types.ts +++ b/packages/gatsby/src/redux/types.ts @@ -425,7 +425,7 @@ export interface IGatsbyState { manager: IAdapterManager config: IAdapterFinalConfig } - imageCdnAllowedUrls: Set + remoteFileAllowedUrls: Set } export type GatsbyStateKeys = keyof IGatsbyState @@ -1221,7 +1221,7 @@ export interface IDisablePluginsByNameAction { } export interface IAddImageCdnAllowedUrl { - type: `ADD_IMAGE_CDN_ALLOWED_URL` + type: `ADD_REMOTE_FILE_ALLOWED_URL` payload: { urls: Array } diff --git a/packages/gatsby/src/utils/adapter/manager.ts b/packages/gatsby/src/utils/adapter/manager.ts index 706e407f36b05..bfca83f836482 100644 --- a/packages/gatsby/src/utils/adapter/manager.ts +++ b/packages/gatsby/src/utils/adapter/manager.ts @@ -231,10 +231,10 @@ export async function initAdapterManager(): Promise { return _headerRoutes }, - get imageCdnAllowedUrls(): Array { + get remoteFileAllowedUrls(): Array { if (!_imageCdnAllowedUrls) { _imageCdnAllowedUrls = Array.from( - store.getState().imageCdnAllowedUrls + store.getState().remoteFileAllowedUrls ) } diff --git a/packages/gatsby/src/utils/adapter/types.ts b/packages/gatsby/src/utils/adapter/types.ts index 6c32793de34cd..dc0e23aa70536 100644 --- a/packages/gatsby/src/utils/adapter/types.ts +++ b/packages/gatsby/src/utils/adapter/types.ts @@ -132,7 +132,7 @@ export interface IAdaptContext extends IDefaultContext { /** * @todo */ - imageCdnAllowedUrls: Array + remoteFileAllowedUrls: Array } export interface ICacheContext extends IDefaultContext { From 68ee816defdbd73a4206effb1dc4bfca0473864b Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Wed, 6 Dec 2023 14:02:17 +0100 Subject: [PATCH 08/12] compare allowed remote urls in netlify.toml with ones generated by gatsby --- packages/gatsby-adapter-netlify/.babelrc | 2 +- packages/gatsby-adapter-netlify/package.json | 1 + .../src/allowed-remote-urls.ts | 24 ++ .../src/file-cdn-handler.ts | 1 + packages/gatsby-adapter-netlify/src/index.ts | 7 +- yarn.lock | 309 +++++++++++++++++- 6 files changed, 327 insertions(+), 17 deletions(-) create mode 100644 packages/gatsby-adapter-netlify/src/allowed-remote-urls.ts diff --git a/packages/gatsby-adapter-netlify/.babelrc b/packages/gatsby-adapter-netlify/.babelrc index 84af48678d3f0..264f094569e08 100644 --- a/packages/gatsby-adapter-netlify/.babelrc +++ b/packages/gatsby-adapter-netlify/.babelrc @@ -3,7 +3,7 @@ [ "babel-preset-gatsby-package", { - "keepDynamicImports": ["./src/index.ts"] + "keepDynamicImports": ["./src/index.ts", "./src/allowed-remote-urls.ts"] } ] ] diff --git a/packages/gatsby-adapter-netlify/package.json b/packages/gatsby-adapter-netlify/package.json index 0dd2e1552ae59..f5a230377d993 100644 --- a/packages/gatsby-adapter-netlify/package.json +++ b/packages/gatsby-adapter-netlify/package.json @@ -33,6 +33,7 @@ "dependencies": { "@babel/runtime": "^7.20.13", "@netlify/cache-utils": "^5.1.5", + "@netlify/config": "^20.10.0", "@netlify/functions": "^1.6.0", "cookie": "^0.5.0", "fastq": "^1.15.0", diff --git a/packages/gatsby-adapter-netlify/src/allowed-remote-urls.ts b/packages/gatsby-adapter-netlify/src/allowed-remote-urls.ts new file mode 100644 index 0000000000000..4b7d950a75172 --- /dev/null +++ b/packages/gatsby-adapter-netlify/src/allowed-remote-urls.ts @@ -0,0 +1,24 @@ +export async function handleAllowedRemoteUrls( + remoteFileAllowedUrls: Array +): Promise { + const { resolveConfig } = await import(`@netlify/config`) + const cfg = await resolveConfig() + + const allowedUrlsInNetlifyToml: Array = + cfg?.config?.images?.remote_images ?? [] + + const missingAllowedUrlsInNetlifyToml: Array = [] + for (const remoteFileAllowedUrl of remoteFileAllowedUrls) { + if (!allowedUrlsInNetlifyToml.includes(remoteFileAllowedUrl)) { + missingAllowedUrlsInNetlifyToml.push(remoteFileAllowedUrl) + } + } + + console.log(`[dev log start] - will not be in merged code`) + console.log({ + from_netlify_toml: cfg.config.images.remote_images, + gatsby_generated: remoteFileAllowedUrls, + missing_in_netlify_toml: missingAllowedUrlsInNetlifyToml, + }) + console.log(`[dev log end] - will not be in merged code`) +} diff --git a/packages/gatsby-adapter-netlify/src/file-cdn-handler.ts b/packages/gatsby-adapter-netlify/src/file-cdn-handler.ts index cc8482c790c04..ce6ef19f2f98f 100644 --- a/packages/gatsby-adapter-netlify/src/file-cdn-handler.ts +++ b/packages/gatsby-adapter-netlify/src/file-cdn-handler.ts @@ -29,6 +29,7 @@ export async function prepareFileCdnHandler({ pathPrefix, }: { pathPrefix: string + remoteFileAllowedUrls: Array }): Promise { const functionId = `file-cdn` diff --git a/packages/gatsby-adapter-netlify/src/index.ts b/packages/gatsby-adapter-netlify/src/index.ts index 1b5b6a3ee263a..0e962c0b71937 100644 --- a/packages/gatsby-adapter-netlify/src/index.ts +++ b/packages/gatsby-adapter-netlify/src/index.ts @@ -4,6 +4,7 @@ import { prepareFunctionVariants } from "./lambda-handler" import { prepareFileCdnHandler } from "./file-cdn-handler" import { handleRoutesManifest } from "./route-handler" import packageJson from "gatsby-adapter-netlify/package.json" +import { handleAllowedRemoteUrls } from "./allowed-remote-urls" interface INetlifyCacheUtils { restore: (paths: Array) => Promise @@ -86,8 +87,10 @@ const createNetlifyAdapter: AdapterInit = options => { pathPrefix, remoteFileAllowedUrls, }): Promise { + await handleAllowedRemoteUrls(remoteFileAllowedUrls) + if (useNetlifyImageCDN) { - await prepareFileCdnHandler({ pathPrefix }) + await prepareFileCdnHandler({ pathPrefix, remoteFileAllowedUrls }) } const { lambdasThatUseCaching } = await handleRoutesManifest( @@ -102,8 +105,6 @@ const createNetlifyAdapter: AdapterInit = options => { lambdasThatUseCaching.get(fun.functionId) ) } - - console.log({ remoteFileAllowedUrls }) }, config: ({ reporter }): IAdapterConfig => { reporter.verbose( diff --git a/yarn.lock b/yarn.lock index edca74a7eaf04..9604f412e782f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3262,6 +3262,36 @@ path-exists "^5.0.0" readdirp "^3.4.0" +"@netlify/config@^20.10.0": + version "20.10.0" + resolved "https://registry.yarnpkg.com/@netlify/config/-/config-20.10.0.tgz#df51f277c7eaa984105f4e90c3ea676b935c5df3" + integrity sha512-7CNoL5IPSRBzDVzxuQgltZ71D/vZ/oYR29sfN8EXjAFOZPSLtnZgborcPa9V9BXLN4N5h0hFp2A26lnnCttEFg== + dependencies: + chalk "^5.0.0" + cron-parser "^4.1.0" + deepmerge "^4.2.2" + dot-prop "^7.0.0" + execa "^6.0.0" + fast-safe-stringify "^2.0.7" + figures "^5.0.0" + filter-obj "^5.0.0" + find-up "^6.0.0" + indent-string "^5.0.0" + is-plain-obj "^4.0.0" + js-yaml "^4.0.0" + map-obj "^5.0.0" + netlify "^13.1.11" + netlify-headers-parser "^7.1.2" + netlify-redirect-parser "^14.2.0" + node-fetch "^3.3.1" + omit.js "^2.0.2" + p-locate "^6.0.0" + path-type "^5.0.0" + toml "^3.0.0" + tomlify-j0.4 "^3.0.0" + validate-npm-package-name "^4.0.0" + yargs "^17.6.0" + "@netlify/edge-functions@^2.2.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@netlify/edge-functions/-/edge-functions-2.2.0.tgz#5f7f5c7602a7f98888a4b4421576ca609dad2083" @@ -3274,6 +3304,11 @@ dependencies: is-promise "^4.0.0" +"@netlify/open-api@^2.26.0": + version "2.26.0" + resolved "https://registry.yarnpkg.com/@netlify/open-api/-/open-api-2.26.0.tgz#cdc8033371079955501f4b9a5323f2dcd76741c2" + integrity sha512-B7q+ySzQm6rJhaGbY0Pzqnb1p3FsBqwiPLnLtA17JgTsqmXgQ7j6OQImW9fRJy/Al1ob9M6Oxng/FA2c7aIW1g== + "@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3": version "2.1.8-no-fsevents.3" resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz#323d72dd25103d0c4fbdce89dadf574a787b1f9b" @@ -7165,6 +7200,13 @@ builtins@^2.0.0: dependencies: semver "^6.0.0" +builtins@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/builtins/-/builtins-5.0.1.tgz#87f6db9ab0458be728564fa81d876d8d74552fa9" + integrity sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ== + dependencies: + semver "^7.0.0" + busboy@^1.0.0: version "1.6.0" resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" @@ -7531,6 +7573,11 @@ chalk@^4.0, chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^5.0.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" + integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== + change-case-all@1.0.14: version "1.0.14" resolved "https://registry.yarnpkg.com/change-case-all/-/change-case-all-1.0.14.tgz#bac04da08ad143278d0ac3dda7eccd39280bfba1" @@ -8765,6 +8812,13 @@ createerror@1.3.0, createerror@^1.2.0, createerror@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/createerror/-/createerror-1.3.0.tgz#c666bd4cd6b94e35415396569d4649dd0cdb3313" +cron-parser@^4.1.0: + version "4.9.0" + resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-4.9.0.tgz#0340694af3e46a0894978c6f52a6dbb5c0f11ad5" + integrity sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q== + dependencies: + luxon "^3.2.1" + cross-env@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" @@ -9288,6 +9342,11 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +data-uri-to-buffer@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e" + integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== + data-urls@^1.0.0, data-urls@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe" @@ -10028,6 +10087,13 @@ dot-prop@^5.1.0, dot-prop@^5.2.0: dependencies: is-obj "^2.0.0" +dot-prop@^7.0.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-7.2.0.tgz#468172a3529779814d21a779c1ba2f6d76609809" + integrity sha512-Ol/IPXUARn9CSbkrdV4VJo7uCy1I3VuSiWCaFSg+8BdUOzF9n3jefIpcgAydvUZbTdEBZs2vEiTiS9m61ssiDA== + dependencies: + type-fest "^2.11.2" + dotenv-expand@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0" @@ -10420,7 +10486,7 @@ escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" -escape-string-regexp@5.0.0: +escape-string-regexp@5.0.0, escape-string-regexp@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8" integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== @@ -11032,6 +11098,21 @@ execa@^5.0.0, execa@^5.1.1: signal-exit "^3.0.3" strip-final-newline "^2.0.0" +execa@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-6.1.0.tgz#cea16dee211ff011246556388effa0818394fb20" + integrity sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.1" + human-signals "^3.0.1" + is-stream "^3.0.0" + merge-stream "^2.0.0" + npm-run-path "^5.1.0" + onetime "^6.0.0" + signal-exit "^3.0.7" + strip-final-newline "^3.0.0" + execall@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/execall/-/execall-2.0.0.tgz#16a06b5fe5099df7d00be5d9c06eecded1663b45" @@ -11280,7 +11361,7 @@ fast-redact@^3.0.0: resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.1.2.tgz#d58e69e9084ce9fa4c1a6fa98a3e1ecf5d7839aa" integrity sha512-+0em+Iya9fKGfEQGcd62Yv6onjBmmhV1uh86XVfOU8VwAe6kaFdQCWI9s0/Nnugx5Vd9tdbZ7e6gE2tR9dzXdw== -fast-safe-stringify@2.1.1: +fast-safe-stringify@2.1.1, fast-safe-stringify@^2.0.7, fast-safe-stringify@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== @@ -11337,6 +11418,14 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" +fetch-blob@^3.1.2, fetch-blob@^3.1.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9" + integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ== + dependencies: + node-domexception "^1.0.0" + web-streams-polyfill "^3.0.3" + figgy-pudding@^3.4.1, figgy-pudding@^3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790" @@ -11361,6 +11450,14 @@ figures@^3.0.0, figures@^3.1.0, figures@^3.2.0: dependencies: escape-string-regexp "^1.0.5" +figures@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-5.0.0.tgz#126cd055052dea699f8a54e8c9450e6ecfc44d5f" + integrity sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg== + dependencies: + escape-string-regexp "^5.0.0" + is-unicode-supported "^1.2.0" + file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -11456,6 +11553,11 @@ filter-obj@^1.1.0: resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b" integrity sha1-mzERErxsYSehbgFsbF1/GeCAXFs= +filter-obj@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-5.1.0.tgz#5bd89676000a713d7db2e197f660274428e524ed" + integrity sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng== + finalhandler@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" @@ -11534,6 +11636,14 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" +find-up@^6.0.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-6.3.0.tgz#2abab3d3280b2dc7ac10199ef324c4e002c8c790" + integrity sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw== + dependencies: + locate-path "^7.1.0" + path-exists "^5.0.0" + find-yarn-workspace-root@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz#f47fb8d239c900eb78179aa81b66673eac88f7bd" @@ -11731,6 +11841,13 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" +formdata-polyfill@^4.0.10: + version "4.0.10" + resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" + integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== + dependencies: + fetch-blob "^3.1.2" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -13278,6 +13395,11 @@ human-signals@^2.1.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== +human-signals@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-3.0.1.tgz#c740920859dafa50e5a3222da9d3bf4bb0e5eef5" + integrity sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ== + humanize-ms@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" @@ -14209,6 +14331,11 @@ is-stream@^2.0.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== +is-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" + integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== + is-string@^1.0.5, is-string@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" @@ -14256,6 +14383,11 @@ is-unicode-supported@^0.1.0: resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== +is-unicode-supported@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz#d824984b616c292a2e198207d4a609983842f714" + integrity sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ== + is-upper-case@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/is-upper-case/-/is-upper-case-2.0.2.tgz#f1105ced1fe4de906a5f39553e7d3803fd804649" @@ -14911,7 +15043,7 @@ js-levenshtein@^1.1.6: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" -js-yaml@4.1.0, js-yaml@^4.1.0: +js-yaml@4.1.0, js-yaml@^4.0.0, js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== @@ -15641,7 +15773,7 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" -locate-path@^7.0.0: +locate-path@^7.0.0, locate-path@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-7.2.0.tgz#69cb1779bd90b35ab1e771e1f2f89a202c2a8a8a" integrity sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA== @@ -16035,6 +16167,11 @@ lru-queue@^0.1.0: dependencies: es5-ext "~0.10.2" +luxon@^3.2.1: + version "3.4.4" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.4.4.tgz#cf20dc27dc532ba41a169c43fdcc0063601577af" + integrity sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA== + lz-string@^1.4.4, lz-string@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" @@ -16127,6 +16264,11 @@ map-obj@^4.0.0, map-obj@^4.1.0: resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a" integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ== +map-obj@^5.0.0: + version "5.0.2" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-5.0.2.tgz#174ad9f7e5e4e777a219126d9a734ff3e14a1c68" + integrity sha512-K6K2NgKnTXimT3779/4KxSvobxOtMmx1LBZ3NwRxT/MDIR3Br/fQ4Q+WCX5QxjyUR8zg5+RV9Tbf2c5pAWTD2A== + map-stream@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194" @@ -16693,6 +16835,11 @@ methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" +micro-api-client@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/micro-api-client/-/micro-api-client-3.3.0.tgz#52dd567d322f10faffe63d19d4feeac4e4ffd215" + integrity sha512-y0y6CUB9RLVsy3kfgayU28746QrNMpSm9O/AYGNsBgOkJr/X/Jk0VLGoO8Ude7Bpa8adywzF+MzXNZRFRsNPhg== + microbundle@^0.15.0, microbundle@^0.15.1: version "0.15.1" resolved "https://registry.yarnpkg.com/microbundle/-/microbundle-0.15.1.tgz#3fa67128934b31736823b5c868dae4b92d94e766" @@ -17170,6 +17317,11 @@ mimic-fn@^3.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-3.1.0.tgz#65755145bbf3e36954b949c16450427451d5ca74" integrity sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ== +mimic-fn@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" + integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== + mimic-response@^1.0.0, mimic-response@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" @@ -17575,11 +17727,47 @@ nested-error-stacks@^2.0.0, nested-error-stacks@^2.1.0: resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz#0fbdcf3e13fe4994781280524f8b96b0cdff9c61" integrity sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug== +netlify-headers-parser@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/netlify-headers-parser/-/netlify-headers-parser-7.1.2.tgz#5b2f76e030eb8ba423c370c4e4814ddcc9c65e3b" + integrity sha512-DfoboA8PrcLXMan3jIVyLsQtKS+nepKDx6WwZKk5EQDMr2AJoBPCtSHTOLuABzkde1UXdOITf3snmcAmzlNLqw== + dependencies: + escape-string-regexp "^5.0.0" + fast-safe-stringify "^2.0.7" + is-plain-obj "^4.0.0" + map-obj "^5.0.0" + path-exists "^5.0.0" + toml "^3.0.0" + netlify-identity-widget@^1.9.2: version "1.9.2" resolved "https://registry.yarnpkg.com/netlify-identity-widget/-/netlify-identity-widget-1.9.2.tgz#4339c9155fc4c2570ae3ddd61825d952b574b02e" integrity sha512-IbS1JHhs7BflCCvp3C9f6tmNSZqbyBhZ4Gs5+Qxt4IlPybTOVv0PqJ4TAsA7uxh1R+oXOAmk0OOMAkEaPYeCtA== +netlify-redirect-parser@^14.2.0: + version "14.2.0" + resolved "https://registry.yarnpkg.com/netlify-redirect-parser/-/netlify-redirect-parser-14.2.0.tgz#8da1b911b43ea51e0c5fa5dd1401157d1301a8f5" + integrity sha512-3Mi7sMH7XXZhjvXx/5RtJ/rU/E6zKkE4etcYQbEu8B3r872D0opoYyGdPW/MvaYQyVdfg23XEFaEI4zzQTupaw== + dependencies: + fast-safe-stringify "^2.1.1" + filter-obj "^5.0.0" + is-plain-obj "^4.0.0" + path-exists "^5.0.0" + toml "^3.0.0" + +netlify@^13.1.11: + version "13.1.11" + resolved "https://registry.yarnpkg.com/netlify/-/netlify-13.1.11.tgz#f5151bbd5e05cd5a67713f89c05a57dd6377b599" + integrity sha512-exrD6cqwo5avDNtU7YT9iuN0+yoW+aaEUxvr/39oP36wZRKreoPm6KNVisTR9d4lXrPeUG8XI+rERuLhwMQmdw== + dependencies: + "@netlify/open-api" "^2.26.0" + lodash-es "^4.17.21" + micro-api-client "^3.3.0" + node-fetch "^3.0.0" + omit.js "^2.0.2" + p-wait-for "^4.0.0" + qs "^6.9.6" + next-tick@1, next-tick@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" @@ -17654,6 +17842,11 @@ node-dir@^0.1.10, node-dir@^0.1.17: dependencies: minimatch "^3.0.2" +node-domexception@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" + integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== + node-environment-flags@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/node-environment-flags/-/node-environment-flags-1.0.6.tgz#a30ac13621f6f7d674260a54dede048c3982c088" @@ -17692,6 +17885,15 @@ node-fetch@^2.5.0, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.11, nod dependencies: whatwg-url "^5.0.0" +node-fetch@^3.0.0, node-fetch@^3.3.1: + version "3.3.2" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.2.tgz#d1e889bacdf733b4ff3b2b243eb7a12866a0b78b" + integrity sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA== + dependencies: + data-uri-to-buffer "^4.0.0" + fetch-blob "^3.1.4" + formdata-polyfill "^4.0.10" + node-gyp-build-optional-packages@5.0.3: version "5.0.3" resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.3.tgz#92a89d400352c44ad3975010368072b41ad66c17" @@ -17934,6 +18136,13 @@ npm-run-path@^4.0.0, npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" +npm-run-path@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.1.0.tgz#bc62f7f3f6952d9894bd08944ba011a6ee7b7e00" + integrity sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q== + dependencies: + path-key "^4.0.0" + npmlog@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" @@ -18096,6 +18305,11 @@ octokit-pagination-methods@^1.1.0: resolved "https://registry.yarnpkg.com/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz#cf472edc9d551055f9ef73f6e42b4dbb4c80bea4" integrity sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ== +omit.js@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/omit.js/-/omit.js-2.0.2.tgz#dd9b8436fab947a5f3ff214cb2538631e313ec2f" + integrity sha512-hJmu9D+bNB40YpL9jYebQl4lsTW6yEHRTroJzNLqQJYHm7c+NQnJGfZmIWh8S3q3KoaxV1aLhV6B3+0N0/kyJg== + on-exit-leak-free@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-0.2.0.tgz#b39c9e3bf7690d890f4861558b0d7b90a442d209" @@ -18132,6 +18346,13 @@ onetime@^5.1.0, onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" +onetime@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-6.0.0.tgz#7c24c18ed1fd2e9bca4bd26806a33613c77d34b4" + integrity sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ== + dependencies: + mimic-fn "^4.0.0" + open@^7.0.3: version "7.3.1" resolved "https://registry.yarnpkg.com/open/-/open-7.3.1.tgz#111119cb919ca1acd988f49685c4fdd0f4755356" @@ -18442,6 +18663,11 @@ p-timeout@^3.1.0, p-timeout@^3.2.0: dependencies: p-finally "^1.0.0" +p-timeout@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-5.1.0.tgz#b3c691cf4415138ce2d9cfe071dba11f0fee085b" + integrity sha512-auFDyzzzGZZZdHz3BtET9VEz0SE/uMEAx7uWfGPucfzEwwe/xH0iVeZibQmANYE/hp9T2+UUZT5m+BKyrDp3Ew== + p-try@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" @@ -18451,6 +18677,13 @@ p-try@^2.0.0, p-try@^2.1.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +p-wait-for@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-wait-for/-/p-wait-for-4.1.0.tgz#290f126f49bbd7c84e0cedccb342cd631aaa0f16" + integrity sha512-i8nE5q++9h8oaQHWltS1Tnnv4IoMDOlqN7C0KFG2OdbK0iFJIt6CROZ8wfBM+K4Pxqfnq4C4lkkpXqTEpB5DZw== + dependencies: + p-timeout "^5.0.0" + p-waterfall@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-waterfall/-/p-waterfall-1.0.0.tgz#7ed94b3ceb3332782353af6aae11aa9fc235bb00" @@ -18773,6 +19006,11 @@ path-key@^3.0.0, path-key@^3.1.0: resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== +path-key@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-4.0.0.tgz#295588dc3aee64154f877adb9d780b81c554bf18" + integrity sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ== + path-parse@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" @@ -18830,6 +19068,11 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +path-type@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-5.0.0.tgz#14b01ed7aea7ddf9c7c3f46181d4d04f9c785bb8" + integrity sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg== + pause-stream@0.0.11: version "0.0.11" resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" @@ -20109,7 +20352,7 @@ qs@6.11.0: dependencies: side-channel "^1.0.4" -qs@^6.1.0, qs@^6.11.0, qs@^6.4.0, qs@^6.5.2, qs@^6.9.4: +qs@^6.1.0, qs@^6.11.0, qs@^6.4.0, qs@^6.5.2, qs@^6.9.4, qs@^6.9.6: version "6.11.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA== @@ -21749,6 +21992,13 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^7.0.0, semver@^7.5.4: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== + dependencies: + lru-cache "^6.0.0" + semver@^7.1.1, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3: version "7.5.3" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.3.tgz#161ce8c2c6b4b3bdca6caadc9fa3317a4c4fe88e" @@ -21756,13 +22006,6 @@ semver@^7.1.1, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semve dependencies: lru-cache "^6.0.0" -semver@^7.5.4: - version "7.5.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" - integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== - dependencies: - lru-cache "^6.0.0" - send@0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" @@ -22768,6 +23011,11 @@ strip-final-newline@^2.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== +strip-final-newline@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd" + integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== + strip-indent@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" @@ -23544,6 +23792,16 @@ toml@^2.3.2, toml@^2.3.6: resolved "https://registry.yarnpkg.com/toml/-/toml-2.3.6.tgz#25b0866483a9722474895559088b436fd11f861b" integrity sha512-gVweAectJU3ebq//Ferr2JUY4WKSDe5N+z0FvjDncLGyHmIDoxgY/2Ie4qfEIDm4IS7OA6Rmdm7pdEEdMcV/xQ== +toml@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/toml/-/toml-3.0.0.tgz#342160f1af1904ec9d204d03a5d61222d762c5ee" + integrity sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w== + +tomlify-j0.4@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/tomlify-j0.4/-/tomlify-j0.4-3.0.0.tgz#99414d45268c3a3b8bf38be82145b7bba34b7473" + integrity sha512-2Ulkc8T7mXJ2l0W476YC/A209PR38Nw8PuaCNtk9uI3t1zzFdGQeWYGQvmj2PZkVvRC/Yoi4xQKMRnWc/N29tQ== + topo@3.x.x: version "3.0.3" resolved "https://registry.yarnpkg.com/topo/-/topo-3.0.3.tgz#d5a67fb2e69307ebeeb08402ec2a2a6f5f7ad95c" @@ -23801,7 +24059,7 @@ type-fest@^1.0.1, type-fest@^1.2.1, type-fest@^1.2.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== -type-fest@^2.19.0: +type-fest@^2.11.2, type-fest@^2.19.0: version "2.19.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== @@ -24579,6 +24837,13 @@ validate-npm-package-name@^3.0.0: dependencies: builtins "^1.0.3" +validate-npm-package-name@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-4.0.0.tgz#fe8f1c50ac20afdb86f177da85b3600f0ac0d747" + integrity sha512-mzR0L8ZDktZjpX4OB46KT+56MAhl4EIazWP/+G/HPGuvfdaqg4YsCdtOm6U9+LOFyYDoh4dpnpxZRB9MQQns5Q== + dependencies: + builtins "^5.0.0" + validator@13.9.0: version "13.9.0" resolved "https://registry.yarnpkg.com/validator/-/validator-13.9.0.tgz#33e7b85b604f3bbce9bb1a05d5c3e22e1c2ff855" @@ -24891,6 +25156,11 @@ web-namespaces@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-1.1.2.tgz#c8dc267ab639505276bae19e129dbd6ae72b22b4" +web-streams-polyfill@^3.0.3: + version "3.2.1" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" + integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q== + web-vitals@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-1.1.2.tgz#06535308168986096239aa84716e68b4c6ae6d1c" @@ -25665,6 +25935,19 @@ yargs@^17.2.1, yargs@^17.3.1: y18n "^5.0.5" yargs-parser "^21.1.1" +yargs@^17.6.0: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + yauzl@^2.10.0: version "2.10.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" From 1de02f9b098e1f5c23ee60a0a0c9ba1c019c0beb Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Thu, 7 Dec 2023 13:58:20 +0100 Subject: [PATCH 09/12] url testing in filecdn --- .../adapters/cypress/e2e/remote-file.cy.ts | 18 +++++++++++---- e2e-tests/adapters/gatsby-node.ts | 22 ++++++++++++++++++- .../adapters/src/pages/routes/remote-file.jsx | 7 +++++- .../src/pages/routes/ssr/remote-file.jsx | 7 +++++- .../templates/remote-file-from-context.jsx | 6 ++++- .../src/allowed-remote-urls.ts | 22 +++++++++++-------- .../src/file-cdn-handler.ts | 18 +++++++-------- .../src/file-cdn-url-generator.ts | 2 +- packages/gatsby-adapter-netlify/src/index.ts | 7 ++++-- packages/gatsby/package.json | 1 + packages/gatsby/src/utils/adapter/manager.ts | 13 ++++++++--- packages/gatsby/src/utils/adapter/types.ts | 7 +++++- 12 files changed, 97 insertions(+), 33 deletions(-) diff --git a/e2e-tests/adapters/cypress/e2e/remote-file.cy.ts b/e2e-tests/adapters/cypress/e2e/remote-file.cy.ts index 81b3acb000e2c..5f168eab76c80 100644 --- a/e2e-tests/adapters/cypress/e2e/remote-file.cy.ts +++ b/e2e-tests/adapters/cypress/e2e/remote-file.cy.ts @@ -222,12 +222,17 @@ for (const config of configs) { it(`File CDN`, () => { cy.get('[data-testid="file-public"]').then(async $urls => { - const urls = Array.from( - $urls.map((_, $url) => $url.getAttribute("href")) + 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 of urls) { + for (const { urlWithoutOrigin, allowed } of fileCdnFixtures) { // using Netlify Image CDN expect(urlWithoutOrigin).to.match( new RegExp(`^${PATH_PREFIX}/_gatsby/file`) @@ -235,7 +240,12 @@ for (const config of configs) { const res = await fetch(urlWithoutOrigin, { method: "HEAD", }) - expect(res.ok).to.be.true + if (allowed) { + expect(res.ok).to.be.true + } else { + expect(res.ok).to.be.false + expect(res.status).to.be.equal(500) + } } }) }) diff --git a/e2e-tests/adapters/gatsby-node.ts b/e2e-tests/adapters/gatsby-node.ts index e054bf7e56693..9342bb30fa191 100644 --- a/e2e-tests/adapters/gatsby-node.ts +++ b/e2e-tests/adapters/gatsby-node.ts @@ -47,6 +47,7 @@ export const createPages: GatsbyNode["createPages"] = async ({ url filename publicUrl + isAllowed } } } @@ -138,7 +139,9 @@ export const createSchemaCustomization: GatsbyNode["createSchemaCustomization"] addRemoteFilePolyfillInterface( schema.buildObjectType({ name: "MyRemoteFile", - fields: {}, + fields: { + isAllowed: `String!`, + }, interfaces: ["Node", "RemoteFile"], }), { @@ -148,6 +151,13 @@ export const createSchemaCustomization: GatsbyNode["createSchemaCustomization"] } ) ) + + if (typeof actions.addRemoteFileAllowedUrl === `function`) { + actions.addRemoteFileAllowedUrl([ + `https://images.unsplash.com/*`, + `https://www.gatsbyjs.com/*`, + ]) + } } export const sourceNodes: GatsbyNode["sourceNodes"] = function sourceNodes({ @@ -191,6 +201,16 @@ export const sourceNodes: GatsbyNode["sourceNodes"] = function sourceNodes({ mimeType: "image/svg+xml", filename: "Gatsby-Logo.svg", type: `MyRemoteFile`, + isAllowed: true, + }, + { + // svg is not considered for image cdn - file cdn will be used + name: "fileB.svg", + url: "https://www.not-allowed.com/not-allowed.svg", + mimeType: "image/svg+xml", + filename: "Gatsby-Logo.svg", + type: `MyRemoteFile`, + isAllowed: false, }, ] diff --git a/e2e-tests/adapters/src/pages/routes/remote-file.jsx b/e2e-tests/adapters/src/pages/routes/remote-file.jsx index c93c3f7eff70b..8f2ceba8a1756 100644 --- a/e2e-tests/adapters/src/pages/routes/remote-file.jsx +++ b/e2e-tests/adapters/src/pages/routes/remote-file.jsx @@ -43,7 +43,11 @@ const RemoteFile = ({ data }) => { return (

- + {node.filename}

@@ -91,6 +95,7 @@ export const pageQuery = graphql` url filename publicUrl + isAllowed } } } diff --git a/e2e-tests/adapters/src/pages/routes/ssr/remote-file.jsx b/e2e-tests/adapters/src/pages/routes/ssr/remote-file.jsx index 923831a179b6d..87477e2ea3b9c 100644 --- a/e2e-tests/adapters/src/pages/routes/ssr/remote-file.jsx +++ b/e2e-tests/adapters/src/pages/routes/ssr/remote-file.jsx @@ -43,7 +43,11 @@ const RemoteFile = ({ data }) => { return (

- + {node.filename}

@@ -98,6 +102,7 @@ export const pageQuery = graphql` url filename publicUrl + isAllowed } } } diff --git a/e2e-tests/adapters/src/templates/remote-file-from-context.jsx b/e2e-tests/adapters/src/templates/remote-file-from-context.jsx index d6532903dce06..4773a88beb1cb 100644 --- a/e2e-tests/adapters/src/templates/remote-file-from-context.jsx +++ b/e2e-tests/adapters/src/templates/remote-file-from-context.jsx @@ -42,7 +42,11 @@ const RemoteFile = ({ pageContext: data }) => { return (

- + {node.filename}

diff --git a/packages/gatsby-adapter-netlify/src/allowed-remote-urls.ts b/packages/gatsby-adapter-netlify/src/allowed-remote-urls.ts index 4b7d950a75172..0202029b2eec1 100644 --- a/packages/gatsby-adapter-netlify/src/allowed-remote-urls.ts +++ b/packages/gatsby-adapter-netlify/src/allowed-remote-urls.ts @@ -1,5 +1,5 @@ export async function handleAllowedRemoteUrls( - remoteFileAllowedUrls: Array + remoteFileAllowedUrlRegexes: Array ): Promise { const { resolveConfig } = await import(`@netlify/config`) const cfg = await resolveConfig() @@ -8,17 +8,21 @@ export async function handleAllowedRemoteUrls( cfg?.config?.images?.remote_images ?? [] const missingAllowedUrlsInNetlifyToml: Array = [] - for (const remoteFileAllowedUrl of remoteFileAllowedUrls) { + for (const remoteFileAllowedUrl of remoteFileAllowedUrlRegexes) { if (!allowedUrlsInNetlifyToml.includes(remoteFileAllowedUrl)) { missingAllowedUrlsInNetlifyToml.push(remoteFileAllowedUrl) } } - console.log(`[dev log start] - will not be in merged code`) - console.log({ - from_netlify_toml: cfg.config.images.remote_images, - gatsby_generated: remoteFileAllowedUrls, - missing_in_netlify_toml: missingAllowedUrlsInNetlifyToml, - }) - console.log(`[dev log end] - will not be in merged code`) + // console.log(`[dev log start] - will not be in merged code`) + // console.log({ + // from_netlify_toml: cfg.config.images.remote_images, + // gatsby_generated: remoteFileAllowedUrlRegexes, + // missing_in_netlify_toml: missingAllowedUrlsInNetlifyToml, + // }) + // console.log(`[dev log end] - will not be in merged code`) + + // for (const s of missingAllowedUrlsInNetlifyToml) { + // console.log(`needed: ` + s) + // } } diff --git a/packages/gatsby-adapter-netlify/src/file-cdn-handler.ts b/packages/gatsby-adapter-netlify/src/file-cdn-handler.ts index ce6ef19f2f98f..a6ba815172b46 100644 --- a/packages/gatsby-adapter-netlify/src/file-cdn-handler.ts +++ b/packages/gatsby-adapter-netlify/src/file-cdn-handler.ts @@ -27,9 +27,10 @@ export interface IFunctionManifest { export async function prepareFileCdnHandler({ pathPrefix, + remoteFileAllowedUrlRegexes, }: { pathPrefix: string - remoteFileAllowedUrls: Array + remoteFileAllowedUrlRegexes: Array }): Promise { const functionId = `file-cdn` @@ -49,21 +50,20 @@ export async function prepareFileCdnHandler({ ) const handlerSource = /* typescript */ ` - import type { Context } from "@netlify/edge-functions" + const allowedUrlPatterns = [${remoteFileAllowedUrlRegexes.map( + regexSource => `new RegExp(\`${regexSource}\`)` + )}] - export default async (req: Request, context: Context): Promise => { + export default async (req: Request): Promise => { const url = new URL(req.url) const remoteUrl = url.searchParams.get("url") - - // @todo: use allowed remote urls to decide wether request should be allowed - // blocked by https://github.com/gatsbyjs/gatsby/pull/38719 - const isAllowed = true + + const isAllowed = allowedUrlPatterns.some(allowedUrlPattern => allowedUrlPattern.test(remoteUrl)) if (isAllowed) { - console.log(\`URL allowed\`, { remoteUrl }) return fetch(remoteUrl); } else { console.error(\`URL not allowed: \${remoteUrl}\`) - return new Response("Not allowed", { status: 403 }) + return new Response("Bad request", { status: 500 }) } } ` diff --git a/packages/gatsby-adapter-netlify/src/file-cdn-url-generator.ts b/packages/gatsby-adapter-netlify/src/file-cdn-url-generator.ts index 1bc3e37abfe64..f24efd9c8281c 100644 --- a/packages/gatsby-adapter-netlify/src/file-cdn-url-generator.ts +++ b/packages/gatsby-adapter-netlify/src/file-cdn-url-generator.ts @@ -29,8 +29,8 @@ export const generateFileUrl: FileCdnUrlGeneratorFn = function generateFileUrl( baseURL.searchParams.append(`url`, source.url) baseURL.searchParams.append(`cd`, source.internal.contentDigest) - return `${baseURL.pathname}${baseURL.search}` } + return `${baseURL.pathname}${baseURL.search}` } diff --git a/packages/gatsby-adapter-netlify/src/index.ts b/packages/gatsby-adapter-netlify/src/index.ts index 0e962c0b71937..4f0f1e79803b4 100644 --- a/packages/gatsby-adapter-netlify/src/index.ts +++ b/packages/gatsby-adapter-netlify/src/index.ts @@ -87,10 +87,13 @@ const createNetlifyAdapter: AdapterInit = options => { pathPrefix, remoteFileAllowedUrls, }): Promise { - await handleAllowedRemoteUrls(remoteFileAllowedUrls) + await handleAllowedRemoteUrls(remoteFileAllowedUrls.regexes) if (useNetlifyImageCDN) { - await prepareFileCdnHandler({ pathPrefix, remoteFileAllowedUrls }) + await prepareFileCdnHandler({ + pathPrefix, + remoteFileAllowedUrlRegexes: remoteFileAllowedUrls.regexes, + }) } const { lambdasThatUseCaching } = await handleRoutesManifest( diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index 617962f238521..725aeaca54084 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -137,6 +137,7 @@ "opentracing": "^0.14.7", "p-defer": "^3.0.0", "parseurl": "^1.3.3", + "path-to-regexp": "0.1.7", "physical-cpu-count": "^2.0.0", "platform": "^1.3.6", "postcss": "^8.4.24", diff --git a/packages/gatsby/src/utils/adapter/manager.ts b/packages/gatsby/src/utils/adapter/manager.ts index bfca83f836482..bfdbdbb3a99bb 100644 --- a/packages/gatsby/src/utils/adapter/manager.ts +++ b/packages/gatsby/src/utils/adapter/manager.ts @@ -7,6 +7,7 @@ import { posix } from "path" import { sync as globSync } from "glob" import telemetry from "gatsby-telemetry" import { copy, pathExists, unlink } from "fs-extra" +import pathToRegexp from "path-to-regexp" import type { FunctionsManifest, IAdaptContext, @@ -18,6 +19,7 @@ import type { IAdapterFinalConfig, IAdapterConfig, HeaderRoutes, + IRemoteFileAllowedUrls, } from "./types" import { store, readState } from "../../redux" import { getPageMode } from "../page-mode" @@ -204,7 +206,7 @@ export async function initAdapterManager(): Promise { let _routesManifest: RoutesManifest | undefined = undefined let _functionsManifest: FunctionsManifest | undefined = undefined let _headerRoutes: HeaderRoutes | undefined = undefined - let _imageCdnAllowedUrls: Array | undefined = undefined + let _imageCdnAllowedUrls: IRemoteFileAllowedUrls | undefined = undefined const adaptContext: IAdaptContext = { get routesManifest(): RoutesManifest { if (!_routesManifest) { @@ -231,11 +233,16 @@ export async function initAdapterManager(): Promise { return _headerRoutes }, - get remoteFileAllowedUrls(): Array { + get remoteFileAllowedUrls(): IRemoteFileAllowedUrls { if (!_imageCdnAllowedUrls) { - _imageCdnAllowedUrls = Array.from( + const urlPatterns = Array.from( store.getState().remoteFileAllowedUrls ) + + _imageCdnAllowedUrls = { + urlPatterns, + regexes: urlPatterns.map(pattern => pathToRegexp(pattern).source), + } } return _imageCdnAllowedUrls diff --git a/packages/gatsby/src/utils/adapter/types.ts b/packages/gatsby/src/utils/adapter/types.ts index dc0e23aa70536..7655ea3a8c299 100644 --- a/packages/gatsby/src/utils/adapter/types.ts +++ b/packages/gatsby/src/utils/adapter/types.ts @@ -117,6 +117,11 @@ interface IDefaultContext { reporter: typeof reporter } +export interface IRemoteFileAllowedUrls { + urlPatterns: Array + regexes: Array +} + export interface IAdaptContext extends IDefaultContext { routesManifest: RoutesManifest functionsManifest: FunctionsManifest @@ -132,7 +137,7 @@ export interface IAdaptContext extends IDefaultContext { /** * @todo */ - remoteFileAllowedUrls: Array + remoteFileAllowedUrls: IRemoteFileAllowedUrls } export interface ICacheContext extends IDefaultContext { From eb77cd8feb39d62cb2ee91262b27ec2d90ec95ae Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Thu, 7 Dec 2023 15:24:04 +0100 Subject: [PATCH 10/12] jsdocs --- packages/gatsby/src/redux/actions/restricted.ts | 12 ++++++++++-- packages/gatsby/src/utils/adapter/types.ts | 10 +++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/gatsby/src/redux/actions/restricted.ts b/packages/gatsby/src/redux/actions/restricted.ts index 18ad234a020ea..0764b763f8010 100644 --- a/packages/gatsby/src/redux/actions/restricted.ts +++ b/packages/gatsby/src/redux/actions/restricted.ts @@ -536,7 +536,16 @@ export const actions = { } }, /** - * @todo + * Declares URL Pattern that should be allowed to be used for Image or File CDN to prevent using of unexpected RemoteFile URLs. + * + * @availableIn [onPreInit, onPreBootstrap, onPluginInit, createSchemaCustomization] + * + * @param {string | string []} url URLPattern or Array of URL Patternss that should be allowed. + * URLPattern is a string that can contain wildcards (*) or parameter placeholders (e.g. :id). + * @example + * exports.onPreInit = ({ actions }) => { + * actions.addRemoteFileAllowedUrl(`https://your-wordpress-instance.com/*`) + * } */ addRemoteFileAllowedUrl: ( url: string | Array, @@ -680,7 +689,6 @@ export const availableActionsByAPI = mapAvailableActionsToAPIs({ `onPreBootstrap`, `onPluginInit`, `createSchemaCustomization`, - `sourceNodes`, ], }, }) diff --git a/packages/gatsby/src/utils/adapter/types.ts b/packages/gatsby/src/utils/adapter/types.ts index 7655ea3a8c299..56cd696f5c89c 100644 --- a/packages/gatsby/src/utils/adapter/types.ts +++ b/packages/gatsby/src/utils/adapter/types.ts @@ -118,7 +118,13 @@ interface IDefaultContext { } export interface IRemoteFileAllowedUrls { + /** + * List of allowed remote image URLs using URLPattern format. In particular it uses wildcard `*` and param `:param` syntax. + */ urlPatterns: Array + /** + * List of allowed remote image URLs using Regex syntax. + */ regexes: Array } @@ -135,7 +141,9 @@ export interface IAdaptContext extends IDefaultContext { */ trailingSlash: TrailingSlash /** - * @todo + * List of allowed remote file URLs represented in URLPattern and Regex formats. + * Allowed urls are provided by user or plugins using `addRemoteFileAllowedUrl` action. + * @see https://www.gatsbyjs.com/docs/reference/config-files/actions/#addRemoteFileAllowedUrl */ remoteFileAllowedUrls: IRemoteFileAllowedUrls } From f01e427568377533548baef29b0ef8fa9f1462ae Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Fri, 8 Dec 2023 09:59:20 +0100 Subject: [PATCH 11/12] print warnings for netlify.toml about missing remote_images patterns --- .../src/allowed-remote-urls.ts | 55 ++++++++++++------- packages/gatsby-adapter-netlify/src/index.ts | 8 ++- 2 files changed, 42 insertions(+), 21 deletions(-) diff --git a/packages/gatsby-adapter-netlify/src/allowed-remote-urls.ts b/packages/gatsby-adapter-netlify/src/allowed-remote-urls.ts index 0202029b2eec1..222ddc2e760f3 100644 --- a/packages/gatsby-adapter-netlify/src/allowed-remote-urls.ts +++ b/packages/gatsby-adapter-netlify/src/allowed-remote-urls.ts @@ -1,28 +1,45 @@ -export async function handleAllowedRemoteUrls( +import type { Reporter } from "gatsby" + +export async function handleAllowedRemoteUrls({ + remoteFileAllowedUrlRegexes, + reporter, +}: { remoteFileAllowedUrlRegexes: Array -): Promise { + reporter: Reporter +}): Promise { const { resolveConfig } = await import(`@netlify/config`) const cfg = await resolveConfig() - const allowedUrlsInNetlifyToml: Array = - cfg?.config?.images?.remote_images ?? [] + if (cfg?.config) { + const allowedUrlsInNetlifyToml: Array = + cfg.config.images?.remote_images ?? [] - const missingAllowedUrlsInNetlifyToml: Array = [] - for (const remoteFileAllowedUrl of remoteFileAllowedUrlRegexes) { - if (!allowedUrlsInNetlifyToml.includes(remoteFileAllowedUrl)) { - missingAllowedUrlsInNetlifyToml.push(remoteFileAllowedUrl) + const missingAllowedUrlsInNetlifyToml: Array = [] + for (const remoteFileAllowedUrl of remoteFileAllowedUrlRegexes) { + if (!allowedUrlsInNetlifyToml.includes(remoteFileAllowedUrl)) { + missingAllowedUrlsInNetlifyToml.push(remoteFileAllowedUrl) + } } - } - // console.log(`[dev log start] - will not be in merged code`) - // console.log({ - // from_netlify_toml: cfg.config.images.remote_images, - // gatsby_generated: remoteFileAllowedUrlRegexes, - // missing_in_netlify_toml: missingAllowedUrlsInNetlifyToml, - // }) - // console.log(`[dev log end] - will not be in merged code`) + if (missingAllowedUrlsInNetlifyToml.length > 0) { + const entriesToAddToToml = `${missingAllowedUrlsInNetlifyToml + .map( + missingAllowedUrlInNetlifyToml => + ` ${JSON.stringify(missingAllowedUrlInNetlifyToml)}` + ) + .join(`,\n`)},\n` - // for (const s of missingAllowedUrlsInNetlifyToml) { - // console.log(`needed: ` + s) - // } + if (typeof cfg.config.images?.remote_images === `undefined`) { + reporter.warn( + `Missing allowed URLs in your Netlify configuration. Add following to your netlify.toml:\n\`\`\`toml\n[images]\nremote_images = [\n${entriesToAddToToml}]\n\`\`\`` + ) + } else { + reporter.warn( + `Missing allowed URLs in your Netlify configuration. Add following entries to your existing \`images.remote_images\` configuration in netlify.toml:\n\`\`\`toml\n${entriesToAddToToml}\`\`\`` + ) + } + } + } else { + reporter.verbose(`[gatsby-adapter-netlify] no netlify.toml found`) + } } diff --git a/packages/gatsby-adapter-netlify/src/index.ts b/packages/gatsby-adapter-netlify/src/index.ts index 4f0f1e79803b4..8ed26e81a0f7b 100644 --- a/packages/gatsby-adapter-netlify/src/index.ts +++ b/packages/gatsby-adapter-netlify/src/index.ts @@ -86,10 +86,14 @@ const createNetlifyAdapter: AdapterInit = options => { headerRoutes, pathPrefix, remoteFileAllowedUrls, + reporter, }): Promise { - await handleAllowedRemoteUrls(remoteFileAllowedUrls.regexes) - if (useNetlifyImageCDN) { + await handleAllowedRemoteUrls({ + remoteFileAllowedUrlRegexes: remoteFileAllowedUrls.regexes, + reporter, + }) + await prepareFileCdnHandler({ pathPrefix, remoteFileAllowedUrlRegexes: remoteFileAllowedUrls.regexes, From f868a5e5f6eb6ff77fcda2469456a36a5c81ccfb Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Fri, 8 Dec 2023 10:21:35 +0100 Subject: [PATCH 12/12] test if any existing pattern in netlify.toml allow needed remote url instead of just string comparison --- .../src/allowed-remote-urls.ts | 23 +++++++++++++------ .../src/file-cdn-handler.ts | 10 ++++---- packages/gatsby-adapter-netlify/src/index.ts | 8 +++---- packages/gatsby/index.d.ts | 1 + packages/gatsby/src/utils/adapter/manager.ts | 20 ++++++++-------- packages/gatsby/src/utils/adapter/types.ts | 14 +++++------ 6 files changed, 44 insertions(+), 32 deletions(-) diff --git a/packages/gatsby-adapter-netlify/src/allowed-remote-urls.ts b/packages/gatsby-adapter-netlify/src/allowed-remote-urls.ts index 222ddc2e760f3..daddb94d64a3d 100644 --- a/packages/gatsby-adapter-netlify/src/allowed-remote-urls.ts +++ b/packages/gatsby-adapter-netlify/src/allowed-remote-urls.ts @@ -1,10 +1,10 @@ -import type { Reporter } from "gatsby" +import type { Reporter, RemoteFileAllowedUrls } from "gatsby" -export async function handleAllowedRemoteUrls({ - remoteFileAllowedUrlRegexes, +export async function handleAllowedRemoteUrlsNetlifyConfig({ + remoteFileAllowedUrls, reporter, }: { - remoteFileAllowedUrlRegexes: Array + remoteFileAllowedUrls: RemoteFileAllowedUrls reporter: Reporter }): Promise { const { resolveConfig } = await import(`@netlify/config`) @@ -14,10 +14,19 @@ export async function handleAllowedRemoteUrls({ const allowedUrlsInNetlifyToml: Array = cfg.config.images?.remote_images ?? [] + const allowedUrlsInNetlifyTomlRegexes = allowedUrlsInNetlifyToml.map( + regexSource => new RegExp(regexSource) + ) + const missingAllowedUrlsInNetlifyToml: Array = [] - for (const remoteFileAllowedUrl of remoteFileAllowedUrlRegexes) { - if (!allowedUrlsInNetlifyToml.includes(remoteFileAllowedUrl)) { - missingAllowedUrlsInNetlifyToml.push(remoteFileAllowedUrl) + for (const remoteFileAllowedUrl of remoteFileAllowedUrls) { + // test if url pattern already passes one of the regexes in netlify.toml + const isAlreadyAllowed = allowedUrlsInNetlifyTomlRegexes.some( + allowedRegex => allowedRegex.test(remoteFileAllowedUrl.urlPattern) + ) + + if (!isAlreadyAllowed) { + missingAllowedUrlsInNetlifyToml.push(remoteFileAllowedUrl.regexSource) } } diff --git a/packages/gatsby-adapter-netlify/src/file-cdn-handler.ts b/packages/gatsby-adapter-netlify/src/file-cdn-handler.ts index a6ba815172b46..39f26c18c3023 100644 --- a/packages/gatsby-adapter-netlify/src/file-cdn-handler.ts +++ b/packages/gatsby-adapter-netlify/src/file-cdn-handler.ts @@ -3,6 +3,8 @@ import * as path from "path" import packageJson from "gatsby-adapter-netlify/package.json" +import type { RemoteFileAllowedUrls } from "gatsby" + export interface IFunctionManifest { version: 1 functions: Array< @@ -27,10 +29,10 @@ export interface IFunctionManifest { export async function prepareFileCdnHandler({ pathPrefix, - remoteFileAllowedUrlRegexes, + remoteFileAllowedUrls, }: { pathPrefix: string - remoteFileAllowedUrlRegexes: Array + remoteFileAllowedUrls: RemoteFileAllowedUrls }): Promise { const functionId = `file-cdn` @@ -50,8 +52,8 @@ export async function prepareFileCdnHandler({ ) const handlerSource = /* typescript */ ` - const allowedUrlPatterns = [${remoteFileAllowedUrlRegexes.map( - regexSource => `new RegExp(\`${regexSource}\`)` + const allowedUrlPatterns = [${remoteFileAllowedUrls.map( + allowedUrl => `new RegExp(\`${allowedUrl.regexSource}\`)` )}] export default async (req: Request): Promise => { diff --git a/packages/gatsby-adapter-netlify/src/index.ts b/packages/gatsby-adapter-netlify/src/index.ts index 8ed26e81a0f7b..b34fafbb8718a 100644 --- a/packages/gatsby-adapter-netlify/src/index.ts +++ b/packages/gatsby-adapter-netlify/src/index.ts @@ -4,7 +4,7 @@ import { prepareFunctionVariants } from "./lambda-handler" import { prepareFileCdnHandler } from "./file-cdn-handler" import { handleRoutesManifest } from "./route-handler" import packageJson from "gatsby-adapter-netlify/package.json" -import { handleAllowedRemoteUrls } from "./allowed-remote-urls" +import { handleAllowedRemoteUrlsNetlifyConfig } from "./allowed-remote-urls" interface INetlifyCacheUtils { restore: (paths: Array) => Promise @@ -89,14 +89,14 @@ const createNetlifyAdapter: AdapterInit = options => { reporter, }): Promise { if (useNetlifyImageCDN) { - await handleAllowedRemoteUrls({ - remoteFileAllowedUrlRegexes: remoteFileAllowedUrls.regexes, + await handleAllowedRemoteUrlsNetlifyConfig({ + remoteFileAllowedUrls, reporter, }) await prepareFileCdnHandler({ pathPrefix, - remoteFileAllowedUrlRegexes: remoteFileAllowedUrls.regexes, + remoteFileAllowedUrls, }) } diff --git a/packages/gatsby/index.d.ts b/packages/gatsby/index.d.ts index 118356773d61e..6ef0ef1a31c02 100644 --- a/packages/gatsby/index.d.ts +++ b/packages/gatsby/index.d.ts @@ -50,6 +50,7 @@ export { ImageCdnTransformArgs, FileCdnUrlGeneratorFn, FileCdnSourceImage, + RemoteFileAllowedUrls, } from "./dist/utils/adapter/types" export const useScrollRestoration: (key: string) => { diff --git a/packages/gatsby/src/utils/adapter/manager.ts b/packages/gatsby/src/utils/adapter/manager.ts index bfdbdbb3a99bb..fbe10be215dd0 100644 --- a/packages/gatsby/src/utils/adapter/manager.ts +++ b/packages/gatsby/src/utils/adapter/manager.ts @@ -19,7 +19,7 @@ import type { IAdapterFinalConfig, IAdapterConfig, HeaderRoutes, - IRemoteFileAllowedUrls, + RemoteFileAllowedUrls, } from "./types" import { store, readState } from "../../redux" import { getPageMode } from "../page-mode" @@ -206,7 +206,7 @@ export async function initAdapterManager(): Promise { let _routesManifest: RoutesManifest | undefined = undefined let _functionsManifest: FunctionsManifest | undefined = undefined let _headerRoutes: HeaderRoutes | undefined = undefined - let _imageCdnAllowedUrls: IRemoteFileAllowedUrls | undefined = undefined + let _imageCdnAllowedUrls: RemoteFileAllowedUrls | undefined = undefined const adaptContext: IAdaptContext = { get routesManifest(): RoutesManifest { if (!_routesManifest) { @@ -233,16 +233,16 @@ export async function initAdapterManager(): Promise { return _headerRoutes }, - get remoteFileAllowedUrls(): IRemoteFileAllowedUrls { + get remoteFileAllowedUrls(): RemoteFileAllowedUrls { if (!_imageCdnAllowedUrls) { - const urlPatterns = Array.from( + _imageCdnAllowedUrls = Array.from( store.getState().remoteFileAllowedUrls - ) - - _imageCdnAllowedUrls = { - urlPatterns, - regexes: urlPatterns.map(pattern => pathToRegexp(pattern).source), - } + ).map(urlPattern => { + return { + urlPattern, + regexSource: pathToRegexp(urlPattern).source, + } + }) } return _imageCdnAllowedUrls diff --git a/packages/gatsby/src/utils/adapter/types.ts b/packages/gatsby/src/utils/adapter/types.ts index 56cd696f5c89c..c22e15019c5d1 100644 --- a/packages/gatsby/src/utils/adapter/types.ts +++ b/packages/gatsby/src/utils/adapter/types.ts @@ -117,16 +117,16 @@ interface IDefaultContext { reporter: typeof reporter } -export interface IRemoteFileAllowedUrls { +export type RemoteFileAllowedUrls = Array<{ /** - * List of allowed remote image URLs using URLPattern format. In particular it uses wildcard `*` and param `:param` syntax. + * Allowed url in URLPattern format. In particular it uses wildcard `*` and param `:param` syntax. */ - urlPatterns: Array + urlPattern: string /** - * List of allowed remote image URLs using Regex syntax. + *Allowed url in regex source format */ - regexes: Array -} + regexSource: string +}> export interface IAdaptContext extends IDefaultContext { routesManifest: RoutesManifest @@ -145,7 +145,7 @@ export interface IAdaptContext extends IDefaultContext { * Allowed urls are provided by user or plugins using `addRemoteFileAllowedUrl` action. * @see https://www.gatsbyjs.com/docs/reference/config-files/actions/#addRemoteFileAllowedUrl */ - remoteFileAllowedUrls: IRemoteFileAllowedUrls + remoteFileAllowedUrls: RemoteFileAllowedUrls } export interface ICacheContext extends IDefaultContext {