📅 You're Invited: Meet the Socket team at RSAC (April 28 – May 1).RSVP

gatsby-transformer-cloudinary

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

gatsby-transformer-cloudinary - npm Package Compare versions

Comparing version

to
1.1.0

@@ -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',
}),
);
});
});