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 ( +
+

+ + {node.filename} + +

+ +
+ + + +
+
+ ) + })} +
+ ) +} + +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 ( +
+

+ + {node.filename} + +

+ +
+ + + +
+
+ ) + })} +
+ ) +} + +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