Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: retain existing Imgix URL parameters on images #375

Merged
merged 4 commits into from
May 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/gatsby-plugin-prismic-previews/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"prismic"
],
"dependencies": {
"@imgix/gatsby": "^1.6.3",
"@imgix/gatsby": "^1.6.6",
"@reach/dialog": "^0.15.0",
"camel-case": "^4.1.2",
"chalk": "^4.1.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import * as imgixGatsbyHelpers from '@imgix/gatsby/dist/pluginHelpers.browser'
import * as RE from 'fp-ts/ReaderEither'
import * as R from 'fp-ts/Record'
import * as O from 'fp-ts/Option'
import * as S from 'fp-ts/Semigroup'
import * as A from 'fp-ts/Array'
import { pipe } from 'fp-ts/function'

import { ProxyDocumentSubtreeEnv } from '../lib/proxyDocumentSubtree'
Expand Down Expand Up @@ -49,9 +51,23 @@ const buildImageProxyValue = (
RE.fromOption(() => new Error(sprintf('Missing height'))),
),
),
RE.bindW('existingImageImgixParams', (env) =>
pipe(
new URL(env.url),
(url) => [...url.searchParams.entries()],
R.fromFoldable(S.last<string>(), A.Foldable),
RE.right,
),
),
RE.bind('mergedImageImgixParams', (env) =>
RE.of({
...env.existingImageImgixParams,
...env.imageImgixParams,
}),
),
RE.bind('args', (env) =>
RE.of({
imgixParams: env.imageImgixParams,
imgixParams: env.mergedImageImgixParams,
placeholderImgixParams: env.imagePlaceholderImgixParams,
}),
),
Expand Down Expand Up @@ -83,7 +99,7 @@ const buildImageProxyValue = (
width: env.sourceWidth,
height: env.sourceHeight,
},
defaultParams: env.imageImgixParams,
defaultParams: env.mergedImageImgixParams,
resolverArgs: {},
}),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { polyfillKy } from './__testutils__/polyfillKy'

import {
PluginOptions,
PrismicAPIDocumentNodeInput,
PrismicPreviewProvider,
PrismicRepositoryConfigs,
usePrismicPreviewBootstrap,
Expand Down Expand Up @@ -480,6 +481,87 @@ test.serial('image', async (t) => {
)
})

test.serial(
'image retains existing URL parameters unless replaced by gatsby-plugin-image or gatsby-image',
async (t) => {
const gatsbyContext = createGatsbyContext()
const pluginOptions = createPluginOptions(t)
const config = createRepositoryConfigs(pluginOptions)

// We want to leave `rect` untouched. We're adding the `w=1` parameter to
// ensure that it is replaced by the resolver to properly support responsive
// images.
const originalUrl = new URL(
'https://example.com/image.png?rect=0,0,100,200&w=1',
)
const doc = createPrismicAPIDocument({
image: {
dimensions: { width: 400, height: 300 },
alt: 'alt',
copyright: 'copyright',
url: originalUrl.toString(),
},
})
const queryResponse = createPrismicAPIQueryResponse([doc])

const result = await performPreview(
t,
// @ts-expect-error - Partial gatsbyContext provided
gatsbyContext,
pluginOptions,
config,
queryResponse,
'822e45b2918cabfa88ea9d2f5600eab7.json',
{
type: gatsbyPrismic.PrismicSpecialType.Document,
'type.data': gatsbyPrismic.PrismicSpecialType.DocumentData,
'type.data.image': gatsbyPrismic.PrismicFieldType.Image,
},
)

const node = result.current.context[0].nodes[
doc.id
] as PrismicAPIDocumentNodeInput<{
image: {
url: string
fixed: { src: string }
fluid: { src: string }
gatsbyImageData: { images: { fallback: { src: string } } }
}
}>

const urlUrl = new URL(node.data.image.url)
t.is(urlUrl.searchParams.get('rect'), originalUrl.searchParams.get('rect'))
t.is(urlUrl.searchParams.get('w'), originalUrl.searchParams.get('w'))

const fixedSrcUrl = new URL(node.data.image.fixed.src)
t.is(
fixedSrcUrl.searchParams.get('rect'),
originalUrl.searchParams.get('rect'),
)
t.not(fixedSrcUrl.searchParams.get('w'), originalUrl.searchParams.get('w'))

const fluidSrcUrl = new URL(node.data.image.fluid.src)
t.is(
fluidSrcUrl.searchParams.get('rect'),
originalUrl.searchParams.get('rect'),
)
t.not(fluidSrcUrl.searchParams.get('w'), originalUrl.searchParams.get('w'))

const gatsbyImageDataSrcUrl = new URL(
node.data.image.gatsbyImageData.images.fallback.src,
)
t.is(
gatsbyImageDataSrcUrl.searchParams.get('rect'),
originalUrl.searchParams.get('rect'),
)
t.not(
gatsbyImageDataSrcUrl.searchParams.get('w'),
originalUrl.searchParams.get('w'),
)
},
)

test.serial('group', async (t) => {
const gatsbyContext = createGatsbyContext()
const pluginOptions = createPluginOptions(t)
Expand Down
2 changes: 1 addition & 1 deletion packages/gatsby-source-prismic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"prismic"
],
"dependencies": {
"@imgix/gatsby": "^1.6.3",
"@imgix/gatsby": "^1.6.6",
"camel-case": "^4.1.2",
"fp-ts": "^2.10.5",
"gatsby-node-helpers": "^1.2.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import * as gqlc from 'graphql-compose'
import * as gatsbyFs from 'gatsby-source-filesystem'
import * as imgixGatsby from '@imgix/gatsby/dist/pluginHelpers'
import * as RTE from 'fp-ts/ReaderTaskEither'
import { pipe } from 'fp-ts/function'
import * as O from 'fp-ts/Option'
import * as S from 'fp-ts/Semigroup'
import * as A from 'fp-ts/Array'
import * as R from 'fp-ts/Record'
import { constNull, pipe } from 'fp-ts/function'

import { Dependencies, PrismicAPIImageField } from '../types'

Expand Down Expand Up @@ -35,6 +39,65 @@ const resolveWidth = (source: PrismicAPIImageField): number | undefined =>
const resolveHeight = (source: PrismicAPIImageField): number | undefined =>
source.dimensions?.height

/**
* The minimum required GraphQL argument properties for an `@imgix/gatsby` field.
*/
interface ImgixGatsbyFieldArgsLike {
imgixParams: Record<string, string | number | boolean>
}

/**
* Modifies an `@imgix/gatsby` GraphQL field config to retain existing Imgix
* parameters set on the source URL.
*
* This is needed if the source URL contains parameters like `rect` (crops an
* image). Without this config enhancer, the `rect` parameter would be removed.
*
* @param fieldConfig GraphQL field config object to be enhanced.
*
* @returns `fieldConfig` with the ability to retain existing Imgix parameters on the source URL.
*/
const withExistingURLImgixParameters = <
TContext,
TArgs extends ImgixGatsbyFieldArgsLike,
>(
fieldConfig: gqlc.ObjectTypeComposerFieldConfigAsObjectDefinition<
PrismicAPIImageField,
TContext,
TArgs
>,
): typeof fieldConfig => ({
...fieldConfig,
resolve: (source, args, ...rest) =>
pipe(
O.Do,
O.bind('url', () =>
O.fromNullable(source.url ? new URL(source.url) : null),
),
O.bind('existingImgixParams', (scope) =>
pipe(
[...scope.url.searchParams.entries()],
R.fromFoldable(S.last<string>(), A.Foldable),
O.of,
),
),
O.map((scope) =>
fieldConfig.resolve?.(
source,
{
...args,
imgixParams: {
...scope.existingImgixParams,
...args.imgixParams,
},
},
...rest,
),
),
O.getOrElseW(constNull),
),
})

/**
* Builds a GraphQL field configuration object to be used as part of another
* Image field GraphQL configuration object. For example, this base
Expand All @@ -61,12 +124,20 @@ export const buildImageBaseFieldConfigMap: RTE.ReaderTaskEither<
}),
),
),
RTE.bind('urlField', (scope) => RTE.right(scope.imgixTypes.fields.url)),
RTE.bind('fixedField', (scope) => RTE.right(scope.imgixTypes.fields.fixed)),
RTE.bind('fluidField', (scope) => RTE.right(scope.imgixTypes.fields.fluid)),
RTE.bind('urlField', (scope) =>
RTE.right(withExistingURLImgixParameters(scope.imgixTypes.fields.url)),
),
RTE.bind('fixedField', (scope) =>
RTE.right(withExistingURLImgixParameters(scope.imgixTypes.fields.fixed)),
),
RTE.bind('fluidField', (scope) =>
RTE.right(withExistingURLImgixParameters(scope.imgixTypes.fields.fluid)),
),
RTE.bind('gatsbyImageDataField', (scope) =>
pipe(
RTE.right(scope.imgixTypes.fields.gatsbyImageData),
RTE.right(
withExistingURLImgixParameters(scope.imgixTypes.fields.gatsbyImageData),
),
// This field is 'JSON!' by default (i.e. non-nullable). If an image is
// not set in Prismic, however, this field throws a GraphQL error saying a
// non-nullable field was returned a null value. This should not happen
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import test from 'ava'
import * as sinon from 'sinon'
import * as msw from 'msw'
import * as mswn from 'msw/node'

import { createGatsbyContext } from './__testutils__/createGatsbyContext'
import { createPluginOptions } from './__testutils__/createPluginOptions'
Expand Down Expand Up @@ -242,3 +244,107 @@ test('thumbnail field resolves thumbnails', async (t) => {

t.true(res.mobile === field.mobile)
})

test('existing image URL parameters are persisted unless replaced in gatsby-plugin-image and gatsby-image fields', async (t) => {
const gatsbyContext = createGatsbyContext()
const pluginOptions = createPluginOptions(t)

pluginOptions.schemas = {
foo: {
Main: {
image: { type: PrismicFieldType.Image, config: {} },
},
},
}

// @ts-expect-error - Partial gatsbyContext provided
await createSchemaCustomization(gatsbyContext, pluginOptions)

const call = findCreateTypesCall(
'PrismicPrefixFooDataImageImageType',
gatsbyContext.actions.createTypes as sinon.SinonStub,
)
// We'll override the `sat` parameter, but leave `rect` untouched.
// We're adding the `w=1` parameter to ensure that it is replaced by the
// resolver to properly support responsive images.
const originalUrl = new URL(
'https://example.com/image.png?rect=0,0,100,200&sat=100&w=1',
)
const field = { url: originalUrl.toString() }
const imgixParams = { sat: 50 }

const server = mswn.setupServer(
msw.rest.get(
`${originalUrl.origin}${originalUrl.pathname}`,
(req, res, ctx) => {
const params = req.url.searchParams

if (params.get('fm') === 'json') {
return res(
ctx.json({
'Content-Type': 'image/png',
PixelWidth: 200,
PixelHeight: 100,
}),
)
} else {
t.fail(
'Forcing a failure due to an unhandled request for Imgix image metadata',
)

return
}
},
),
)
server.listen({ onUnhandledRequest: 'error' })
t.teardown(() => {
server.close()
})

const urlRes = await call.config.fields.url.resolve(field, { imgixParams })
const urlUrl = new URL(urlRes)
t.is(urlUrl.searchParams.get('sat'), imgixParams.sat.toString())
t.is(urlUrl.searchParams.get('rect'), originalUrl.searchParams.get('rect'))
t.is(urlUrl.searchParams.get('w'), originalUrl.searchParams.get('w'))

const fixedRes = await call.config.fields.fixed.resolve(field, {
imgixParams,
})
const fixedSrcUrl = new URL(fixedRes.src)
t.is(fixedSrcUrl.searchParams.get('sat'), imgixParams.sat.toString())
t.is(
fixedSrcUrl.searchParams.get('rect'),
originalUrl.searchParams.get('rect'),
)
t.not(fixedSrcUrl.searchParams.get('w'), originalUrl.searchParams.get('w'))

const fluidRes = await call.config.fields.fluid.resolve(field, {
imgixParams,
})
const fluidSrcUrl = new URL(fluidRes.src)
t.is(fluidSrcUrl.searchParams.get('sat'), imgixParams.sat.toString())
t.is(
fluidSrcUrl.searchParams.get('rect'),
originalUrl.searchParams.get('rect'),
)
t.not(fluidSrcUrl.searchParams.get('w'), originalUrl.searchParams.get('w'))

const gatsbyImageDataRes = await call.config.fields.gatsbyImageData.resolve(
field,
{ imgixParams },
)
const gatsbyImageDataSrcUrl = new URL(gatsbyImageDataRes.images.fallback.src)
t.is(
gatsbyImageDataSrcUrl.searchParams.get('sat'),
imgixParams.sat.toString(),
)
t.is(
gatsbyImageDataSrcUrl.searchParams.get('rect'),
originalUrl.searchParams.get('rect'),
)
t.not(
gatsbyImageDataSrcUrl.searchParams.get('w'),
originalUrl.searchParams.get('w'),
)
})
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"compilerOptions": {
"module": "CommonJS",
"target": "ESNext",
"lib": ["DOM", "ESNext"],
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"importHelpers": true,
"sourceMap": true,
"strict": true,
Expand Down
Loading