gatsby-transformer-cloudinary
Advanced tools
@@ -0,4 +1,19 @@ | ||
# Version 1.1.0 | ||
Additions: | ||
- Added ability to use existing Cloudinary images by marking nodes with `cloudinaryAssetData: true` and providing `cloudName`, `publicId`, `originalHeight`, and `originalWidth` properties. | ||
- Added an optional `height` argument to `fixed` queries. | ||
Improvements: | ||
- Cache base64 images when running queries to prevent duplicate network requests. | ||
Fixes: | ||
- Changed the public_id to be the relative path of files without the extension instead of just the file's name. This fixes an [issue with childrenCloudinaryAsset nodes](https://github.com/cloudinary-devs/gatsby-transformer-cloudinary/issues/42) being created instead of childCloudinaryAsset nodes. | ||
# Version 1.0.1 | ||
Other changes: | ||
Additions: | ||
@@ -5,0 +20,0 @@ - Added CloudinaryAssetFluidLimitPresentationSize fragment. |
@@ -0,1 +1,2 @@ | ||
const stringify = require('fast-json-stable-stringify'); | ||
const { getPluginOptions } = require('./options'); | ||
@@ -32,3 +33,2 @@ | ||
width, | ||
secure_url, | ||
}, | ||
@@ -38,5 +38,4 @@ parentNode, | ||
createNodeId, | ||
cloudName, | ||
}) => { | ||
const { cloudName } = getPluginOptions(); | ||
let breakpoints = getDefaultBreakpoints(width); | ||
@@ -54,6 +53,15 @@ if ( | ||
const fingerprint = stringify({ | ||
cloudName, | ||
height, | ||
public_id, | ||
breakpoints, | ||
version, | ||
width, | ||
}); | ||
const imageNode = { | ||
// These helper fields are only here so the resolvers have access to them. | ||
// They will *not* be available via Gatsby’s data layer. | ||
cloudName: cloudName, | ||
cloudName: cloudName || getPluginOptions().cloudName, | ||
public_id: public_id, | ||
@@ -66,3 +74,3 @@ version: version, | ||
// Add the required internal Gatsby node fields. | ||
id: createNodeId(`CloudinaryAsset-${secure_url}`), | ||
id: createNodeId(`CloudinaryAsset-${fingerprint}`), | ||
parent: parentNode.id, | ||
@@ -73,3 +81,3 @@ internal: { | ||
// node. We can use the Cloudinary URL to avoid doing extra work. | ||
contentDigest: createContentDigest(secure_url), | ||
contentDigest: createContentDigest(fingerprint), | ||
}, | ||
@@ -76,0 +84,0 @@ }; |
@@ -156,3 +156,5 @@ const { createImageNode } = require('./create-image-node'); | ||
const createNodeId = jest.fn(createNodeIdArg => { | ||
expect(createNodeIdArg).toEqual('CloudinaryAsset-secure_url'); | ||
expect(createNodeIdArg).toEqual( | ||
'CloudinaryAsset-{"breakpoints":[20,35],"cloudName":"cloudName","height":100,"public_id":"public_id","version":7,"width":200}', | ||
); | ||
return 'createNodeIdResult'; | ||
@@ -162,3 +164,12 @@ }); | ||
createNodeId, | ||
cloudinaryUploadResult: { secure_url: 'secure_url' }, | ||
cloudinaryUploadResult: { | ||
height: 100, | ||
public_id: 'public_id', | ||
responsive_breakpoints: [ | ||
{ breakpoints: [{ width: 20 }, { width: 35 }] }, | ||
], | ||
version: 7, | ||
width: 200, | ||
}, | ||
cloudName: 'cloudName', | ||
}); | ||
@@ -189,3 +200,5 @@ const actual = createImageNode(args); | ||
const createContentDigest = jest.fn(createContentDigestArg => { | ||
expect(createContentDigestArg).toEqual('secure_url'); | ||
expect(createContentDigestArg).toEqual( | ||
'{"breakpoints":[20,35],"cloudName":"cloudName","height":100,"public_id":"public_id","version":7,"width":200}', | ||
); | ||
return 'createContentDigestResult'; | ||
@@ -195,3 +208,12 @@ }); | ||
createContentDigest, | ||
cloudinaryUploadResult: { secure_url: 'secure_url' }, | ||
cloudinaryUploadResult: { | ||
height: 100, | ||
public_id: 'public_id', | ||
responsive_breakpoints: [ | ||
{ breakpoints: [{ width: 20 }, { width: 35 }] }, | ||
], | ||
version: 7, | ||
width: 200, | ||
}, | ||
cloudName: 'cloudName', | ||
}); | ||
@@ -198,0 +220,0 @@ const actual = createImageNode(args); |
@@ -16,9 +16,14 @@ const path = require('path'); | ||
}) => { | ||
if (!url) { | ||
if (!reporter) { | ||
throw Error( | ||
"`url` is a required argument. Pass the URL where the image is currently hosted so it can be downloaded by Cloudinary.", | ||
"`reporter` is a required argument. It's available at `CreateNodeArgs.reporter`.", | ||
); | ||
} | ||
if (!url) { | ||
reporter.panic( | ||
'`url` is a required argument. Pass the URL where the image is currently hosted so it can be downloaded by Cloudinary.', | ||
); | ||
} | ||
if (!parentNode) { | ||
throw Error( | ||
reporter.panic( | ||
"`parentNode` is a required argument. This parameter is used to link a newly created node representing the image to a parent node in Gatsby's GraphQL layer.", | ||
@@ -28,3 +33,3 @@ ); | ||
if (!relationshipName) { | ||
throw Error( | ||
reporter.panic( | ||
"`relationshipName` is a required argument. This parameter sets the name of the relationship between the parent node and the newly created node for this image in Gatsby's GraphQL layer.", | ||
@@ -34,3 +39,3 @@ ); | ||
if (!createContentDigest) { | ||
throw Error( | ||
reporter.panic( | ||
"`createContentDigest` is a required argument. It's available at `CreateNodeArgs.createContentDigest`.", | ||
@@ -40,3 +45,3 @@ ); | ||
if (!createNode) { | ||
throw Error( | ||
reporter.panic( | ||
"`createNode` is a required argument. It's available at `CreateNodeArgs.actions.createNode`.", | ||
@@ -46,11 +51,6 @@ ); | ||
if (!createNodeId) { | ||
throw Error( | ||
reporter.panic( | ||
"`createNodeId` is a required argument. It's available at `CreateNodeArgs.createNodeId`.", | ||
); | ||
} | ||
if (!reporter) { | ||
throw Error( | ||
"`reporter` is a required argument. It's available at `CreateNodeArgs.reporter`.", | ||
); | ||
} | ||
@@ -72,7 +72,5 @@ const overwrite = | ||
const imageNode = createImageNode({ | ||
relationshipName, | ||
cloudinaryUploadResult, | ||
parentNode, | ||
createContentDigest, | ||
createNode, | ||
createNodeId, | ||
@@ -79,0 +77,0 @@ }); |
@@ -19,3 +19,7 @@ const path = require('path'); | ||
createContentDigest: jest.fn(() => 'createContentDigest'), | ||
reporter: {}, | ||
reporter: { | ||
panic: msg => { | ||
throw Error(`[reporter] ${msg}}`); | ||
}, | ||
}, | ||
parentNode: { id: 'abc-123' }, | ||
@@ -27,166 +31,166 @@ overwriteExisting: false, | ||
test('requires url', async () => { | ||
const args = getDefaultArgs(); | ||
delete args.url; | ||
describe('createRemoteImageNode', () => { | ||
test('requires url', async () => { | ||
const args = getDefaultArgs(); | ||
delete args.url; | ||
await expect(createRemoteImageNode(args)).rejects.toThrow( | ||
'`url` is a required argument. Pass the URL where the image is currently hosted so it can be downloaded by Cloudinary.', | ||
); | ||
}); | ||
await expect(createRemoteImageNode(args)).rejects.toThrow( | ||
'[reporter] `url` is a required argument. Pass the URL where the image is currently hosted so it can be downloaded by Cloudinary.', | ||
); | ||
}); | ||
test('requires parentNode', async () => { | ||
const args = getDefaultArgs(); | ||
delete args.parentNode; | ||
test('requires parentNode', async () => { | ||
const args = getDefaultArgs(); | ||
delete args.parentNode; | ||
await expect(createRemoteImageNode(args)).rejects.toThrow( | ||
"`parentNode` is a required argument. This parameter is used to link a newly created node representing the image to a parent node in Gatsby's GraphQL layer.", | ||
); | ||
}); | ||
await expect(createRemoteImageNode(args)).rejects.toThrow( | ||
"[reporter] `parentNode` is a required argument. This parameter is used to link a newly created node representing the image to a parent node in Gatsby's GraphQL layer.", | ||
); | ||
}); | ||
test('requires relationshipName', async () => { | ||
const args = getDefaultArgs(); | ||
delete args.relationshipName; | ||
test('requires relationshipName', async () => { | ||
const args = getDefaultArgs(); | ||
delete args.relationshipName; | ||
await expect(createRemoteImageNode(args)).rejects.toThrow( | ||
"`relationshipName` is a required argument. This parameter sets the name of the relationship between the parent node and the newly created node for this image in Gatsby's GraphQL layer.", | ||
); | ||
}); | ||
await expect(createRemoteImageNode(args)).rejects.toThrow( | ||
"[reporter] `relationshipName` is a required argument. This parameter sets the name of the relationship between the parent node and the newly created node for this image in Gatsby's GraphQL layer.", | ||
); | ||
}); | ||
test('requires createContentDigest', async () => { | ||
const args = getDefaultArgs(); | ||
delete args.createContentDigest; | ||
test('requires createContentDigest', async () => { | ||
const args = getDefaultArgs(); | ||
delete args.createContentDigest; | ||
await expect(createRemoteImageNode(args)).rejects.toThrow( | ||
"`createContentDigest` is a required argument. It's available at `CreateNodeArgs.createContentDigest`.", | ||
); | ||
}); | ||
await expect(createRemoteImageNode(args)).rejects.toThrow( | ||
"[reporter] `createContentDigest` is a required argument. It's available at `CreateNodeArgs.createContentDigest`.", | ||
); | ||
}); | ||
test('requires createNode', async () => { | ||
const args = getDefaultArgs(); | ||
delete args.createNode; | ||
test('requires createNode', async () => { | ||
const args = getDefaultArgs(); | ||
delete args.createNode; | ||
await expect(createRemoteImageNode(args)).rejects.toThrow( | ||
"`createNode` is a required argument. It's available at `CreateNodeArgs.actions.createNode`.", | ||
); | ||
}); | ||
await expect(createRemoteImageNode(args)).rejects.toThrow( | ||
"[reporter] `createNode` is a required argument. It's available at `CreateNodeArgs.actions.createNode`.", | ||
); | ||
}); | ||
test('requires createNodeId', async () => { | ||
const args = getDefaultArgs(); | ||
delete args.createNodeId; | ||
test('requires createNodeId', async () => { | ||
const args = getDefaultArgs(); | ||
delete args.createNodeId; | ||
await expect(createRemoteImageNode(args)).rejects.toThrow( | ||
"`createNodeId` is a required argument. It's available at `CreateNodeArgs.createNodeId`.", | ||
); | ||
}); | ||
await expect(createRemoteImageNode(args)).rejects.toThrow( | ||
"[reporter] `createNodeId` is a required argument. It's available at `CreateNodeArgs.createNodeId`.", | ||
); | ||
}); | ||
test('requires reporter', async () => { | ||
const args = getDefaultArgs(); | ||
delete args.reporter; | ||
test('requires reporter', async () => { | ||
const args = getDefaultArgs(); | ||
delete args.reporter; | ||
await expect(createRemoteImageNode(args)).rejects.toThrow( | ||
"`reporter` is a required argument. It's available at `CreateNodeArgs.reporter`.", | ||
); | ||
}); | ||
await expect(createRemoteImageNode(args)).rejects.toThrow( | ||
"`reporter` is a required argument. It's available at `CreateNodeArgs.reporter`.", | ||
); | ||
}); | ||
test('calls uploadImageToCloudinary with overwrite from plugin options by default', async () => { | ||
const args = getDefaultArgs(); | ||
delete args.overwriteExisting; | ||
test('calls uploadImageToCloudinary with overwrite from plugin options by default', async () => { | ||
const args = getDefaultArgs(); | ||
delete args.overwriteExisting; | ||
const optionOverwrite = 'optionOverwrite'; | ||
getPluginOptions.mockReturnValue({ overwriteExisting: optionOverwrite }); | ||
createImageNode.mockReturnValue({ id: 'image-node-id' }); | ||
const optionOverwrite = 'optionOverwrite'; | ||
getPluginOptions.mockReturnValue({ overwriteExisting: optionOverwrite }); | ||
createImageNode.mockReturnValue({ id: 'image-node-id' }); | ||
await createRemoteImageNode(args); | ||
await createRemoteImageNode(args); | ||
const expectedArgs = { overwrite: optionOverwrite }; | ||
expect(uploadImageToCloudinary).toHaveBeenCalledWith( | ||
expect.objectContaining(expectedArgs), | ||
); | ||
}); | ||
const expectedArgs = { overwrite: optionOverwrite }; | ||
expect(uploadImageToCloudinary).toHaveBeenCalledWith( | ||
expect.objectContaining(expectedArgs), | ||
); | ||
}); | ||
test('calls uploadImageToCloudinary with overwrite from args if provided', async () => { | ||
const argsOverwrite = 'argsOverwrite'; | ||
const args = getDefaultArgs({ overwriteExisting: argsOverwrite }); | ||
test('calls uploadImageToCloudinary with overwrite from args if provided', async () => { | ||
const argsOverwrite = 'argsOverwrite'; | ||
const args = getDefaultArgs({ overwriteExisting: argsOverwrite }); | ||
const optionOverwrite = 'optionOverwrite'; | ||
getPluginOptions.mockReturnValue({ overwriteExisting: optionOverwrite }); | ||
createImageNode.mockReturnValue({ id: 'image-node-id' }); | ||
const optionOverwrite = 'optionOverwrite'; | ||
getPluginOptions.mockReturnValue({ overwriteExisting: optionOverwrite }); | ||
createImageNode.mockReturnValue({ id: 'image-node-id' }); | ||
await createRemoteImageNode(args); | ||
await createRemoteImageNode(args); | ||
const expectedArgs = { overwrite: argsOverwrite }; | ||
expect(uploadImageToCloudinary).toHaveBeenCalledWith( | ||
expect.objectContaining(expectedArgs), | ||
); | ||
}); | ||
const expectedArgs = { overwrite: argsOverwrite }; | ||
expect(uploadImageToCloudinary).toHaveBeenCalledWith( | ||
expect.objectContaining(expectedArgs), | ||
); | ||
}); | ||
test('calls uploadImageToCloudinary with the correct arguments', async () => { | ||
const imageNodeId = 'image-node-id'; | ||
createImageNode.mockReturnValue({ id: imageNodeId }); | ||
const reporter = 'reporter'; | ||
const args = getDefaultArgs({ reporter }); | ||
await createRemoteImageNode(args); | ||
const expectedArgs = { | ||
url: args.url, | ||
publicId: path.parse(args.url).name, | ||
reporter, | ||
}; | ||
expect(uploadImageToCloudinary).toHaveBeenCalledWith( | ||
expect.objectContaining(expectedArgs), | ||
); | ||
}); | ||
test('calls uploadImageToCloudinary with the correct arguments', async () => { | ||
const imageNodeId = 'image-node-id'; | ||
createImageNode.mockReturnValue({ id: imageNodeId }); | ||
const reporter = 'reporter'; | ||
const args = getDefaultArgs({ reporter }); | ||
await createRemoteImageNode(args); | ||
const expectedArgs = { | ||
url: args.url, | ||
publicId: path.parse(args.url).name, | ||
reporter, | ||
}; | ||
expect(uploadImageToCloudinary).toHaveBeenCalledWith( | ||
expect.objectContaining(expectedArgs), | ||
); | ||
}); | ||
test('passes the correct arguments to createImageNode', async () => { | ||
const args = getDefaultArgs(); | ||
createImageNode.mockReturnValue({ id: 'image-node-id' }); | ||
const uploadImageToCloudinaryResult = 'uploadImageToCloudinaryResult'; | ||
uploadImageToCloudinary.mockReturnValue(uploadImageToCloudinaryResult); | ||
test('passes the correct arguments to createImageNode', async () => { | ||
const args = getDefaultArgs(); | ||
createImageNode.mockReturnValue({ id: 'image-node-id' }); | ||
const cloudinaryUploadResult = 'cloudinaryUploadResult'; | ||
uploadImageToCloudinary.mockReturnValue(cloudinaryUploadResult); | ||
await createRemoteImageNode(args); | ||
await createRemoteImageNode(args); | ||
const expectedArgs = { | ||
relationshipName: args.relationshipName, | ||
cloudinaryUploadResult: uploadImageToCloudinaryResult, | ||
parentNode: args.parentNode, | ||
createContentDigest: args.createContentDigest, | ||
createNode: args.createNode, | ||
createNodeId: args.createNodeId, | ||
}; | ||
expect(createImageNode).toHaveBeenCalledWith( | ||
expect.objectContaining(expectedArgs), | ||
); | ||
}); | ||
const expectedArgs = { | ||
cloudinaryUploadResult, | ||
parentNode: args.parentNode, | ||
createContentDigest: args.createContentDigest, | ||
createNodeId: args.createNodeId, | ||
}; | ||
expect(createImageNode).toHaveBeenCalledWith( | ||
expect.objectContaining(expectedArgs), | ||
); | ||
}); | ||
test("creates an imageNode in Gatsby's GraphQL layer", async () => { | ||
const createNode = jest.fn(); | ||
const args = getDefaultArgs({ createNode }); | ||
const createImageNodeResult = 'createImageNodeResult'; | ||
createImageNode.mockReturnValue(createImageNodeResult); | ||
test("creates an imageNode in Gatsby's GraphQL layer", async () => { | ||
const createNode = jest.fn(); | ||
const args = getDefaultArgs({ createNode }); | ||
const createImageNodeResult = 'createImageNodeResult'; | ||
createImageNode.mockReturnValue(createImageNodeResult); | ||
await createRemoteImageNode(args); | ||
expect(createNode).toHaveBeenCalledWith(createImageNodeResult, { | ||
name: 'gatsby-transformer-cloudinary', | ||
await createRemoteImageNode(args); | ||
expect(createNode).toHaveBeenCalledWith(createImageNodeResult, { | ||
name: 'gatsby-transformer-cloudinary', | ||
}); | ||
}); | ||
}); | ||
test('links the newly created node to the provided parent node in GraphQL', async () => { | ||
const args = getDefaultArgs(); | ||
const imageNodeId = 'image-node-id'; | ||
createImageNode.mockReturnValue({ id: imageNodeId }); | ||
await createRemoteImageNode(args); | ||
test('links the newly created node to the provided parent node in GraphQL', async () => { | ||
const args = getDefaultArgs(); | ||
const imageNodeId = 'image-node-id'; | ||
createImageNode.mockReturnValue({ id: imageNodeId }); | ||
await createRemoteImageNode(args); | ||
expect(args.parentNode[`${args.relationshipName}___NODE`]).toEqual( | ||
imageNodeId, | ||
); | ||
}); | ||
expect(args.parentNode[`${args.relationshipName}___NODE`]).toEqual( | ||
imageNodeId, | ||
); | ||
}); | ||
test('returns the image node that it created', async () => { | ||
const args = getDefaultArgs(); | ||
const imageNodeId = 'image-node-id'; | ||
const imageNode = { id: imageNodeId }; | ||
createImageNode.mockReturnValue(imageNode); | ||
test('returns the image node that it created', async () => { | ||
const args = getDefaultArgs(); | ||
const imageNodeId = 'image-node-id'; | ||
const imageNode = { id: imageNodeId }; | ||
createImageNode.mockReturnValue(imageNode); | ||
const actual = await createRemoteImageNode(args); | ||
const actual = await createRemoteImageNode(args); | ||
expect(actual).toEqual(imageNode); | ||
expect(actual).toEqual(imageNode); | ||
}); | ||
}); |
@@ -9,2 +9,5 @@ const fs = require('fs-extra'); | ||
const { createImageNode } = require('./create-image-node'); | ||
const { | ||
createAssetNodesFromData, | ||
} = require('./gatsby-node/create-asset-nodes-from-data'); | ||
@@ -36,2 +39,3 @@ const ALLOWED_MEDIA_TYPES = ['image/png', 'image/jpeg', 'image/gif']; | ||
chained: [String!] | ||
height: Int | ||
transformations: [String!] | ||
@@ -81,2 +85,3 @@ width: Int | ||
base64Transformations, | ||
height, | ||
width, | ||
@@ -93,2 +98,3 @@ transformations, | ||
originalWidth, | ||
height, | ||
width, | ||
@@ -140,3 +146,3 @@ base64Width, | ||
exports.onCreateNode = async ({ | ||
async function createAssetNodeFromFile({ | ||
node, | ||
@@ -147,3 +153,3 @@ actions: { createNode, createParentChildLink }, | ||
reporter, | ||
}) => { | ||
}) { | ||
if (!ALLOWED_MEDIA_TYPES.includes(node.internal.mediaType)) { | ||
@@ -164,3 +170,2 @@ return; | ||
createNodeId, | ||
createParentChildLink, | ||
}); | ||
@@ -178,2 +183,25 @@ | ||
return imageNode; | ||
} | ||
exports.onCreateNode = async ({ | ||
node, | ||
actions, | ||
createNodeId, | ||
createContentDigest, | ||
reporter, | ||
}) => { | ||
createAssetNodesFromData({ | ||
node, | ||
actions, | ||
createNodeId, | ||
createContentDigest, | ||
reporter, | ||
}); | ||
await createAssetNodeFromFile({ | ||
node, | ||
actions, | ||
createNodeId, | ||
createContentDigest, | ||
reporter, | ||
}); | ||
}; | ||
@@ -180,0 +208,0 @@ |
@@ -10,2 +10,4 @@ const axios = require('axios'); | ||
const base64Cache = {}; | ||
// Create Cloudinary image URL with transformations. | ||
@@ -40,6 +42,9 @@ const getImageURL = ({ | ||
const getBase64 = async url => { | ||
const result = await axios.get(url, { responseType: 'arraybuffer' }); | ||
const data = Buffer.from(result.data).toString('base64'); | ||
if (!base64Cache[url]) { | ||
const result = await axios.get(url, { responseType: 'arraybuffer' }); | ||
const data = Buffer.from(result.data).toString('base64'); | ||
base64Cache[url] = `data:image/jpeg;base64,${data}`; | ||
} | ||
return `data:image/jpeg;base64,${data}`; | ||
return base64Cache[url]; | ||
}; | ||
@@ -101,3 +106,4 @@ | ||
version = false, | ||
width = DEFAULT_FIXED_WIDTH, | ||
height, | ||
width, | ||
base64Width = DEFAULT_BASE64_WIDTH, | ||
@@ -123,5 +129,14 @@ base64Transformations = [], | ||
let displayWidth; | ||
if (!!width) { | ||
displayWidth = Math.min(width, originalWidth); | ||
} else if (!!height) { | ||
displayWidth = Math.min(height * aspectRatio, originalWidth); | ||
} else if (!height && !width) { | ||
displayWidth = Math.min(DEFAULT_FIXED_WIDTH, originalWidth); | ||
} | ||
const sizes = [1, 1.5, 2, 3].map(size => ({ | ||
resolution: size, | ||
width: width * size, | ||
width: Math.round(displayWidth * size), | ||
})); | ||
@@ -148,9 +163,13 @@ | ||
base64, | ||
height: width / aspectRatio, | ||
height: Math.round(displayWidth / aspectRatio), | ||
src, | ||
srcSet, | ||
width, | ||
width: Math.round(displayWidth), | ||
}; | ||
}; | ||
function onlyUnique(element, index, array) { | ||
return array.indexOf(element) === index; | ||
} | ||
exports.getFluidImageObject = async ({ | ||
@@ -187,10 +206,9 @@ public_id, | ||
const cleaned = breakpoints | ||
const breakpointWidths = breakpoints | ||
.concat(max) // make sure we get the max size | ||
.filter(w => w <= max) // don’t add larger sizes | ||
.sort((a, b) => a - b); // sort in ascending order | ||
.sort((a, b) => a - b) // sort in ascending order | ||
.filter(onlyUnique); // remove duplicates | ||
const deduped = [...new Set(cleaned)]; | ||
const srcSet = deduped | ||
const srcSet = breakpointWidths | ||
.map(breakpointWidth => { | ||
@@ -197,0 +215,0 @@ // Get URL for each image including user-defined transformations. |
@@ -1,63 +0,218 @@ | ||
const { getFluidImageObject } = require('./get-image-objects'); | ||
const { | ||
getFluidImageObject, | ||
getFixedImageObject, | ||
} = require('./get-image-objects'); | ||
jest.mock('./options'); | ||
jest.mock('axios'); | ||
const { get } = require('axios'); | ||
const base64ImageData = ['1', '2', '3']; | ||
get.mockReturnValue({ data: base64ImageData }); | ||
const axios = require('axios'); | ||
jest.mock('./options'); | ||
const { getPluginOptions } = require('./options'); | ||
function getDefaultArgs(args) { | ||
return { | ||
public_id: 'public_id', | ||
cloudName: 'cloudName', | ||
originalWidth: 1920, | ||
originalHeight: 1080, | ||
...args, | ||
}; | ||
} | ||
describe('getFluidImageObject', () => { | ||
function getDefaultArgs(args) { | ||
return { | ||
public_id: 'public_id', | ||
cloudName: 'cloudName', | ||
originalWidth: 1920, | ||
originalHeight: 1080, | ||
...args, | ||
}; | ||
} | ||
function getDefaultOptions(options) { | ||
return { | ||
fluidMaxWidth: 1000, | ||
...options, | ||
}; | ||
} | ||
function getDefaultOptions(options) { | ||
return { | ||
fluidMaxWidth: 1000, | ||
...options, | ||
}; | ||
} | ||
beforeEach(() => { | ||
axios.get = jest.fn(() => ({ data: ['1', '2', '3'] })); | ||
}); | ||
it('returns presentationWidth=fluidMaxWidth when fluidMaxWidth is smaller', async () => { | ||
const options = getDefaultOptions(); | ||
getPluginOptions.mockReturnValue(options); | ||
const args = getDefaultArgs(); | ||
test('getFluidImageObject returns presentationWidth=fluidMaxWidth when fluidMaxWidth is smaller', async () => { | ||
const options = getDefaultOptions(); | ||
getPluginOptions.mockReturnValue(options); | ||
const args = getDefaultArgs(); | ||
const presentationWidth = options.fluidMaxWidth; | ||
expect(await getFluidImageObject(args)).toEqual( | ||
expect.objectContaining({ presentationWidth }), | ||
); | ||
}); | ||
const presentationWidth = options.fluidMaxWidth; | ||
expect(await getFluidImageObject(args)).toEqual( | ||
expect.objectContaining({ presentationWidth }), | ||
); | ||
}); | ||
it('returns presentationWidth=originalWidth when originalWidth is smaller', async () => { | ||
const options = getDefaultOptions({ fluidMaxWidth: 10000 }); | ||
getPluginOptions.mockReturnValue(options); | ||
const args = getDefaultArgs(); | ||
test('getFluidImageObject returns presentationWidth=originalWidth when originalWidth is smaller', async () => { | ||
const options = getDefaultOptions({ fluidMaxWidth: 10000 }); | ||
getPluginOptions.mockReturnValue(options); | ||
const args = getDefaultArgs(); | ||
const presentationWidth = args.originalWidth; | ||
expect(await getFluidImageObject(args)).toEqual( | ||
expect.objectContaining({ presentationWidth }), | ||
); | ||
}); | ||
const presentationWidth = args.originalWidth; | ||
expect(await getFluidImageObject(args)).toEqual( | ||
expect.objectContaining({ presentationWidth }), | ||
); | ||
it('calculates presentation height', async () => { | ||
const options = getDefaultOptions(); | ||
getPluginOptions.mockReturnValue(options); | ||
const args = getDefaultArgs(); | ||
const presentationHeight = Math.round( | ||
(options.fluidMaxWidth * args.originalHeight) / args.originalWidth, | ||
); | ||
expect(await getFluidImageObject(args)).toEqual( | ||
expect.objectContaining({ presentationHeight }), | ||
); | ||
}); | ||
it('returns a base64 image', async () => { | ||
const options = getDefaultOptions(); | ||
getPluginOptions.mockReturnValue(options); | ||
const args = getDefaultArgs(); | ||
const base64 = Buffer.from(base64ImageData).toString('base64'); | ||
const expectedBase64Image = `data:image/jpeg;base64,${base64}`; | ||
expect(await getFluidImageObject(args)).toEqual( | ||
expect.objectContaining({ base64: expectedBase64Image }), | ||
); | ||
}); | ||
it('does not fetch base64 images multiple times', async () => { | ||
const options = getDefaultOptions(); | ||
getPluginOptions.mockReturnValue(options); | ||
const args = getDefaultArgs(); | ||
for (let i = 1; i <= 3; i++) { | ||
await getFluidImageObject(args); | ||
} | ||
expect(get).toHaveBeenCalledTimes(1); | ||
}); | ||
}); | ||
test('getFluidImageObject calculates presentation height', async () => { | ||
const options = getDefaultOptions(); | ||
getPluginOptions.mockReturnValue(options); | ||
const args = getDefaultArgs(); | ||
describe('getFixedImageObject', () => { | ||
function getDefaultArgs(args) { | ||
return { | ||
public_id: 'public_id', | ||
cloudName: 'cloudName', | ||
originalWidth: 1920, | ||
originalHeight: 1080, | ||
...args, | ||
}; | ||
} | ||
const presentationHeight = Math.round( | ||
(options.fluidMaxWidth * args.originalHeight) / args.originalWidth, | ||
); | ||
expect(await getFluidImageObject(args)).toEqual( | ||
expect.objectContaining({ presentationHeight }), | ||
); | ||
function getDefaultOptions(options) { | ||
return { | ||
...options, | ||
}; | ||
} | ||
it('uses a width of 400 px if no width is provided', async () => { | ||
const options = getDefaultOptions(); | ||
getPluginOptions.mockReturnValue(options); | ||
const args = getDefaultArgs(); | ||
expect(await getFixedImageObject(args)).toEqual( | ||
expect.objectContaining({ width: 400 }), | ||
); | ||
}); | ||
it("uses the image's originalWidth if it is smaller than the requested width", async () => { | ||
const options = getDefaultOptions(); | ||
getPluginOptions.mockReturnValue(options); | ||
const args = getDefaultArgs({ width: 20000 }); | ||
expect(await getFixedImageObject(args)).toEqual( | ||
expect.objectContaining({ width: 1920 }), | ||
); | ||
}); | ||
it("uses the image's originalWidth if it is smaller than the default width of 400 px", async () => { | ||
const options = getDefaultOptions(); | ||
getPluginOptions.mockReturnValue(options); | ||
const args = getDefaultArgs({ originalWidth: 100 }); | ||
expect(await getFixedImageObject(args)).toEqual( | ||
expect.objectContaining({ width: 100 }), | ||
); | ||
}); | ||
it('calculates the height based on the provided width', async () => { | ||
const options = getDefaultOptions(); | ||
getPluginOptions.mockReturnValue(options); | ||
const args = getDefaultArgs({ width: 100 }); | ||
expect(await getFixedImageObject(args)).toEqual( | ||
expect.objectContaining({ width: 100, height: 56 }), | ||
); | ||
}); | ||
it('calculates the width based on the provided height', async () => { | ||
const options = getDefaultOptions(); | ||
getPluginOptions.mockReturnValue(options); | ||
const args = getDefaultArgs({ height: 100 }); | ||
expect(await getFixedImageObject(args)).toEqual( | ||
expect.objectContaining({ width: 178, height: 100 }), | ||
); | ||
}); | ||
it('creates a srcset with multiple images based on the provided width', async () => { | ||
const options = getDefaultOptions(); | ||
getPluginOptions.mockReturnValue(options); | ||
const args = getDefaultArgs({ width: 160 }); | ||
const expectedSrcSet = [ | ||
'https://res.cloudinary.com/cloudName/image/upload/w_160,f_auto,q_auto/public_id 1x', | ||
'https://res.cloudinary.com/cloudName/image/upload/w_240,f_auto,q_auto/public_id 1.5x', | ||
'https://res.cloudinary.com/cloudName/image/upload/w_320,f_auto,q_auto/public_id 2x', | ||
'https://res.cloudinary.com/cloudName/image/upload/w_480,f_auto,q_auto/public_id 3x', | ||
]; | ||
expect(await getFixedImageObject(args)).toEqual( | ||
expect.objectContaining({ | ||
srcSet: expectedSrcSet.join(','), | ||
}), | ||
); | ||
}); | ||
it('creates a srcset with multiple images based on the provided height', async () => { | ||
const options = getDefaultOptions(); | ||
getPluginOptions.mockReturnValue(options); | ||
const args = getDefaultArgs({ height: 100 }); | ||
const expectedSrcSet = [ | ||
'https://res.cloudinary.com/cloudName/image/upload/w_178,f_auto,q_auto/public_id 1x', | ||
'https://res.cloudinary.com/cloudName/image/upload/w_267,f_auto,q_auto/public_id 1.5x', | ||
'https://res.cloudinary.com/cloudName/image/upload/w_356,f_auto,q_auto/public_id 2x', | ||
'https://res.cloudinary.com/cloudName/image/upload/w_533,f_auto,q_auto/public_id 3x', | ||
]; | ||
expect(await getFixedImageObject(args)).toEqual( | ||
expect.objectContaining({ | ||
srcSet: expectedSrcSet.join(','), | ||
}), | ||
); | ||
}); | ||
it('returns a base64 image', async () => { | ||
const options = getDefaultOptions(); | ||
getPluginOptions.mockReturnValue(options); | ||
const args = getDefaultArgs(); | ||
const base64 = Buffer.from(base64ImageData).toString('base64'); | ||
const expectedBase64Image = `data:image/jpeg;base64,${base64}`; | ||
expect(await getFluidImageObject(args)).toEqual( | ||
expect.objectContaining({ base64: expectedBase64Image }), | ||
); | ||
}); | ||
it('does not fetch base64 images multiple times', async () => { | ||
const options = getDefaultOptions(); | ||
getPluginOptions.mockReturnValue(options); | ||
const args = getDefaultArgs(); | ||
for (let i = 1; i <= 3; i++) { | ||
await getFluidImageObject(args); | ||
} | ||
expect(get).toHaveBeenCalledTimes(1); | ||
}); | ||
}); |
@@ -1,2 +0,2 @@ | ||
import { NodePluginArgs, Reporter } from 'gatsby'; | ||
import { Node, NodePluginArgs, Reporter } from 'gatsby'; | ||
@@ -9,3 +9,3 @@ export function createRemoteImageNode( | ||
url: string; | ||
parentNode: any; | ||
parentNode: Node; | ||
relationshipName: string; | ||
@@ -12,0 +12,0 @@ overwriteExisting?: boolean; |
@@ -0,3 +1,3 @@ | ||
exports.createRemoteImageNode = require('./create-remote-image-node').createRemoteImageNode; | ||
exports.getFixedImageObject = require('./get-image-objects').getFixedImageObject; | ||
exports.getFluidImageObject = require('./get-image-objects').getFluidImageObject; | ||
exports.createRemoteImageNode = require('./create-remote-image-node').createRemoteImageNode; |
{ | ||
"name": "gatsby-transformer-cloudinary", | ||
"version": "1.0.1", | ||
"version": "1.1.0", | ||
"description": "Transform local files into Cloudinary-managed assets for Gatsby sites.", | ||
@@ -23,2 +23,3 @@ "main": "index.js", | ||
"cloudinary": "^1.22.0", | ||
"fast-json-stable-stringify": "^2.1.0", | ||
"fs-extra": "^9.0.1" | ||
@@ -30,4 +31,5 @@ }, | ||
"scripts": { | ||
"test": "jest" | ||
"test": "jest", | ||
"test:watch": "jest --watch" | ||
} | ||
} |
# gatsby-transformer-cloudinary | ||
Creates `CloudinaryAsset` nodes from compatible `File` nodes. The `File` nodes are uploaded to [Cloudinary](https://cloudinary.com), and the `CloudinaryAsset` responses are made up of Cloudinary URLs to transformed images in a format that‘s compatible with [`gatsby-image`](https://www.gatsbyjs.org/packages/gatsby-image/). | ||
Provides three ways to use [Cloudinary](https://cloudinary.com) with Gatsby: 1) Upload images in `File` nodes to Cloudinary. 2) Upload remote images by their URL to Cloudinary. 3) Create nodes for images that have already been uploaded to Cloudinary. | ||
Each of the three methods above create `CloudinaryAsset` nodes compatible with [`gatsby-image`](https://www.gatsbyjs.org/packages/gatsby-image/). | ||
You’ll need a [Cloudinary account](https://cloudinary.com) to use this plugin. They have a generous free tier, so for most of us this will stay free for quite a while. | ||
@@ -114,2 +116,4 @@ | ||
### Upload remote images | ||
To directly upload images to Cloudinary from remote sources, you can use the `createRemoteImageNode` function: | ||
@@ -146,2 +150,22 @@ | ||
### Use images already on Cloudinary | ||
To create GraphQL nodes for images that are already uploaded to Cloudinary, you need to create nodes containing data that describe the images on Cloudinary. For example, you might have a `post` node that has a cover photo stored on Cloudinary. The data in the post node should look something like... | ||
```js | ||
{ | ||
title: "How to beat the pandemic blues", | ||
publishedAt: "2020-07-26T21:55:13.358Z", | ||
coverPhoto: { | ||
cloudinaryAssetData: true, | ||
cloudName: "my-amazing-blog", | ||
publicId: "blue-blue-blue", | ||
originalHeight: 360, | ||
originalWidth: 820, | ||
} | ||
} | ||
``` | ||
The `coverPhoto` property in the node above will be deleted and replaced by `gatsby-transformer-cloudinary` with a `CloudinaryAsset` node that can be used with [`gatsby-image`](https://www.gatsbyjs.org/packages/gatsby-image/). This transformation will be done for any top-level properties of nodes that have `cloudinaryAssetData: true`, and values `cloudName`, `publicId`, `originalHeight`, and `originalWidth` properties. The top-level property name, `coverPhoto` in the example above, will be the name of the relationship between the parent node and the `CloudinaryAsset` node that will be created. | ||
### Plugin options | ||
@@ -290,5 +314,6 @@ | ||
| argument | type | default | description | | ||
| -------- | ----- | ------- | ------------------------------------------- | | ||
| `width` | `Int` | `400` | The width that the image should display at. | | ||
| argument | type | default | description | | ||
| -------- | ----- | ------- | ---------------------------------------------------------------------------------------------- | | ||
| `height` | `Int` | `n/a` | The height that the image should display at. If `width` is provided, then `height` is ignored. | | ||
| `width` | `Int` | `400` | The width that the image should display at. | | ||
@@ -295,0 +320,0 @@ ### Arguments for `fluid` |
@@ -96,3 +96,4 @@ const cloudinary = require('cloudinary').v2; | ||
const url = node.absolutePath; | ||
const publicId = node.name; | ||
const relativePathWithoutExtension = node.relativePath.replace(/\.[^.]*$/, ""); | ||
const publicId = relativePathWithoutExtension; | ||
const result = await exports.uploadImageToCloudinary({ | ||
@@ -99,0 +100,0 @@ url, |
@@ -1,2 +0,5 @@ | ||
const { uploadImageToCloudinary } = require('./upload'); | ||
const { | ||
uploadImageToCloudinary, | ||
uploadImageNodeToCloudinary, | ||
} = require('./upload'); | ||
@@ -123,1 +126,22 @@ jest.mock('./options'); | ||
}); | ||
describe('uploadImageNodeToCloudinary', () => { | ||
it("uses the image's relative path without the extension as the public ID", async () => { | ||
const cloudinaryUpload = jest.fn(); | ||
cloudinary.uploader.upload = cloudinaryUpload; | ||
const reporter = { info: jest.fn() }; | ||
const node = { | ||
relativePath: 'folder-name/image.name.with.dots.jpg', | ||
}; | ||
await uploadImageNodeToCloudinary({ node, reporter }); | ||
expect(cloudinaryUpload).toHaveBeenCalledWith( | ||
undefined, | ||
expect.objectContaining({ | ||
public_id: 'folder-name/image.name.with.dots', | ||
}), | ||
); | ||
}); | ||
}); |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
74584
27.11%18
12.5%1544
34.03%343
7.86%4
33.33%