diff --git a/e2e-tests/development-runtime/cypress/integration/remote-file/gatsby-plugin-image.js b/e2e-tests/development-runtime/cypress/integration/remote-file/gatsby-plugin-image.js
new file mode 100644
index 0000000000000..06581bb5d7277
--- /dev/null
+++ b/e2e-tests/development-runtime/cypress/integration/remote-file/gatsby-plugin-image.js
@@ -0,0 +1,81 @@
+before(() => {
+ cy.exec(`npm run reset`)
+})
+
+after(() => {
+ cy.exec(`npm run reset`)
+})
+
+describe(`remote-file`, () => {
+ it(`should render correct dimensions`, () => {
+ cy.visit(`/remote-file/`).waitForRouteChange()
+
+ cy.get('[data-testid="public"]').then($urls => {
+ const urls = Array.from($urls.map((_, $url) => $url.getAttribute("href")))
+
+ expect(urls[0].endsWith(".jpg")).to.be.true
+ expect(urls[1].endsWith(".jpg")).to.be.true
+ expect(urls[2].endsWith(".jpg")).to.be.true
+ })
+
+ cy.get(".resize").then($imgs => {
+ const imgDimensions = $imgs.map((_, $img) => $img.getBoundingClientRect())
+
+ expect(imgDimensions[0].width).to.be.equal(100)
+ expect(imgDimensions[0].height).to.be.equal(133)
+ expect(imgDimensions[1].width).to.be.equal(100)
+ expect(imgDimensions[1].height).to.be.equal(160)
+ expect(imgDimensions[2].width).to.be.equal(100)
+ expect(imgDimensions[2].height).to.be.equal(67)
+ })
+
+ cy.get(".fixed").then($imgs => {
+ const imgDimensions = $imgs.map((_, $img) => $img.getBoundingClientRect())
+
+ expect(imgDimensions[0].width).to.be.equal(100)
+ expect(imgDimensions[0].height).to.be.equal(133)
+ expect(imgDimensions[1].width).to.be.equal(100)
+ expect(imgDimensions[1].height).to.be.equal(160)
+ expect(imgDimensions[2].width).to.be.equal(100)
+ expect(imgDimensions[2].height).to.be.equal(67)
+ })
+
+ cy.get(".constrained").then($imgs => {
+ const imgDimensions = $imgs.map((_, $img) => $img.getBoundingClientRect())
+
+ expect(imgDimensions[0].width).to.be.equal(300)
+ expect(imgDimensions[0].height).to.be.equal(400)
+ expect(imgDimensions[1].width).to.be.equal(300)
+ expect(imgDimensions[1].height).to.be.equal(481)
+ expect(imgDimensions[2].width).to.be.equal(300)
+ expect(imgDimensions[2].height).to.be.equal(200)
+ })
+
+ cy.get(".full").then($imgs => {
+ const parentWidth = $imgs[0].parentElement.getBoundingClientRect().width
+ const imgDimensions = $imgs.map((_, $img) => $img.getBoundingClientRect())
+
+ expect(imgDimensions[0].width).to.be.equal(parentWidth)
+ expect(Math.ceil(imgDimensions[0].height)).to.be.equal(1229)
+ expect(imgDimensions[1].width).to.be.equal(parentWidth)
+ expect(Math.ceil(imgDimensions[1].height)).to.be.equal(1478)
+ expect(imgDimensions[2].width).to.be.equal(parentWidth)
+ expect(Math.ceil(imgDimensions[2].height)).to.be.equal(614)
+ })
+ })
+
+ it(`should render a placeholder`, () => {
+ cy.visit(`/remote-file/`).waitForRouteChange()
+
+ cy.get(".fixed [data-placeholder-image]")
+ .first()
+ .should("have.css", "background-color", "rgb(232, 184, 8)")
+ cy.get(".constrained [data-placeholder-image]")
+ .first()
+ .should("have.prop", "tagName", "IMG")
+ cy.get(".constrained [data-placeholder-image]")
+ .first()
+ .should("contain.prop", "src", "data:image/jpg;base64")
+ cy.get(".full [data-placeholder-image]").first().should("be.empty")
+ })
+})
diff --git a/e2e-tests/development-runtime/gatsby-node.js b/e2e-tests/development-runtime/gatsby-node.js
index e2bed83a75bb2..555cdde5cf55e 100644
--- a/e2e-tests/development-runtime/gatsby-node.js
+++ b/e2e-tests/development-runtime/gatsby-node.js
@@ -1,6 +1,75 @@
const path = require(`path`)
const { createFilePath } = require(`gatsby-source-filesystem`)
+const {
+ addRemoteFilePolyfillInterface,
+ polyfillImageServiceDevRoutes,
+} = require("gatsby-plugin-utils/polyfill-remote-file")
+/** @type{import('gatsby').createSchemaCustomization} */
+exports.createSchemaCustomization = ({ actions, schema, store }) => {
+ actions.createTypes(
+ addRemoteFilePolyfillInterface(
+ schema.buildObjectType({
+ name: "MyRemoteFile",
+ fields: {},
+ interfaces: ["Node", "RemoteFile"],
+ }),
+ {
+ store,
+ schema,
+ }
+ )
+ )
+}
+
+/** @type {imporg('gatsby').sourceNodes} */
+exports.sourceNodes = ({ actions, createNodeId, createContentDigest }) => {
+ const items = [
+ {
+ name: "photoA.jpg",
+ url: "https://images.unsplash.com/photo-1517849845537-4d257902454a?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2000&q=80",
+ placeholderUrl:
+ "https://images.unsplash.com/photo-1517849845537-4d257902454a?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=%width%&h=%height%",
+ mimeType: "image/jpg",
+ filename: "photo-1517849845537.jpg",
+ width: 2000,
+ height: 2667,
+ },
+ {
+ name: "photoB.jpg",
+ url: "https://images.unsplash.com/photo-1552053831-71594a27632d?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&h=2000&q=10",
+ mimeType: "image/jpg",
+ filename: "photo-1552053831.jpg",
+ width: 1247,
+ height: 2000,
+ },
+ {
+ name: "photoC.jpg",
+ url: "https://images.unsplash.com/photo-1561037404-61cd46aa615b?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2000&q=80",
+ placeholderUrl:
+ "https://images.unsplash.com/photo-1561037404-61cd46aa615b?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=%width%&h=%height%",
+ mimeType: "image/jpg",
+ filename: "photo-1561037404.jpg",
+ width: 2000,
+ height: 1333,
+ },
+ ]
+
+ items.forEach((item, index) => {
+ actions.createNode({
+ id: createNodeId(`remote-file-${index}`),
+ ...item,
+ internal: {
+ type: "MyRemoteFile",
+ contentDigest: createContentDigest(item.url),
+ },
+ })
+ })
+}
+
+/**
+ * @type {import('gatsby').onCreateNode}
+ */
exports.onCreateNode = function onCreateNode({
actions: { createNodeField },
node,
@@ -27,6 +96,9 @@ exports.onCreateNode = function onCreateNode({
}
}
+/**
+ * @type {import('gatsby').createPages}
+ */
exports.createPages = async function createPages({
actions: { createPage, createRedirect },
graphql,
@@ -115,6 +187,9 @@ exports.createPages = async function createPages({
})
}
+/**
+ * @type {import('gatsby').onCreatePage}
+ */
exports.onCreatePage = async ({ page, actions }) => {
const { createPage, createRedirect, deletePage } = actions
@@ -169,6 +244,9 @@ exports.onCreatePage = async ({ page, actions }) => {
}
}
+/**
+ * @type {import('gatsby').createResolvers}
+ */
exports.createResolvers = ({ createResolvers }) => {
const resolvers = {
QueryDataCachesJson: {
@@ -192,3 +270,8 @@ exports.createResolvers = ({ createResolvers }) => {
}
createResolvers(resolvers)
}
+
+/** @type{import('gatsby').onCreateDevServer} */
+exports.onCreateDevServer = ({ app }) => {
+ polyfillImageServiceDevRoutes(app)
+}
diff --git a/e2e-tests/development-runtime/src/pages/remote-file.js b/e2e-tests/development-runtime/src/pages/remote-file.js
new file mode 100644
index 0000000000000..a355708493dd5
--- /dev/null
+++ b/e2e-tests/development-runtime/src/pages/remote-file.js
@@ -0,0 +1,73 @@
+import { graphql } from "gatsby"
+import React from "react"
+
+import { GatsbyImage } from "gatsby-plugin-image"
+import Layout from "../components/layout"
+import SEO from "../components/seo"
+
+const RemoteFile = ({ data }) => {
+ return (
+
+
+
+ {data.allMyRemoteFile.nodes.map(node => {
+ return (
+
+
+
+
+
+
+
+
+
+ )
+ })}
+
+ )
+}
+
+export const pageQuery = graphql`
+ {
+ allMyRemoteFile {
+ nodes {
+ id
+ url
+ filename
+ publicUrl
+ resize(width: 100) {
+ height
+ width
+ src
+ }
+ fixed: gatsbyImageData(
+ layout: FIXED
+ width: 100
+ placeholder: DOMINANT_COLOR
+ )
+ constrained: gatsbyImageData(
+ layout: CONSTRAINED
+ width: 300
+ placeholder: BLURRED
+ )
+ full: gatsbyImageData(layout: FULL_WIDTH, width: 500, placeholder: NONE)
+ }
+ }
+ }
+`
+
+export default RemoteFile
diff --git a/e2e-tests/production-runtime/cypress/integration/remote-file.js b/e2e-tests/production-runtime/cypress/integration/remote-file.js
new file mode 100644
index 0000000000000..a355708493dd5
--- /dev/null
+++ b/e2e-tests/production-runtime/cypress/integration/remote-file.js
@@ -0,0 +1,73 @@
+import { graphql } from "gatsby"
+import React from "react"
+
+import { GatsbyImage } from "gatsby-plugin-image"
+import Layout from "../components/layout"
+import SEO from "../components/seo"
+
+const RemoteFile = ({ data }) => {
+ return (
+
+
+
+ {data.allMyRemoteFile.nodes.map(node => {
+ return (
+
+
+
+
+
+
+
+
+
+ )
+ })}
+
+ )
+}
+
+export const pageQuery = graphql`
+ {
+ allMyRemoteFile {
+ nodes {
+ id
+ url
+ filename
+ publicUrl
+ resize(width: 100) {
+ height
+ width
+ src
+ }
+ fixed: gatsbyImageData(
+ layout: FIXED
+ width: 100
+ placeholder: DOMINANT_COLOR
+ )
+ constrained: gatsbyImageData(
+ layout: CONSTRAINED
+ width: 300
+ placeholder: BLURRED
+ )
+ full: gatsbyImageData(layout: FULL_WIDTH, width: 500, placeholder: NONE)
+ }
+ }
+ }
+`
+
+export default RemoteFile
diff --git a/e2e-tests/production-runtime/cypress/integration/remote-file/gatsby-plugin-image.js b/e2e-tests/production-runtime/cypress/integration/remote-file/gatsby-plugin-image.js
new file mode 100644
index 0000000000000..06581bb5d7277
--- /dev/null
+++ b/e2e-tests/production-runtime/cypress/integration/remote-file/gatsby-plugin-image.js
@@ -0,0 +1,81 @@
+before(() => {
+ cy.exec(`npm run reset`)
+})
+
+after(() => {
+ cy.exec(`npm run reset`)
+})
+
+describe(`remote-file`, () => {
+ it(`should render correct dimensions`, () => {
+ cy.visit(`/remote-file/`).waitForRouteChange()
+
+ cy.get('[data-testid="public"]').then($urls => {
+ const urls = Array.from($urls.map((_, $url) => $url.getAttribute("href")))
+
+ expect(urls[0].endsWith(".jpg")).to.be.true
+ expect(urls[1].endsWith(".jpg")).to.be.true
+ expect(urls[2].endsWith(".jpg")).to.be.true
+ })
+
+ cy.get(".resize").then($imgs => {
+ const imgDimensions = $imgs.map((_, $img) => $img.getBoundingClientRect())
+
+ expect(imgDimensions[0].width).to.be.equal(100)
+ expect(imgDimensions[0].height).to.be.equal(133)
+ expect(imgDimensions[1].width).to.be.equal(100)
+ expect(imgDimensions[1].height).to.be.equal(160)
+ expect(imgDimensions[2].width).to.be.equal(100)
+ expect(imgDimensions[2].height).to.be.equal(67)
+ })
+
+ cy.get(".fixed").then($imgs => {
+ const imgDimensions = $imgs.map((_, $img) => $img.getBoundingClientRect())
+
+ expect(imgDimensions[0].width).to.be.equal(100)
+ expect(imgDimensions[0].height).to.be.equal(133)
+ expect(imgDimensions[1].width).to.be.equal(100)
+ expect(imgDimensions[1].height).to.be.equal(160)
+ expect(imgDimensions[2].width).to.be.equal(100)
+ expect(imgDimensions[2].height).to.be.equal(67)
+ })
+
+ cy.get(".constrained").then($imgs => {
+ const imgDimensions = $imgs.map((_, $img) => $img.getBoundingClientRect())
+
+ expect(imgDimensions[0].width).to.be.equal(300)
+ expect(imgDimensions[0].height).to.be.equal(400)
+ expect(imgDimensions[1].width).to.be.equal(300)
+ expect(imgDimensions[1].height).to.be.equal(481)
+ expect(imgDimensions[2].width).to.be.equal(300)
+ expect(imgDimensions[2].height).to.be.equal(200)
+ })
+
+ cy.get(".full").then($imgs => {
+ const parentWidth = $imgs[0].parentElement.getBoundingClientRect().width
+ const imgDimensions = $imgs.map((_, $img) => $img.getBoundingClientRect())
+
+ expect(imgDimensions[0].width).to.be.equal(parentWidth)
+ expect(Math.ceil(imgDimensions[0].height)).to.be.equal(1229)
+ expect(imgDimensions[1].width).to.be.equal(parentWidth)
+ expect(Math.ceil(imgDimensions[1].height)).to.be.equal(1478)
+ expect(imgDimensions[2].width).to.be.equal(parentWidth)
+ expect(Math.ceil(imgDimensions[2].height)).to.be.equal(614)
+ })
+ })
+
+ it(`should render a placeholder`, () => {
+ cy.visit(`/remote-file/`).waitForRouteChange()
+
+ cy.get(".fixed [data-placeholder-image]")
+ .first()
+ .should("have.css", "background-color", "rgb(232, 184, 8)")
+ cy.get(".constrained [data-placeholder-image]")
+ .first()
+ .should("have.prop", "tagName", "IMG")
+ cy.get(".constrained [data-placeholder-image]")
+ .first()
+ .should("contain.prop", "src", "data:image/jpg;base64")
+ cy.get(".full [data-placeholder-image]").first().should("be.empty")
+ })
+})
diff --git a/packages/gatsby-plugin-utils/package.json b/packages/gatsby-plugin-utils/package.json
index b4936ec6d6d8e..13433d3bcf030 100644
--- a/packages/gatsby-plugin-utils/package.json
+++ b/packages/gatsby-plugin-utils/package.json
@@ -48,7 +48,6 @@
"dependencies": {
"@babel/runtime": "^7.15.4",
"gatsby-core-utils": "3.9.0-next.0",
- "gatsby-plugin-utils": "^3.3.0-next.0",
"graphql-compose": "^9.0.7",
"import-from": "^4.0.0",
"joi": "^17.4.2",
diff --git a/packages/gatsby-plugin-utils/src/polyfill-remote-file/graphql/public-url-resolver.ts b/packages/gatsby-plugin-utils/src/polyfill-remote-file/graphql/public-url-resolver.ts
index 891ade9ee699a..11c8c0e181b87 100644
--- a/packages/gatsby-plugin-utils/src/polyfill-remote-file/graphql/public-url-resolver.ts
+++ b/packages/gatsby-plugin-utils/src/polyfill-remote-file/graphql/public-url-resolver.ts
@@ -1,6 +1,4 @@
-import path from "path"
import { generatePublicUrl } from "../utils/url-generator"
-import { getFileExtensionFromMimeType } from "../utils/mime-type-helpers"
import {
dispatchLocalFileServiceJob,
shouldDispatch,
@@ -24,11 +22,13 @@ export function publicUrlResolver(
}
return (
- generatePublicUrl({
- url: source.url,
- // We always want file based url
- mimeType: `application/octet-stream`,
- }) + `/${source.filename}`
+ generatePublicUrl(
+ {
+ url: source.url,
+ mimeType: source.mimeType,
+ },
+ false
+ ) + `/${source.filename}`
)
}
diff --git a/packages/gatsby-plugin-utils/src/polyfill-remote-file/http-routes.ts b/packages/gatsby-plugin-utils/src/polyfill-remote-file/http-routes.ts
index 26e0c216b1627..c39c4e82a6873 100644
--- a/packages/gatsby-plugin-utils/src/polyfill-remote-file/http-routes.ts
+++ b/packages/gatsby-plugin-utils/src/polyfill-remote-file/http-routes.ts
@@ -18,9 +18,9 @@ export function polyfillImageServiceDevRoutes(app: Application): void {
}
export function addImageRoutes(app: Application): Application {
- app.get(`/_gatsby/file/:url`, async (req, res) => {
+ app.get(`/_gatsby/file/:url/:filename`, async (req, res) => {
// remove the file extension
- const [url] = req.params.url.split(`.`)
+ const url = req.params.url
const outputDir = path.join(
global.__GATSBY?.root || process.cwd(),
`public`,
@@ -31,14 +31,13 @@ export function addImageRoutes(app: Application): Application {
const filePath = await fetchRemoteFile({
directory: outputDir,
url: url,
- name: req.params.url,
+ name: req.params.filename,
})
fs.createReadStream(filePath).pipe(res)
})
- app.get(`/_gatsby/image/:url/:params`, async (req, res) => {
- const [params, extension] = req.params.params.split(`.`)
- const url = req.params.url
+ app.get(`/_gatsby/image/:url/:params/:filename`, async (req, res) => {
+ const { params, url, filename } = req.params
const searchParams = new URLSearchParams(
Buffer.from(params, `base64`).toString()
@@ -47,13 +46,13 @@ export function addImageRoutes(app: Application): Application {
const resizeParams: {
width: number
height: number
+ quality: number
format: string
- fit: ImageFit
} = {
width: 0,
height: 0,
+ quality: 75,
format: ``,
- fit: `cover`,
}
for (const [key, value] of searchParams) {
@@ -70,8 +69,8 @@ export function addImageRoutes(app: Application): Application {
resizeParams.format = value
break
}
- case `fit`: {
- resizeParams.fit = value as ImageFit
+ case `q`: {
+ resizeParams.quality = Number(value)
break
}
}
@@ -90,12 +89,15 @@ export function addImageRoutes(app: Application): Application {
outputDir,
args: {
url: remoteUrl,
- filename: generateImageArgs(resizeParams) + `.${extension}`,
+ filename,
...resizeParams,
},
})
- res.setHeader(`content-type`, getFileExtensionFromMimeType(extension))
+ res.setHeader(
+ `content-type`,
+ getFileExtensionFromMimeType(path.extname(filename))
+ )
fs.createReadStream(filePath).pipe(res)
})
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 f13a59e3a4639..6efeb8f3c92d3 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
@@ -24,11 +24,14 @@ export function dispatchLocalFileServiceJob(
store: Store
): void {
const GATSBY_VERSION = getGatsbyVersion()
- const publicUrl = generatePublicUrl({
- url,
- // We always want file based url
- mimeType: `application/octet-stream`,
- }).split(`/`)
+ const publicUrl = generatePublicUrl(
+ {
+ url,
+ // We always want file based url
+ mimeType,
+ },
+ false
+ ).split(`/`)
const extension = getFileExtensionFromMimeType(mimeType)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const filename = publicUrl.pop()
diff --git a/packages/gatsby-plugin-utils/src/polyfill-remote-file/transform-images.ts b/packages/gatsby-plugin-utils/src/polyfill-remote-file/transform-images.ts
index 04740891cbe14..07a98678efe4b 100644
--- a/packages/gatsby-plugin-utils/src/polyfill-remote-file/transform-images.ts
+++ b/packages/gatsby-plugin-utils/src/polyfill-remote-file/transform-images.ts
@@ -29,13 +29,9 @@ export async function transformImage({
args: { url, filename, contentDigest, ...args },
}: {
outputDir: string
- args: {
+ args: IResizeArgs & {
url: string
filename: string
- width: number
- height: number
- format: string
- fit: import("sharp").FitEnum[keyof import("sharp").FitEnum]
contentDigest?: string
}
}): Promise {
@@ -48,7 +44,8 @@ export async function transformImage({
return cachedValue
}
- const [basename, ext] = filename.split(`.`)
+ const ext = path.extname(filename)
+ const basename = path.basename(filename, ext)
const filePath = await fetchRemoteFile({
directory: cache.directory,
url: url,
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 0691bdfb82573..efb0fbac1b8d2 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
@@ -1,16 +1,22 @@
import { isImage } from "../types"
import type { ImageCropFocus, WidthOrHeight } from "../types"
-export function generatePublicUrl({
- url,
- mimeType,
-}: {
- url: string
- mimeType: string
-}): string {
+export function generatePublicUrl(
+ {
+ url,
+ mimeType,
+ }: {
+ url: string
+ mimeType: string
+ },
+ checkMimeType: boolean = true
+): string {
const remoteUrl = Buffer.from(url).toString(`base64`)
- let publicUrl = isImage({ mimeType }) ? `/_gatsby/image/` : `/_gatsby/file/`
+ let publicUrl =
+ checkMimeType && isImage({ mimeType })
+ ? `/_gatsby/image/`
+ : `/_gatsby/file/`
publicUrl += `${remoteUrl}`
return publicUrl