mirror of
https://github.com/BillyOutlast/posthog.com.git
synced 2026-02-04 03:11:21 +01:00
add local plugin
This commit is contained in:
52
plugins/gatsby-transformer-cloudinary/gatsby-node.js
Normal file
52
plugins/gatsby-transformer-cloudinary/gatsby-node.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const {
|
||||
initializaGlobalState,
|
||||
getCoreSupportsOnPluginInit,
|
||||
} = require('./options');
|
||||
const {
|
||||
createCloudinaryAssetType,
|
||||
createCloudinaryAssetNodes,
|
||||
} = require('./node-creation');
|
||||
|
||||
const { createGatsbyImageDataResolver } = require('./gatsby-plugin-image');
|
||||
|
||||
let coreSupportsOnPluginInit = getCoreSupportsOnPluginInit();
|
||||
|
||||
if (coreSupportsOnPluginInit === 'stable') {
|
||||
exports.onPluginInit = initializaGlobalState;
|
||||
} else if (coreSupportsOnPluginInit === 'unstable') {
|
||||
exports.unstable_onPluginInit = initializaGlobalState;
|
||||
} else {
|
||||
exports.onPreInit = initializaGlobalState;
|
||||
}
|
||||
|
||||
exports.pluginOptionsSchema = ({ Joi }) => {
|
||||
return Joi.object({
|
||||
cloudName: Joi.string(),
|
||||
apiKey: Joi.string(),
|
||||
apiSecret: Joi.string(),
|
||||
uploadFolder: Joi.string(),
|
||||
uploadSourceInstanceNames: Joi.array().items(Joi.string()),
|
||||
transformTypes: Joi.array()
|
||||
.items(Joi.string())
|
||||
.default(['CloudinaryAsset']),
|
||||
overwriteExisting: Joi.boolean().default(false),
|
||||
defaultTransformations: Joi.array()
|
||||
.items(Joi.string())
|
||||
.default(['c_fill', 'g_auto', 'q_auto']),
|
||||
});
|
||||
};
|
||||
|
||||
exports.createSchemaCustomization = (gatsbyUtils) => {
|
||||
// Type to be used for node creation
|
||||
createCloudinaryAssetType(gatsbyUtils);
|
||||
};
|
||||
|
||||
exports.createResolvers = (gatsbyUtils, pluginOptions) => {
|
||||
// Resolvers to be used with gatsby-plugin-image
|
||||
createGatsbyImageDataResolver(gatsbyUtils, pluginOptions);
|
||||
};
|
||||
|
||||
exports.onCreateNode = async (gatsbyUtils, pluginOptions) => {
|
||||
// Upload and create Cloudinary Asset nodes if applicable
|
||||
await createCloudinaryAssetNodes(gatsbyUtils, pluginOptions);
|
||||
};
|
||||
65
plugins/gatsby-transformer-cloudinary/gatsby-node.test.js
Normal file
65
plugins/gatsby-transformer-cloudinary/gatsby-node.test.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import Joi from 'joi';
|
||||
import { testPluginOptionsSchema } from 'gatsby-plugin-utils';
|
||||
import { pluginOptionsSchema } from './gatsby-node';
|
||||
|
||||
describe('pluginOptionsSchema', () => {
|
||||
test('should validate minimal correct options', async () => {
|
||||
// cloudName, apiKey, apiSecret
|
||||
// only needed if uploading
|
||||
const options = {};
|
||||
|
||||
const { isValid } = await testPluginOptionsSchema(
|
||||
pluginOptionsSchema,
|
||||
options
|
||||
);
|
||||
|
||||
expect(isValid).toBe(true);
|
||||
});
|
||||
|
||||
test('should invalidate incorrect options', async () => {
|
||||
const options = {
|
||||
cloudName: 120,
|
||||
apiKey: '',
|
||||
apiSecret: false,
|
||||
uploadFolder: ['test'],
|
||||
uploadSourceInstanceNames: 'instanceName',
|
||||
transformTypes: [123],
|
||||
overwriteExisting: 3,
|
||||
defaultTransformations: null,
|
||||
};
|
||||
|
||||
const { isValid, errors } = await testPluginOptionsSchema(
|
||||
pluginOptionsSchema,
|
||||
options
|
||||
);
|
||||
|
||||
expect(isValid).toBe(false);
|
||||
expect(errors).toEqual([
|
||||
`"cloudName" must be a string`,
|
||||
`"apiKey" is not allowed to be empty`,
|
||||
`"apiSecret" must be a string`,
|
||||
`"uploadFolder" must be a string`,
|
||||
`"uploadSourceInstanceNames" must be an array`,
|
||||
`"transformTypes[0]" must be a string`,
|
||||
`"overwriteExisting" must be a boolean`,
|
||||
`"defaultTransformations" must be an array`,
|
||||
]);
|
||||
});
|
||||
|
||||
test('should add defaults', async () => {
|
||||
const schema = pluginOptionsSchema({ Joi });
|
||||
const options = {
|
||||
cloudName: 'cloudName',
|
||||
apiKey: 'apiKey',
|
||||
apiSecret: 'apiSecret',
|
||||
};
|
||||
const { value } = schema.validate(options);
|
||||
|
||||
expect(value).toEqual({
|
||||
...options,
|
||||
transformTypes: ['CloudinaryAsset'],
|
||||
overwriteExisting: false,
|
||||
defaultTransformations: ['c_fill', 'g_auto', 'q_auto'],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
const axios = require('axios');
|
||||
const probe = require('probe-image-size');
|
||||
const { generateCloudinaryAssetUrl } = require('./generate-asset-url');
|
||||
|
||||
const dataCache = {};
|
||||
const probeCache = {};
|
||||
|
||||
const getData = async (url, options) => {
|
||||
if (!dataCache[url]) {
|
||||
dataCache[url] = axios.get(url, options);
|
||||
}
|
||||
|
||||
const { data } = await dataCache[url];
|
||||
return data;
|
||||
};
|
||||
|
||||
const probeImage = async (url) => {
|
||||
if (!probeCache[url]) {
|
||||
probeCache[url] = probe(url);
|
||||
}
|
||||
|
||||
return await probeCache[url];
|
||||
};
|
||||
|
||||
exports.getAssetAsTracedSvg = async ({ source, args }) => {
|
||||
const svgUrl = generateCloudinaryAssetUrl({
|
||||
publicId: source.publicId,
|
||||
cloudName: source.cloudName,
|
||||
format: 'svg',
|
||||
options: args,
|
||||
tracedSvg: {
|
||||
options: {
|
||||
colors: 2,
|
||||
detail: 0.3,
|
||||
despeckle: 0.1,
|
||||
},
|
||||
width: 300,
|
||||
},
|
||||
});
|
||||
const data = await getData(svgUrl);
|
||||
return `data:image/svg+xml,${encodeURIComponent(data)}`;
|
||||
};
|
||||
|
||||
exports.getAssetMetadata = async ({ source, args }) => {
|
||||
const metaDataUrl = generateCloudinaryAssetUrl({
|
||||
publicId: source.publicId,
|
||||
cloudName: source.cloudName,
|
||||
options: args,
|
||||
});
|
||||
|
||||
const result = await probeImage(metaDataUrl);
|
||||
return {
|
||||
width: result.width,
|
||||
height: result.height,
|
||||
format: result.type,
|
||||
};
|
||||
};
|
||||
|
||||
exports.getUrlAsBase64Image = async (url) => {
|
||||
const data = await getData(url, { responseType: 'arraybuffer' });
|
||||
const base64 = Buffer.from(data, 'binary').toString('base64');
|
||||
return `data:image/jpeg;base64,${base64}`;
|
||||
};
|
||||
@@ -0,0 +1,125 @@
|
||||
jest.mock('axios');
|
||||
jest.mock('probe-image-size');
|
||||
|
||||
const axios = require('axios');
|
||||
const probe = require('probe-image-size');
|
||||
const {
|
||||
getAssetMetadata,
|
||||
getUrlAsBase64Image,
|
||||
getAssetAsTracedSvg,
|
||||
} = require('./asset-data');
|
||||
|
||||
const source = {
|
||||
publicId: 'public-id',
|
||||
cloudName: 'cloud-name',
|
||||
};
|
||||
|
||||
const args = {
|
||||
transformations: ['e_grayscale'],
|
||||
};
|
||||
|
||||
describe('getAssetMetaData', () => {
|
||||
beforeEach(() => {
|
||||
probe.mockResolvedValue({
|
||||
width: 400,
|
||||
height: 300,
|
||||
type: 'jpg',
|
||||
extra: 'extra',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('fetches the correct metadata url', async () => {
|
||||
await getAssetMetadata({ source, args });
|
||||
expect(probe).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'res.cloudinary.com/cloud-name/image/upload/f_auto,e_grayscale/public-id'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the metadata', async () => {
|
||||
const metadata = await getAssetMetadata({ source, args });
|
||||
expect(metadata).toStrictEqual({ width: 400, height: 300, format: 'jpg' });
|
||||
});
|
||||
|
||||
it('to cache responses', async () => {
|
||||
await getAssetMetadata({ source, args: {} });
|
||||
await getAssetMetadata({ source, args: {} });
|
||||
await getAssetMetadata({ source, args: { chained: ['t_lwj'] } });
|
||||
await getAssetMetadata({ source, args: { chained: ['t_lwj'] } });
|
||||
expect(probe).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUrlAsBase64Image', () => {
|
||||
beforeEach(() => {
|
||||
axios.get.mockResolvedValue({
|
||||
data: '49 27 6d 20 61 20 73 74 72 69 6e 67 21',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('fetches an arraybuffer from the url', async () => {
|
||||
await getUrlAsBase64Image('example-url');
|
||||
expect(axios.get).toHaveBeenCalledWith(`example-url`, {
|
||||
responseType: 'arraybuffer',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the base64 data url', async () => {
|
||||
const base64 = await getUrlAsBase64Image('example-url');
|
||||
expect(base64).toBe(
|
||||
'data:image/jpeg;base64,NDkgMjcgNmQgMjAgNjEgMjAgNzMgNzQgNzIgNjkgNmUgNjcgMjE='
|
||||
);
|
||||
});
|
||||
|
||||
it('to cache responses', async () => {
|
||||
await getUrlAsBase64Image('one-url');
|
||||
await getUrlAsBase64Image('one-url');
|
||||
await getUrlAsBase64Image('another-url');
|
||||
await getUrlAsBase64Image('another-url');
|
||||
expect(axios.get).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAssetAsTracedSvg', () => {
|
||||
beforeEach(() => {
|
||||
axios.get.mockResolvedValue({
|
||||
data: '<svg path.....>',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('fetches the correct url', async () => {
|
||||
await getAssetAsTracedSvg({ source, args });
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'res.cloudinary.com/cloud-name/image/upload/f_svg,e_grayscale/e_vectorize:colors:2:detail:0.3:despeckle:0.1,w_300/public-id'
|
||||
),
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the svg as data url', async () => {
|
||||
const svg = await getAssetAsTracedSvg({ source, args });
|
||||
expect(svg).toBe('data:image/svg+xml,%3Csvg%20path.....%3E');
|
||||
});
|
||||
|
||||
it('to cache responses', async () => {
|
||||
await getAssetAsTracedSvg({ source, args: {} });
|
||||
await getAssetAsTracedSvg({ source, args: {} });
|
||||
await getAssetAsTracedSvg({ source, args: { chained: ['t_lwj'] } });
|
||||
await getAssetAsTracedSvg({ source, args: { chained: ['t_lwj'] } });
|
||||
expect(axios.get).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
const cloudinary = require('cloudinary').v2;
|
||||
const pluginPkg = require('../package.json');
|
||||
const gatsbyPkg = require('gatsby/package.json');
|
||||
|
||||
const SDK_CODE = 'X';
|
||||
const SDK_SEMVER = pluginPkg.version;
|
||||
const TECH_VERSION = gatsbyPkg.version;
|
||||
|
||||
const generateTransformations = ({ width, height, format, options = {} }) => {
|
||||
return [
|
||||
{
|
||||
fetch_format: format || 'auto',
|
||||
width: width,
|
||||
height: height,
|
||||
raw_transformation: (options.transformations || []).join(','),
|
||||
},
|
||||
...(options.chained || []).map((transformations) => {
|
||||
return { raw_transformation: transformations };
|
||||
}),
|
||||
];
|
||||
};
|
||||
|
||||
const generateTracedSVGTransformation = ({ options, width }) => {
|
||||
const effectOptions = Object.keys(options).reduce((acc, key) => {
|
||||
const value = options[key];
|
||||
return value ? acc + `:${key}:${value}` : acc;
|
||||
}, 'vectorize');
|
||||
|
||||
return {
|
||||
effect: effectOptions,
|
||||
width: width,
|
||||
};
|
||||
};
|
||||
|
||||
// Create Cloudinary image URL with transformations.
|
||||
exports.generateCloudinaryAssetUrl = ({
|
||||
publicId,
|
||||
cloudName,
|
||||
width,
|
||||
height,
|
||||
format,
|
||||
options = {},
|
||||
flags,
|
||||
tracedSvg,
|
||||
}) => {
|
||||
const transformation = generateTransformations({
|
||||
width,
|
||||
height,
|
||||
format,
|
||||
options,
|
||||
});
|
||||
|
||||
if (tracedSvg) {
|
||||
transformation.push(generateTracedSVGTransformation(tracedSvg));
|
||||
}
|
||||
|
||||
const url = cloudinary.url(publicId, {
|
||||
cloud_name: cloudName,
|
||||
secure: options.secure,
|
||||
transformation: transformation,
|
||||
flags: flags,
|
||||
urlAnalytics: true,
|
||||
sdkCode: SDK_CODE,
|
||||
sdkSemver: SDK_SEMVER,
|
||||
techVersion: TECH_VERSION,
|
||||
});
|
||||
|
||||
return url;
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
jest.mock('../package.json', () => ({
|
||||
version: '0.1.2',
|
||||
}));
|
||||
jest.mock('gatsby/package.json', () => ({
|
||||
version: '0.5.3',
|
||||
}));
|
||||
|
||||
const { generateCloudinaryAssetUrl } = require('./generate-asset-url');
|
||||
|
||||
const ANALYTICS_CODE = 'AXE6EH00';
|
||||
|
||||
describe('generateCloudinaryAssetUrl', () => {
|
||||
const asset = {
|
||||
publicId: 'public-id',
|
||||
cloudName: 'cloud-name',
|
||||
width: 400,
|
||||
height: 600,
|
||||
format: 'jpg',
|
||||
};
|
||||
|
||||
it('generates correct Cloudinary url when no options', () => {
|
||||
const url = generateCloudinaryAssetUrl(asset);
|
||||
expect(url).toBe(
|
||||
`http://res.cloudinary.com/cloud-name/image/upload/f_jpg,h_600,w_400/public-id?_a=${ANALYTICS_CODE}`
|
||||
);
|
||||
});
|
||||
|
||||
it('generates correct Cloudinary url with transformations option', () => {
|
||||
const url = generateCloudinaryAssetUrl({
|
||||
...asset,
|
||||
options: {
|
||||
transformations: ['e_grayscale', 'e_pixelate'],
|
||||
},
|
||||
});
|
||||
expect(url).toBe(
|
||||
`http://res.cloudinary.com/cloud-name/image/upload/f_jpg,h_600,w_400,e_grayscale,e_pixelate/public-id?_a=${ANALYTICS_CODE}`
|
||||
);
|
||||
});
|
||||
|
||||
it('generates correct Cloudinary url with chained option', () => {
|
||||
const url = generateCloudinaryAssetUrl({
|
||||
...asset,
|
||||
options: {
|
||||
chained: ['t_lwj', 'e_pixelate'],
|
||||
},
|
||||
});
|
||||
expect(url).toBe(
|
||||
`http://res.cloudinary.com/cloud-name/image/upload/f_jpg,h_600,w_400/t_lwj/e_pixelate/public-id?_a=${ANALYTICS_CODE}`
|
||||
);
|
||||
});
|
||||
|
||||
it('generates correct Cloudinary url with scure option set to true', () => {
|
||||
const url = generateCloudinaryAssetUrl({
|
||||
...asset,
|
||||
options: {
|
||||
secure: true,
|
||||
},
|
||||
});
|
||||
expect(url).toBe(
|
||||
`https://res.cloudinary.com/cloud-name/image/upload/f_jpg,h_600,w_400/public-id?_a=${ANALYTICS_CODE}`
|
||||
);
|
||||
});
|
||||
|
||||
it('generates correct Cloudinary url in traced SVG mode', () => {
|
||||
const url = generateCloudinaryAssetUrl({
|
||||
...asset,
|
||||
tracedSvg: {
|
||||
options: {
|
||||
colors: 2,
|
||||
detail: 0.3,
|
||||
despeckle: 0.1,
|
||||
},
|
||||
width: 300,
|
||||
},
|
||||
});
|
||||
expect(url).toBe(
|
||||
`http://res.cloudinary.com/cloud-name/image/upload/f_jpg,h_600,w_400/e_vectorize:colors:2:detail:0.3:despeckle:0.1,w_300/public-id?_a=${ANALYTICS_CODE}`
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
const { createGatsbyPluginImageResolver } = require('./resolvers');
|
||||
|
||||
exports.createGatsbyImageDataResolver = (gatsbyUtils, pluginOptions) => {
|
||||
const { createResolvers } = gatsbyUtils;
|
||||
const { transformTypes } = pluginOptions;
|
||||
const gatsbyImageResolver = createGatsbyPluginImageResolver(
|
||||
gatsbyUtils,
|
||||
pluginOptions
|
||||
);
|
||||
|
||||
if (gatsbyImageResolver) {
|
||||
const resolvers = {};
|
||||
// Make the resolver nullable, createGatsbyPluginImageResolver sets the type to 'GatsbyImageData!'
|
||||
gatsbyImageResolver.type = 'GatsbyImageData';
|
||||
|
||||
transformTypes.forEach((type) => {
|
||||
// Add gatsbyImageData resolver
|
||||
// to all types that should be transformed
|
||||
resolvers[type] = {
|
||||
gatsbyImageData: gatsbyImageResolver,
|
||||
};
|
||||
});
|
||||
|
||||
createResolvers(resolvers);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,175 @@
|
||||
const {
|
||||
getLowResolutionImageURL,
|
||||
generateImageData,
|
||||
} = require('gatsby-plugin-image');
|
||||
|
||||
const { generateCloudinaryAssetUrl } = require('./generate-asset-url');
|
||||
const {
|
||||
getAssetAsTracedSvg,
|
||||
getUrlAsBase64Image,
|
||||
getAssetMetadata,
|
||||
} = require('./asset-data');
|
||||
const { Joi } = require('gatsby-plugin-utils/joi');
|
||||
|
||||
const { resolverReporter } = require('./resolver-reporter');
|
||||
|
||||
const generateCloudinaryAssetSource = (
|
||||
filename,
|
||||
width,
|
||||
height,
|
||||
format,
|
||||
_fit,
|
||||
options
|
||||
) => {
|
||||
const [cloudName, publicId] = filename.split('>>>');
|
||||
const cloudinarySrcUrl = generateCloudinaryAssetUrl({
|
||||
cloudName: cloudName,
|
||||
publicId: publicId,
|
||||
width,
|
||||
height,
|
||||
format,
|
||||
options,
|
||||
});
|
||||
|
||||
const imageSource = {
|
||||
src: cloudinarySrcUrl,
|
||||
width: width,
|
||||
height: height,
|
||||
format: format,
|
||||
};
|
||||
|
||||
return imageSource;
|
||||
};
|
||||
|
||||
const generateMetadata = async (source, args, transformType, reporter) => {
|
||||
const schema = Joi.object({
|
||||
width: Joi.number().positive().required(),
|
||||
height: Joi.number().positive().required(),
|
||||
format: Joi.string().default('auto'),
|
||||
}).required();
|
||||
|
||||
const originalMetadata = {
|
||||
width: source.originalWidth,
|
||||
height: source.originalHeight,
|
||||
format: source.originalFormat,
|
||||
};
|
||||
|
||||
const { value, error } = schema.validate(originalMetadata);
|
||||
|
||||
if (!error) {
|
||||
// Original metadata is valid,
|
||||
// use validated value
|
||||
return value;
|
||||
}
|
||||
|
||||
try {
|
||||
// Lacking metadata, so let's fetch it
|
||||
reporter.verbose(
|
||||
`[gatsby-transformer-cloudinary] Missing metadata fields on ${transformType}: cloudName=${source.cloudName}, publicId=${source.publicId} >>> To save on network requests add originalWidth, originalHeight and originalFormat to ${transformType}`
|
||||
);
|
||||
|
||||
const fetchedMetadata = await getAssetMetadata({ source, args });
|
||||
const { value, error } = schema.validate(fetchedMetadata);
|
||||
|
||||
if (!error) {
|
||||
// Fetched metadata is valid,
|
||||
// use validated value
|
||||
return value;
|
||||
} else {
|
||||
// Fetched metadata is not valid
|
||||
reporter.verbose(
|
||||
`[gatsby-transformer-cloudinary] Invalid fetched metadata for ${transformType}: cloudName=${source.cloudName}, publicId=${source.publicId} >>> ${error.message}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
// Error fetching
|
||||
reporter.verbose(
|
||||
`[gatsby-transformer-cloudinary] Could not fetch metadata for ${transformType}: cloudName=${source.cloudName}, publicId=${source.publicId} >>> ${error.message}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Make it testable
|
||||
exports._generateCloudinaryAssetSource = generateCloudinaryAssetSource;
|
||||
|
||||
exports.createResolveCloudinaryAssetData =
|
||||
(gatsbyUtils) => async (source, args, _context, info) => {
|
||||
let { reporter } = gatsbyUtils;
|
||||
reporter = resolverReporter({ reporter, logLevel: args.logLevel });
|
||||
const transformType = info.parentType || 'UnknownTransformType';
|
||||
|
||||
const schema = Joi.object({
|
||||
cloudName: Joi.string().required(),
|
||||
publicId: Joi.string().required(),
|
||||
}).required();
|
||||
|
||||
const { error } = schema.validate(source, {
|
||||
allowUnknown: true,
|
||||
abortEarly: false,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
if (error.details.length < 2 && error.details[0].path.length > 0) {
|
||||
reporter.warn(
|
||||
`[gatsby-transformer-cloudinary] Missing required field on ${transformType}: cloudName=${source?.cloudName}, publicId=${source?.publicId} >>> gatsbyImageData will resolve to null`
|
||||
);
|
||||
} else {
|
||||
reporter.verbose(
|
||||
`[gatsby-transformer-cloudinary] Missing cloudName and publicId on ${transformType} >>> gatsbyImageData will resolve to null`
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const metadata = await generateMetadata(
|
||||
source,
|
||||
args,
|
||||
transformType,
|
||||
reporter
|
||||
);
|
||||
|
||||
if (!metadata) {
|
||||
reporter.warn(
|
||||
`[gatsby-transformer-cloudinary] No metadata for ${transformType}: cloudName=${source.cloudName}, publicId=${source.publicId} >>> gatsbyImageData will resolve to null`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const assetDataArgs = {
|
||||
...args,
|
||||
filename: source.cloudName + '>>>' + source.publicId,
|
||||
// Passing the plugin name allows for better error messages
|
||||
pluginName: `gatsby-transformer-cloudinary`,
|
||||
sourceMetadata: metadata,
|
||||
generateImageSource: generateCloudinaryAssetSource,
|
||||
options: args,
|
||||
};
|
||||
|
||||
try {
|
||||
if (args.placeholder === 'blurred') {
|
||||
if (source.defaultBase64) {
|
||||
assetDataArgs.placeholderURL = source.defaultBase64;
|
||||
} else {
|
||||
const lowResolutionUrl = getLowResolutionImageURL(assetDataArgs);
|
||||
const base64 = await getUrlAsBase64Image(lowResolutionUrl);
|
||||
assetDataArgs.placeholderURL = base64;
|
||||
}
|
||||
} else if (args.placeholder === 'tracedSVG') {
|
||||
if (source.defaultTracedSVG) {
|
||||
assetDataArgs.placeholderURL = source.defaultTracedSVG;
|
||||
} else {
|
||||
const tracedSvg = await getAssetAsTracedSvg({ source, args });
|
||||
assetDataArgs.placeholderURL = tracedSvg;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
reporter.error(
|
||||
`[gatsby-transformer-cloudinary] Could not generate placeholder (${args.placeholder}) for ${source.cloudName} > ${source.publicId}: ${error.message}`
|
||||
);
|
||||
}
|
||||
|
||||
return generateImageData(assetDataArgs);
|
||||
};
|
||||
@@ -0,0 +1,470 @@
|
||||
jest.mock('gatsby-plugin-image');
|
||||
jest.mock('./asset-data');
|
||||
|
||||
const gatsbyUtilsMocks = {
|
||||
reporter: {
|
||||
panic: jest.fn(),
|
||||
error: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
info: jest.fn(),
|
||||
verbose: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
const {
|
||||
getLowResolutionImageURL,
|
||||
generateImageData,
|
||||
} = require('gatsby-plugin-image');
|
||||
|
||||
const {
|
||||
getAssetAsTracedSvg,
|
||||
getAssetMetadata,
|
||||
getUrlAsBase64Image,
|
||||
} = require('./asset-data');
|
||||
|
||||
const {
|
||||
_generateCloudinaryAssetSource,
|
||||
createResolveCloudinaryAssetData,
|
||||
} = require('./resolve-asset');
|
||||
|
||||
const resolveCloudinaryAssetData =
|
||||
createResolveCloudinaryAssetData(gatsbyUtilsMocks);
|
||||
|
||||
describe('generateCloudinaryAssetSource', () => {
|
||||
const filename = 'cloud-name>>>public-id';
|
||||
const width = 300;
|
||||
const height = 500;
|
||||
const format = 'jpg';
|
||||
const fit = undefined;
|
||||
const options = {
|
||||
chained: ['t_lwj'],
|
||||
secure: true,
|
||||
};
|
||||
|
||||
it('generated correct source data', () => {
|
||||
const result = _generateCloudinaryAssetSource(
|
||||
filename,
|
||||
width,
|
||||
height,
|
||||
format,
|
||||
fit,
|
||||
options
|
||||
);
|
||||
|
||||
expect(result.src).toContain(
|
||||
'https://res.cloudinary.com/cloud-name/image/upload/f_jpg,h_500,w_300/t_lwj/public-id'
|
||||
);
|
||||
expect(result.width).toBe(width);
|
||||
expect(result.height).toBe(height);
|
||||
expect(result.format).toBe(format);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveCloudinaryAssetData', () => {
|
||||
const sourceWithMetadata = {
|
||||
publicId: 'public-id',
|
||||
cloudName: 'cloud-name',
|
||||
originalWidth: '600',
|
||||
originalHeight: '300',
|
||||
originalFormat: 'jpg',
|
||||
};
|
||||
|
||||
const sourceWithoutFormat = {
|
||||
publicId: 'public-id',
|
||||
cloudName: 'cloud-name',
|
||||
originalWidth: '600',
|
||||
originalHeight: '300',
|
||||
};
|
||||
|
||||
const sourceWithoutMeta = {
|
||||
publicId: 'public-id',
|
||||
cloudName: 'cloud-name',
|
||||
};
|
||||
|
||||
const context = {}; // Never used
|
||||
const info = {};
|
||||
|
||||
beforeEach(() => {
|
||||
getAssetMetadata.mockResolvedValue({
|
||||
width: 100,
|
||||
height: 200,
|
||||
format: 'gif',
|
||||
});
|
||||
getUrlAsBase64Image.mockResolvedValue('base64DataUrl');
|
||||
getAssetAsTracedSvg.mockResolvedValue('svgDataUrl');
|
||||
getLowResolutionImageURL.mockResolvedValue('low-resultion-url');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('calls gatsby-plugin-image -> generateImageData once', async () => {
|
||||
const args = {};
|
||||
await resolveCloudinaryAssetData(sourceWithMetadata, args, context, info);
|
||||
expect(generateImageData).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls gatsby-plugin-image -> generateImageData with correct data', async () => {
|
||||
const args = { transformations: ['e_grayscale'] };
|
||||
await resolveCloudinaryAssetData(sourceWithMetadata, args, context, info);
|
||||
await resolveCloudinaryAssetData(sourceWithoutFormat, args, context, info);
|
||||
expect(getAssetMetadata).toBeCalledTimes(0);
|
||||
expect(generateImageData).toHaveBeenNthCalledWith(1, {
|
||||
filename: 'cloud-name>>>public-id',
|
||||
generateImageSource: _generateCloudinaryAssetSource,
|
||||
options: {
|
||||
transformations: ['e_grayscale'],
|
||||
},
|
||||
pluginName: 'gatsby-transformer-cloudinary',
|
||||
sourceMetadata: {
|
||||
format: 'jpg',
|
||||
height: 300,
|
||||
width: 600,
|
||||
},
|
||||
transformations: ['e_grayscale'],
|
||||
});
|
||||
expect(generateImageData).toHaveBeenNthCalledWith(2, {
|
||||
filename: 'cloud-name>>>public-id',
|
||||
generateImageSource: _generateCloudinaryAssetSource,
|
||||
options: {
|
||||
transformations: ['e_grayscale'],
|
||||
},
|
||||
pluginName: 'gatsby-transformer-cloudinary',
|
||||
sourceMetadata: {
|
||||
format: 'auto',
|
||||
height: 300,
|
||||
width: 600,
|
||||
},
|
||||
transformations: ['e_grayscale'],
|
||||
});
|
||||
});
|
||||
|
||||
it('fetches metadata when not present on source', async () => {
|
||||
const args = {};
|
||||
await resolveCloudinaryAssetData(sourceWithMetadata, args, context, info);
|
||||
await resolveCloudinaryAssetData(sourceWithoutMeta, args, context, info);
|
||||
// getAssetMetadata should only be called for sourceWithoutMeta
|
||||
expect(getAssetMetadata).toBeCalledTimes(1);
|
||||
expect(gatsbyUtilsMocks.reporter.verbose).toBeCalledTimes(1);
|
||||
// gatsby-plugin-image -> generateImageData should be called for both
|
||||
expect(generateImageData).toBeCalledTimes(2);
|
||||
expect(generateImageData).toHaveBeenNthCalledWith(2, {
|
||||
filename: 'cloud-name>>>public-id',
|
||||
generateImageSource: _generateCloudinaryAssetSource,
|
||||
options: {},
|
||||
pluginName: 'gatsby-transformer-cloudinary',
|
||||
sourceMetadata: {
|
||||
format: 'gif',
|
||||
height: 200,
|
||||
width: 100,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('fetches and adds correct "blurred" placeholder', async () => {
|
||||
const args = { placeholder: 'blurred' };
|
||||
await resolveCloudinaryAssetData(sourceWithMetadata, args, context, info);
|
||||
|
||||
expect(getLowResolutionImageURL).toBeCalledTimes(1);
|
||||
expect(getUrlAsBase64Image).toBeCalledTimes(1);
|
||||
expect(generateImageData).toHaveBeenCalledWith({
|
||||
filename: 'cloud-name>>>public-id',
|
||||
generateImageSource: _generateCloudinaryAssetSource,
|
||||
options: { placeholder: 'blurred' },
|
||||
pluginName: 'gatsby-transformer-cloudinary',
|
||||
sourceMetadata: {
|
||||
format: 'jpg',
|
||||
height: 300,
|
||||
width: 600,
|
||||
},
|
||||
placeholderURL: 'base64DataUrl',
|
||||
placeholder: 'blurred',
|
||||
});
|
||||
});
|
||||
|
||||
it('fetches and adds correct "tracedSVG" placeholder', async () => {
|
||||
const args = { placeholder: 'tracedSVG' };
|
||||
await resolveCloudinaryAssetData(sourceWithMetadata, args, context, info);
|
||||
|
||||
expect(getAssetAsTracedSvg).toBeCalledTimes(1);
|
||||
expect(generateImageData).toHaveBeenCalledWith({
|
||||
filename: 'cloud-name>>>public-id',
|
||||
generateImageSource: _generateCloudinaryAssetSource,
|
||||
options: { placeholder: 'tracedSVG' },
|
||||
pluginName: 'gatsby-transformer-cloudinary',
|
||||
sourceMetadata: {
|
||||
format: 'jpg',
|
||||
height: 300,
|
||||
width: 600,
|
||||
},
|
||||
placeholderURL: 'svgDataUrl',
|
||||
placeholder: 'tracedSVG',
|
||||
});
|
||||
});
|
||||
|
||||
describe('when unconforming source', () => {
|
||||
it('calls reporter.verbose and returns null for empty source', async () => {
|
||||
const source = {};
|
||||
const args = {};
|
||||
const result = await resolveCloudinaryAssetData(
|
||||
source,
|
||||
args,
|
||||
context,
|
||||
info
|
||||
);
|
||||
expect(generateImageData).toBeCalledTimes(0);
|
||||
expect(gatsbyUtilsMocks.reporter.verbose).toBeCalledTimes(1);
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
describe('returns null for weird source', () => {
|
||||
it('calls reporter.verbose when undefined log level', async () => {
|
||||
const source = {
|
||||
one: 'thing',
|
||||
another: 'thang',
|
||||
};
|
||||
const args = {};
|
||||
const result = await resolveCloudinaryAssetData(
|
||||
source,
|
||||
args,
|
||||
context,
|
||||
info
|
||||
);
|
||||
expect(generateImageData).toBeCalledTimes(0);
|
||||
expect(gatsbyUtilsMocks.reporter.verbose).toBeCalledTimes(1);
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
it('calls reporter.verbose when log level = "verbose"', async () => {
|
||||
const source = {
|
||||
one: 'thing',
|
||||
another: 'thang',
|
||||
};
|
||||
const args = {
|
||||
logLevel: 'verbose',
|
||||
};
|
||||
const result = await resolveCloudinaryAssetData(
|
||||
source,
|
||||
args,
|
||||
context,
|
||||
info
|
||||
);
|
||||
expect(generateImageData).toBeCalledTimes(0);
|
||||
expect(gatsbyUtilsMocks.reporter.verbose).toBeCalledTimes(1);
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
it('does not call reporter.verbose when log level = "warn"', async () => {
|
||||
const source = {
|
||||
one: 'thing',
|
||||
another: 'thang',
|
||||
};
|
||||
const args = {
|
||||
logLevel: 'warn',
|
||||
};
|
||||
const result = await resolveCloudinaryAssetData(
|
||||
source,
|
||||
args,
|
||||
context,
|
||||
info
|
||||
);
|
||||
expect(generateImageData).toBeCalledTimes(0);
|
||||
expect(gatsbyUtilsMocks.reporter.verbose).toBeCalledTimes(0);
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
it('calls reporter.verbose and returns null for null source', async () => {
|
||||
const source = null;
|
||||
const args = {};
|
||||
const result = await resolveCloudinaryAssetData(
|
||||
source,
|
||||
args,
|
||||
context,
|
||||
info
|
||||
);
|
||||
expect(generateImageData).toBeCalledTimes(0);
|
||||
expect(gatsbyUtilsMocks.reporter.verbose).toBeCalledTimes(1);
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
it('calls reporter.verbose and returns null for undefined source', async () => {
|
||||
const source = undefined;
|
||||
const args = {};
|
||||
const result = await resolveCloudinaryAssetData(
|
||||
source,
|
||||
args,
|
||||
context,
|
||||
info
|
||||
);
|
||||
expect(generateImageData).toBeCalledTimes(0);
|
||||
expect(gatsbyUtilsMocks.reporter.verbose).toBeCalledTimes(1);
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
describe('returns null for missing data', () => {
|
||||
it('returns null for missing data when log level is undefined', async () => {
|
||||
const source = {
|
||||
publicId: 'publicId',
|
||||
one: 'thing',
|
||||
};
|
||||
const args = {};
|
||||
const result = await resolveCloudinaryAssetData(
|
||||
source,
|
||||
args,
|
||||
context,
|
||||
info
|
||||
);
|
||||
expect(generateImageData).toBeCalledTimes(0);
|
||||
expect(gatsbyUtilsMocks.reporter.warn).toBeCalledTimes(1);
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
it('does not call reporter.warn when log level = "error"', async () => {
|
||||
const source = {
|
||||
publicId: 'publicId',
|
||||
one: 'thing',
|
||||
};
|
||||
const args = {
|
||||
logLevel: 'error',
|
||||
};
|
||||
const result = await resolveCloudinaryAssetData(
|
||||
source,
|
||||
args,
|
||||
context,
|
||||
info
|
||||
);
|
||||
expect(generateImageData).toBeCalledTimes(0);
|
||||
expect(gatsbyUtilsMocks.reporter.warn).toBeCalledTimes(0);
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when fetched asset data is invalid', () => {
|
||||
beforeEach(() => {
|
||||
getAssetMetadata.mockResolvedValue({
|
||||
width: 100,
|
||||
// height: 200,
|
||||
// format: 'gif',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('calls reporter.warn on invalid metadata and returns null', async () => {
|
||||
const args = {};
|
||||
const result = await resolveCloudinaryAssetData(
|
||||
sourceWithoutMeta,
|
||||
args,
|
||||
context,
|
||||
info
|
||||
);
|
||||
expect(getAssetMetadata).toBeCalledTimes(1);
|
||||
expect(generateImageData).toBeCalledTimes(0);
|
||||
expect(gatsbyUtilsMocks.reporter.warn).toBeCalledTimes(1);
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when fetched asset data is undefined', () => {
|
||||
beforeEach(() => {
|
||||
getAssetMetadata.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('calls reporter.warn on undefined metadata and returns null', async () => {
|
||||
const args = {};
|
||||
const result = await resolveCloudinaryAssetData(
|
||||
sourceWithoutMeta,
|
||||
args,
|
||||
context,
|
||||
info
|
||||
);
|
||||
expect(getAssetMetadata).toBeCalledTimes(1);
|
||||
expect(generateImageData).toBeCalledTimes(0);
|
||||
expect(gatsbyUtilsMocks.reporter.warn).toBeCalledTimes(1);
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when error fetching asset data', () => {
|
||||
beforeEach(() => {
|
||||
getAssetMetadata.mockImplementation(() => {
|
||||
throw new Error();
|
||||
});
|
||||
getUrlAsBase64Image.mockImplementation(() => {
|
||||
throw new Error();
|
||||
});
|
||||
getAssetAsTracedSvg.mockImplementation(() => {
|
||||
throw new Error();
|
||||
});
|
||||
getLowResolutionImageURL.mockResolvedValue('low-resultion-url');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('calls reporter.error on metadata error and returns null', async () => {
|
||||
const args = {};
|
||||
const result = await resolveCloudinaryAssetData(
|
||||
sourceWithoutMeta,
|
||||
args,
|
||||
context,
|
||||
info
|
||||
);
|
||||
expect(getAssetMetadata).toBeCalledTimes(1);
|
||||
expect(generateImageData).toBeCalledTimes(0);
|
||||
expect(gatsbyUtilsMocks.reporter.warn).toBeCalledTimes(1);
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
it('calls reporter.error on blurred placeholder error', async () => {
|
||||
const args = { placeholder: 'blurred' };
|
||||
await resolveCloudinaryAssetData(sourceWithMetadata, args, context, info);
|
||||
expect(getLowResolutionImageURL).toBeCalledTimes(1);
|
||||
expect(getUrlAsBase64Image).toBeCalledTimes(1);
|
||||
expect(gatsbyUtilsMocks.reporter.error).toBeCalledTimes(1);
|
||||
expect(generateImageData).toHaveBeenCalledWith({
|
||||
filename: 'cloud-name>>>public-id',
|
||||
generateImageSource: _generateCloudinaryAssetSource,
|
||||
options: { placeholder: 'blurred' },
|
||||
pluginName: 'gatsby-transformer-cloudinary',
|
||||
sourceMetadata: {
|
||||
format: 'jpg',
|
||||
height: 300,
|
||||
width: 600,
|
||||
},
|
||||
// placeholderURL: 'base64DataUrl',
|
||||
placeholder: 'blurred',
|
||||
});
|
||||
});
|
||||
|
||||
it('calls reporter.error on tracedSVG placeholder error', async () => {
|
||||
const args = { placeholder: 'tracedSVG' };
|
||||
await resolveCloudinaryAssetData(sourceWithMetadata, args, context, info);
|
||||
expect(getAssetAsTracedSvg).toBeCalledTimes(1);
|
||||
expect(gatsbyUtilsMocks.reporter.error).toBeCalledTimes(1);
|
||||
expect(generateImageData).toHaveBeenCalledWith({
|
||||
filename: 'cloud-name>>>public-id',
|
||||
generateImageSource: _generateCloudinaryAssetSource,
|
||||
options: { placeholder: 'tracedSVG' },
|
||||
pluginName: 'gatsby-transformer-cloudinary',
|
||||
sourceMetadata: {
|
||||
format: 'jpg',
|
||||
height: 300,
|
||||
width: 600,
|
||||
},
|
||||
// placeholderURL: 'svgDataUrl',
|
||||
placeholder: 'tracedSVG',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
const LEVEL = {
|
||||
verbose: 0,
|
||||
info: 1,
|
||||
warn: 2,
|
||||
error: 3,
|
||||
panic: 4,
|
||||
panicOnBuild: 4,
|
||||
};
|
||||
|
||||
exports.resolverReporter = ({ reporter, logLevel }) => {
|
||||
const log = (level, message, ...rest) => {
|
||||
if (!logLevel || LEVEL[level] >= LEVEL[logLevel]) {
|
||||
reporter[level](message, ...rest);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
verbose: (...args) => log('verbose', ...args),
|
||||
info: (...args) => log('info', ...args),
|
||||
warn: (...args) => log('warn', ...args),
|
||||
error: (...args) => log('error', ...args),
|
||||
panic: (...args) => log('panic', ...args),
|
||||
panicOnBuild: (...args) => log('panicOnBuild', ...args),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,143 @@
|
||||
const { resolverReporter } = require('./resolver-reporter');
|
||||
|
||||
const gatsbyUtilsMocks = {
|
||||
reporter: {
|
||||
panicOnBuild: jest.fn(),
|
||||
panic: jest.fn(),
|
||||
error: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
info: jest.fn(),
|
||||
verbose: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
describe('resolverReporter', () => {
|
||||
it('reports only panic', () => {
|
||||
const reporter = resolverReporter({
|
||||
reporter: gatsbyUtilsMocks.reporter,
|
||||
logLevel: 'panic',
|
||||
});
|
||||
|
||||
reporter.panicOnBuild('message');
|
||||
reporter.panic('message');
|
||||
reporter.error('message');
|
||||
reporter.warn('message');
|
||||
reporter.info('message');
|
||||
reporter.verbose('message');
|
||||
|
||||
expect(gatsbyUtilsMocks.reporter.panic).toBeCalledWith('message');
|
||||
expect(gatsbyUtilsMocks.reporter.panicOnBuild).toBeCalledWith('message');
|
||||
expect(gatsbyUtilsMocks.reporter.error).toBeCalledTimes(0);
|
||||
expect(gatsbyUtilsMocks.reporter.warn).toBeCalledTimes(0);
|
||||
expect(gatsbyUtilsMocks.reporter.info).toBeCalledTimes(0);
|
||||
expect(gatsbyUtilsMocks.reporter.verbose).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
it('reports only errors and above', () => {
|
||||
const reporter = resolverReporter({
|
||||
reporter: gatsbyUtilsMocks.reporter,
|
||||
logLevel: 'error',
|
||||
});
|
||||
|
||||
reporter.panicOnBuild('message');
|
||||
reporter.panic('message');
|
||||
reporter.error('message');
|
||||
reporter.warn('message');
|
||||
reporter.info('message');
|
||||
reporter.verbose('message');
|
||||
|
||||
expect(gatsbyUtilsMocks.reporter.panic).toBeCalledWith('message');
|
||||
expect(gatsbyUtilsMocks.reporter.panicOnBuild).toBeCalledWith('message');
|
||||
expect(gatsbyUtilsMocks.reporter.error).toBeCalledWith('message');
|
||||
expect(gatsbyUtilsMocks.reporter.warn).toBeCalledTimes(0);
|
||||
expect(gatsbyUtilsMocks.reporter.info).toBeCalledTimes(0);
|
||||
expect(gatsbyUtilsMocks.reporter.verbose).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
it('reports only warnings and above', () => {
|
||||
const reporter = resolverReporter({
|
||||
reporter: gatsbyUtilsMocks.reporter,
|
||||
logLevel: 'warn',
|
||||
});
|
||||
|
||||
reporter.panicOnBuild('message');
|
||||
reporter.panic('message');
|
||||
reporter.error('message');
|
||||
reporter.warn('message');
|
||||
reporter.info('message');
|
||||
reporter.verbose('message');
|
||||
|
||||
expect(gatsbyUtilsMocks.reporter.panic).toBeCalledWith('message');
|
||||
expect(gatsbyUtilsMocks.reporter.panicOnBuild).toBeCalledWith('message');
|
||||
expect(gatsbyUtilsMocks.reporter.error).toBeCalledWith('message');
|
||||
expect(gatsbyUtilsMocks.reporter.warn).toBeCalledWith('message');
|
||||
expect(gatsbyUtilsMocks.reporter.info).toBeCalledTimes(0);
|
||||
expect(gatsbyUtilsMocks.reporter.verbose).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
it('reports only info and above', () => {
|
||||
const reporter = resolverReporter({
|
||||
reporter: gatsbyUtilsMocks.reporter,
|
||||
logLevel: 'info',
|
||||
});
|
||||
|
||||
reporter.panicOnBuild('message');
|
||||
reporter.panic('message');
|
||||
reporter.error('message');
|
||||
reporter.warn('message');
|
||||
reporter.info('message');
|
||||
reporter.verbose('message');
|
||||
|
||||
expect(gatsbyUtilsMocks.reporter.panic).toBeCalledWith('message');
|
||||
expect(gatsbyUtilsMocks.reporter.panicOnBuild).toBeCalledWith('message');
|
||||
expect(gatsbyUtilsMocks.reporter.error).toBeCalledWith('message');
|
||||
expect(gatsbyUtilsMocks.reporter.warn).toBeCalledWith('message');
|
||||
expect(gatsbyUtilsMocks.reporter.info).toBeCalledWith('message');
|
||||
expect(gatsbyUtilsMocks.reporter.verbose).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
describe('reports all', () => {
|
||||
it('when log level is verbose', () => {
|
||||
const reporter = resolverReporter({
|
||||
reporter: gatsbyUtilsMocks.reporter,
|
||||
logLevel: 'verbose',
|
||||
});
|
||||
|
||||
reporter.panicOnBuild('message');
|
||||
reporter.panic('message');
|
||||
reporter.error('message');
|
||||
reporter.warn('message');
|
||||
reporter.info('message');
|
||||
reporter.verbose('message');
|
||||
|
||||
expect(gatsbyUtilsMocks.reporter.panic).toBeCalledWith('message');
|
||||
expect(gatsbyUtilsMocks.reporter.panic).toBeCalledWith('message');
|
||||
expect(gatsbyUtilsMocks.reporter.panicOnBuild).toBeCalledWith('message');
|
||||
expect(gatsbyUtilsMocks.reporter.error).toBeCalledWith('message');
|
||||
expect(gatsbyUtilsMocks.reporter.warn).toBeCalledWith('message');
|
||||
expect(gatsbyUtilsMocks.reporter.info).toBeCalledWith('message');
|
||||
expect(gatsbyUtilsMocks.reporter.verbose).toBeCalledWith('message');
|
||||
});
|
||||
|
||||
it('when log level is undefined', () => {
|
||||
const reporter = resolverReporter({
|
||||
reporter: gatsbyUtilsMocks.reporter,
|
||||
});
|
||||
|
||||
reporter.panicOnBuild('message');
|
||||
reporter.panic('message');
|
||||
reporter.error('message');
|
||||
reporter.warn('message');
|
||||
reporter.info('message');
|
||||
reporter.verbose('message');
|
||||
|
||||
expect(gatsbyUtilsMocks.reporter.panic).toBeCalledWith('message');
|
||||
expect(gatsbyUtilsMocks.reporter.panic).toBeCalledWith('message');
|
||||
expect(gatsbyUtilsMocks.reporter.panicOnBuild).toBeCalledWith('message');
|
||||
expect(gatsbyUtilsMocks.reporter.error).toBeCalledWith('message');
|
||||
expect(gatsbyUtilsMocks.reporter.warn).toBeCalledWith('message');
|
||||
expect(gatsbyUtilsMocks.reporter.info).toBeCalledWith('message');
|
||||
expect(gatsbyUtilsMocks.reporter.verbose).toBeCalledWith('message');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
exports.createGatsbyPluginImageResolver = (gatsbyUtils, pluginOptions) => {
|
||||
const { reporter } = gatsbyUtils;
|
||||
try {
|
||||
const {
|
||||
getGatsbyImageResolver,
|
||||
} = require('gatsby-plugin-image/graphql-utils');
|
||||
const { createResolveCloudinaryAssetData } = require('./resolve-asset');
|
||||
const { CloudinaryPlaceholderType } = require('./types');
|
||||
|
||||
return getGatsbyImageResolver(
|
||||
createResolveCloudinaryAssetData(gatsbyUtils),
|
||||
{
|
||||
transformations: {
|
||||
type: '[String]',
|
||||
defaultValue: pluginOptions.defaultTransformations,
|
||||
},
|
||||
chained: '[String]',
|
||||
placeholder: {
|
||||
type: CloudinaryPlaceholderType,
|
||||
},
|
||||
secure: {
|
||||
type: 'Boolean',
|
||||
defaultValue: true,
|
||||
},
|
||||
logLevel: {
|
||||
type: 'String',
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
reporter.warn(
|
||||
'[gatsby-transformer-cloudinary] Install and configure gatsby-plugin-image to use the new GatsbyImage component and gatsbyImageData resolver'
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
const { GraphQLEnumType } = require('gatsby/graphql');
|
||||
|
||||
exports.CloudinaryPlaceholderType = new GraphQLEnumType({
|
||||
name: `CloudinaryPlaceholder`,
|
||||
values: {
|
||||
TRACED_SVG: { value: `tracedSVG` },
|
||||
BLURRED: { value: `blurred` },
|
||||
NONE: { value: `none` },
|
||||
},
|
||||
});
|
||||
25
plugins/gatsby-transformer-cloudinary/index.d.ts
vendored
Normal file
25
plugins/gatsby-transformer-cloudinary/index.d.ts
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Node, NodePluginArgs, Reporter } from 'gatsby';
|
||||
|
||||
export function createRemoteImageNode(
|
||||
args: CreateRemoteImageNodeArgs
|
||||
): Promise<CloudinaryAssetNode>;
|
||||
|
||||
export interface CreateRemoteImageNodeArgs {
|
||||
url: string;
|
||||
parentNode: Node;
|
||||
overwriteExisting?: boolean;
|
||||
createContentDigest: NodePluginArgs['createContentDigest'];
|
||||
createNode: NodePluginArgs['actions']['createNode'];
|
||||
createNodeId: NodePluginArgs['createNodeId'];
|
||||
reporter: Reporter;
|
||||
}
|
||||
|
||||
export interface CloudinaryAssetNode extends Node {
|
||||
cloudName: string;
|
||||
publicId: string;
|
||||
version?: number;
|
||||
originalHeight?: number;
|
||||
originalWidth?: number;
|
||||
originalFormat?: string;
|
||||
rawCloudinaryData: Object;
|
||||
}
|
||||
2
plugins/gatsby-transformer-cloudinary/index.js
Normal file
2
plugins/gatsby-transformer-cloudinary/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
exports.createRemoteImageNode =
|
||||
require('./node-creation/create-remote-image-node').createRemoteImageNode;
|
||||
@@ -0,0 +1,51 @@
|
||||
const { uploadImageNodeToCloudinary } = require('./upload');
|
||||
const { createImageNode } = require('./create-image-node');
|
||||
|
||||
const ALLOWED_MEDIA_TYPES = ['image/png', 'image/jpeg', 'image/gif'];
|
||||
|
||||
exports.createAssetNodeFromFile = async (gatsbyUtils, pluginOptions) => {
|
||||
const {
|
||||
node,
|
||||
actions: { createNode, createParentChildLink },
|
||||
createNodeId,
|
||||
createContentDigest,
|
||||
reporter,
|
||||
} = gatsbyUtils;
|
||||
|
||||
const { uploadSourceInstanceNames } = pluginOptions || {};
|
||||
|
||||
if (!ALLOWED_MEDIA_TYPES.includes(node.internal.mediaType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
uploadSourceInstanceNames &&
|
||||
!uploadSourceInstanceNames.includes(node.sourceInstanceName)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cloudinaryUploadResult = await uploadImageNodeToCloudinary({
|
||||
node,
|
||||
reporter,
|
||||
});
|
||||
|
||||
const imageNode = createImageNode({
|
||||
cloudinaryUploadResult,
|
||||
parentNode: node,
|
||||
createContentDigest,
|
||||
createNode,
|
||||
createNodeId,
|
||||
});
|
||||
|
||||
// Add the new node to Gatsby’s data layer.
|
||||
createNode(imageNode);
|
||||
|
||||
// Tell Gatsby to add `childCloudinaryAsset` to the parent `File` node.
|
||||
createParentChildLink({
|
||||
parent: node,
|
||||
child: imageNode,
|
||||
});
|
||||
|
||||
return imageNode;
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
jest.mock('./upload');
|
||||
jest.mock('./create-image-node');
|
||||
|
||||
const { createAssetNodeFromFile } = require('./create-asset-node-from-file');
|
||||
const { uploadImageNodeToCloudinary } = require('./upload');
|
||||
const { createImageNode } = require('./create-image-node');
|
||||
|
||||
const gatsbyUtilsMock = {
|
||||
actions: { createNode: jest.fn(), createParentChildLink: jest.fn() },
|
||||
createNodeId: jest.fn(),
|
||||
createContentDigest: jest.fn(),
|
||||
reporter: jest.fn(),
|
||||
};
|
||||
|
||||
describe('createAssetNodeFromFile', () => {
|
||||
beforeEach(() => {
|
||||
createImageNode.mockReturnValue({ id: 'image-node' });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('creates an image node and connects it to the node', async () => {
|
||||
const node = {
|
||||
id: 'node',
|
||||
internal: { mediaType: 'image/png' },
|
||||
};
|
||||
await createAssetNodeFromFile({ node, ...gatsbyUtilsMock });
|
||||
expect(createImageNode).toHaveBeenCalledTimes(1);
|
||||
expect(gatsbyUtilsMock.actions.createParentChildLink).toBeCalledTimes(1);
|
||||
expect(gatsbyUtilsMock.actions.createParentChildLink).toBeCalledWith({
|
||||
parent: node,
|
||||
child: { id: 'image-node' },
|
||||
});
|
||||
});
|
||||
|
||||
test('does not create an image node for invalid media type', async () => {
|
||||
const node = {
|
||||
id: 'node',
|
||||
internal: { mediaType: 'image/svg' },
|
||||
};
|
||||
await createAssetNodeFromFile({ node, ...gatsbyUtilsMock });
|
||||
expect(createImageNode).toHaveBeenCalledTimes(0);
|
||||
expect(gatsbyUtilsMock.actions.createParentChildLink).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
test('does not create an image node for invalid sourceInstanceName', async () => {
|
||||
const node = {
|
||||
id: 'node',
|
||||
sourceInstanceName: '__PROGRAMMATIC__',
|
||||
internal: { mediaType: 'image/png' },
|
||||
};
|
||||
await createAssetNodeFromFile(
|
||||
{ node, ...gatsbyUtilsMock },
|
||||
{ uploadSourceInstanceNames: ['cloudinary'] }
|
||||
);
|
||||
expect(createImageNode).toHaveBeenCalledTimes(0);
|
||||
expect(gatsbyUtilsMock.actions.createParentChildLink).toBeCalledTimes(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
const stringify = require('fast-json-stable-stringify');
|
||||
const { getPluginOptions } = require('../options');
|
||||
|
||||
exports.createImageNode = ({
|
||||
cloudinaryUploadResult,
|
||||
parentNode,
|
||||
createContentDigest,
|
||||
createNodeId,
|
||||
cloudName,
|
||||
defaultBase64,
|
||||
defaultTracedSVG,
|
||||
}) => {
|
||||
const { public_id, height, width, version, format } = cloudinaryUploadResult;
|
||||
|
||||
const fingerprint = stringify({
|
||||
cloudName,
|
||||
height,
|
||||
public_id,
|
||||
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 || getPluginOptions().cloudName,
|
||||
publicId: public_id,
|
||||
version: version,
|
||||
originalHeight: height,
|
||||
originalWidth: width,
|
||||
originalFormat: format,
|
||||
rawCloudinaryData: cloudinaryUploadResult,
|
||||
defaultBase64,
|
||||
defaultTracedSVG,
|
||||
|
||||
// Add the required internal Gatsby node fields.
|
||||
id: createNodeId(`CloudinaryAsset-${fingerprint}`),
|
||||
parent: parentNode.id,
|
||||
internal: {
|
||||
type: 'CloudinaryAsset',
|
||||
// Gatsby uses the content digest to decide when to reprocess a given
|
||||
// node. We can use the Cloudinary URL to avoid doing extra work.
|
||||
contentDigest: createContentDigest(cloudinaryUploadResult),
|
||||
},
|
||||
};
|
||||
|
||||
return imageNode;
|
||||
};
|
||||
@@ -0,0 +1,181 @@
|
||||
const { createImageNode } = require('./create-image-node');
|
||||
|
||||
jest.mock('../options');
|
||||
const { getPluginOptions } = require('../options');
|
||||
|
||||
describe('createImageNode', () => {
|
||||
function getDefaultArgs(args) {
|
||||
return {
|
||||
cloudinaryUploadResult: {},
|
||||
parentNode: {},
|
||||
createContentDigest: jest.fn(),
|
||||
createNodeId: jest.fn(),
|
||||
...args,
|
||||
};
|
||||
}
|
||||
|
||||
function getDefaultOptions(options) {
|
||||
return {
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
it('sets the cloud name', async () => {
|
||||
const options = getDefaultOptions({ cloudName: 'cloudName' });
|
||||
getPluginOptions.mockReturnValue(options);
|
||||
|
||||
const args = getDefaultArgs();
|
||||
const actual = createImageNode(args);
|
||||
|
||||
const expected = { cloudName: 'cloudName' };
|
||||
expect(actual).toEqual(expect.objectContaining(expected));
|
||||
});
|
||||
|
||||
it('sets the public ID', async () => {
|
||||
const options = getDefaultOptions();
|
||||
getPluginOptions.mockReturnValue(options);
|
||||
|
||||
const args = getDefaultArgs({
|
||||
cloudinaryUploadResult: { public_id: 'public-id' },
|
||||
});
|
||||
const actual = createImageNode(args);
|
||||
|
||||
const expected = { publicId: 'public-id' };
|
||||
expect(actual).toEqual(expect.objectContaining(expected));
|
||||
});
|
||||
|
||||
it('sets the version', async () => {
|
||||
const options = getDefaultOptions();
|
||||
getPluginOptions.mockReturnValue(options);
|
||||
|
||||
const args = getDefaultArgs({
|
||||
cloudinaryUploadResult: { version: 'version' },
|
||||
});
|
||||
const actual = createImageNode(args);
|
||||
|
||||
const expected = { version: 'version' };
|
||||
expect(actual).toEqual(expect.objectContaining(expected));
|
||||
});
|
||||
|
||||
it('sets the original height', async () => {
|
||||
const options = getDefaultOptions();
|
||||
getPluginOptions.mockReturnValue(options);
|
||||
|
||||
const args = getDefaultArgs({
|
||||
cloudinaryUploadResult: { height: 'originalHeight' },
|
||||
});
|
||||
const actual = createImageNode(args);
|
||||
|
||||
const expected = { originalHeight: 'originalHeight' };
|
||||
expect(actual).toEqual(expect.objectContaining(expected));
|
||||
});
|
||||
|
||||
it('sets the original width', async () => {
|
||||
const options = getDefaultOptions();
|
||||
getPluginOptions.mockReturnValue(options);
|
||||
|
||||
const args = getDefaultArgs({
|
||||
cloudinaryUploadResult: { width: 'originalWidth' },
|
||||
});
|
||||
const actual = createImageNode(args);
|
||||
|
||||
const expected = { originalWidth: 'originalWidth' };
|
||||
expect(actual).toEqual(expect.objectContaining(expected));
|
||||
});
|
||||
|
||||
it('sets the defaultBase64 image', async () => {
|
||||
const options = getDefaultOptions();
|
||||
getPluginOptions.mockReturnValue(options);
|
||||
|
||||
const args = getDefaultArgs({
|
||||
defaultBase64: 'defaultBase64',
|
||||
});
|
||||
const actual = createImageNode(args);
|
||||
|
||||
const expected = { defaultBase64: 'defaultBase64' };
|
||||
expect(actual).toEqual(expect.objectContaining(expected));
|
||||
});
|
||||
|
||||
it('sets the defaultTracedSVG image', async () => {
|
||||
const options = getDefaultOptions();
|
||||
getPluginOptions.mockReturnValue(options);
|
||||
|
||||
const args = getDefaultArgs({
|
||||
defaultTracedSVG: 'defaultTracedSVG',
|
||||
});
|
||||
const actual = createImageNode(args);
|
||||
|
||||
const expected = { defaultTracedSVG: 'defaultTracedSVG' };
|
||||
expect(actual).toEqual(expect.objectContaining(expected));
|
||||
});
|
||||
|
||||
it('creates a node ID', async () => {
|
||||
const options = getDefaultOptions();
|
||||
getPluginOptions.mockReturnValue(options);
|
||||
|
||||
const createNodeId = jest.fn((createNodeIdArg) => {
|
||||
expect(createNodeIdArg).toEqual(
|
||||
'CloudinaryAsset-{"cloudName":"cloudName","height":100,"public_id":"public_id","version":7,"width":200}'
|
||||
);
|
||||
return 'createNodeIdResult';
|
||||
});
|
||||
const args = getDefaultArgs({
|
||||
createNodeId,
|
||||
cloudinaryUploadResult: {
|
||||
height: 100,
|
||||
public_id: 'public_id',
|
||||
version: 7,
|
||||
width: 200,
|
||||
},
|
||||
cloudName: 'cloudName',
|
||||
});
|
||||
const actual = createImageNode(args);
|
||||
|
||||
const expected = { id: 'createNodeIdResult' };
|
||||
expect(actual).toEqual(expect.objectContaining(expected));
|
||||
});
|
||||
|
||||
it('sets the parent', async () => {
|
||||
const options = getDefaultOptions();
|
||||
getPluginOptions.mockReturnValue(options);
|
||||
|
||||
const args = getDefaultArgs({
|
||||
parentNode: { id: 'parentNodeId' },
|
||||
});
|
||||
const actual = createImageNode(args);
|
||||
|
||||
const expected = { parent: 'parentNodeId' };
|
||||
expect(actual).toEqual(expect.objectContaining(expected));
|
||||
});
|
||||
|
||||
it('creates the content digest', async () => {
|
||||
const options = getDefaultOptions();
|
||||
getPluginOptions.mockReturnValue(options);
|
||||
|
||||
const cloudinaryUploadResult = {
|
||||
height: 100,
|
||||
public_id: 'public_id',
|
||||
version: 7,
|
||||
width: 200,
|
||||
};
|
||||
|
||||
const createContentDigest = jest.fn((createContentDigestArg) => {
|
||||
expect(createContentDigestArg).toEqual(cloudinaryUploadResult);
|
||||
return 'createContentDigestResult';
|
||||
});
|
||||
const args = getDefaultArgs({
|
||||
createContentDigest,
|
||||
cloudinaryUploadResult,
|
||||
cloudName: 'cloudName',
|
||||
});
|
||||
const actual = createImageNode(args);
|
||||
|
||||
const expected = {
|
||||
internal: {
|
||||
type: 'CloudinaryAsset',
|
||||
contentDigest: 'createContentDigestResult',
|
||||
},
|
||||
};
|
||||
expect(actual).toEqual(expect.objectContaining(expected));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
const path = require('path');
|
||||
const { uploadImageToCloudinary } = require('./upload');
|
||||
const { createImageNode } = require('./create-image-node');
|
||||
const { getPluginOptions } = require('../options');
|
||||
|
||||
exports.createRemoteImageNode = async ({
|
||||
url,
|
||||
overwriteExisting,
|
||||
parentNode,
|
||||
createContentDigest,
|
||||
createNode,
|
||||
createNodeId,
|
||||
reporter,
|
||||
}) => {
|
||||
if (!reporter) {
|
||||
throw Error(
|
||||
"`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) {
|
||||
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."
|
||||
);
|
||||
}
|
||||
if (!createContentDigest) {
|
||||
reporter.panic(
|
||||
"`createContentDigest` is a required argument. It's available at `CreateNodeArgs.createContentDigest`."
|
||||
);
|
||||
}
|
||||
if (!createNode) {
|
||||
reporter.panic(
|
||||
"`createNode` is a required argument. It's available at `CreateNodeArgs.actions.createNode`."
|
||||
);
|
||||
}
|
||||
if (!createNodeId) {
|
||||
reporter.panic(
|
||||
"`createNodeId` is a required argument. It's available at `CreateNodeArgs.createNodeId`."
|
||||
);
|
||||
}
|
||||
|
||||
const overwrite =
|
||||
overwriteExisting == null
|
||||
? getPluginOptions().overwriteExisting
|
||||
: overwriteExisting;
|
||||
|
||||
const publicId = path.parse(url).name;
|
||||
|
||||
const cloudinaryUploadResult = await uploadImageToCloudinary({
|
||||
url,
|
||||
publicId,
|
||||
overwrite,
|
||||
reporter,
|
||||
});
|
||||
|
||||
const imageNode = createImageNode({
|
||||
cloudinaryUploadResult,
|
||||
parentNode,
|
||||
createContentDigest,
|
||||
createNodeId,
|
||||
});
|
||||
|
||||
// Add the new node to Gatsby’s data layer.
|
||||
createNode(imageNode, { name: 'gatsby-transformer-cloudinary' });
|
||||
|
||||
return imageNode;
|
||||
};
|
||||
@@ -0,0 +1,174 @@
|
||||
const path = require('path');
|
||||
const { createRemoteImageNode } = require('./create-remote-image-node');
|
||||
|
||||
jest.mock('./create-image-node');
|
||||
jest.mock('../options');
|
||||
jest.mock('./upload');
|
||||
|
||||
const { createImageNode } = require('./create-image-node');
|
||||
const { getPluginOptions } = require('../options');
|
||||
const { uploadImageToCloudinary } = require('./upload');
|
||||
|
||||
function getDefaultArgs(args) {
|
||||
return {
|
||||
url: 'https://www.google.com/images/puppy.jpg#anchor?abc=def',
|
||||
createNode: jest.fn(() => 'createNode'),
|
||||
createNodeId: jest.fn(() => 'createNodeId'),
|
||||
createContentDigest: jest.fn(() => 'createContentDigest'),
|
||||
reporter: {
|
||||
panic: (msg) => {
|
||||
throw Error(`[reporter] ${msg}}`);
|
||||
},
|
||||
},
|
||||
parentNode: { id: 'abc-123' },
|
||||
overwriteExisting: false,
|
||||
...args,
|
||||
};
|
||||
}
|
||||
|
||||
describe('createRemoteImageNode', () => {
|
||||
test('requires url', async () => {
|
||||
const args = getDefaultArgs();
|
||||
delete args.url;
|
||||
|
||||
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;
|
||||
|
||||
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 createContentDigest', async () => {
|
||||
const args = getDefaultArgs();
|
||||
delete args.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;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
const optionOverwrite = 'optionOverwrite';
|
||||
getPluginOptions.mockReturnValue({ overwriteExisting: optionOverwrite });
|
||||
createImageNode.mockReturnValue({ id: 'image-node-id' });
|
||||
|
||||
await createRemoteImageNode(args);
|
||||
|
||||
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 });
|
||||
|
||||
const optionOverwrite = 'optionOverwrite';
|
||||
getPluginOptions.mockReturnValue({ overwriteExisting: optionOverwrite });
|
||||
createImageNode.mockReturnValue({ id: 'image-node-id' });
|
||||
|
||||
await createRemoteImageNode(args);
|
||||
|
||||
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('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);
|
||||
|
||||
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);
|
||||
|
||||
await createRemoteImageNode(args);
|
||||
expect(createNode).toHaveBeenCalledWith(createImageNodeResult, {
|
||||
name: 'gatsby-transformer-cloudinary',
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
expect(actual).toEqual(imageNode);
|
||||
});
|
||||
});
|
||||
18
plugins/gatsby-transformer-cloudinary/node-creation/index.js
Normal file
18
plugins/gatsby-transformer-cloudinary/node-creation/index.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const { createAssetNodeFromFile } = require('./create-asset-node-from-file');
|
||||
const { CloudinaryAssetType } = require('./types');
|
||||
|
||||
exports.createCloudinaryAssetType = (gatsbyUtils) => {
|
||||
const { actions } = gatsbyUtils;
|
||||
actions.createTypes(CloudinaryAssetType);
|
||||
};
|
||||
|
||||
exports.createCloudinaryAssetNodes = async (gatsbyUtils, pluginOptions) => {
|
||||
// Create nodes for files to be uploaded to cloudinary
|
||||
if (
|
||||
pluginOptions.apiKey &&
|
||||
pluginOptions.apiSecret &&
|
||||
pluginOptions.cloudName
|
||||
) {
|
||||
await createAssetNodeFromFile(gatsbyUtils, pluginOptions);
|
||||
}
|
||||
};
|
||||
11
plugins/gatsby-transformer-cloudinary/node-creation/types.js
Normal file
11
plugins/gatsby-transformer-cloudinary/node-creation/types.js
Normal file
@@ -0,0 +1,11 @@
|
||||
exports.CloudinaryAssetType = `
|
||||
type CloudinaryAsset implements Node {
|
||||
id: ID!
|
||||
publicId: String!
|
||||
cloudName: String!
|
||||
version: String
|
||||
originalWidth: Int
|
||||
originalHeight: Int
|
||||
originalFormat: String
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,94 @@
|
||||
const cloudinary = require('cloudinary').v2;
|
||||
const { getPluginOptions } = require('../options');
|
||||
|
||||
let totalImages = 0;
|
||||
let uploadedImages = 0;
|
||||
|
||||
const FIVE_MINUTES = 5 * 60 * 1000;
|
||||
|
||||
exports.uploadImageToCloudinary = async ({
|
||||
url,
|
||||
publicId,
|
||||
overwrite,
|
||||
reporter,
|
||||
}) => {
|
||||
verifyRequiredOptions(reporter);
|
||||
|
||||
const { apiKey, apiSecret, cloudName, uploadFolder } = getPluginOptions();
|
||||
|
||||
cloudinary.config({
|
||||
cloud_name: cloudName,
|
||||
api_key: apiKey,
|
||||
api_secret: apiSecret,
|
||||
});
|
||||
|
||||
const uploadOptions = {
|
||||
folder: uploadFolder,
|
||||
overwrite,
|
||||
public_id: publicId,
|
||||
resource_type: 'auto',
|
||||
timeout: FIVE_MINUTES,
|
||||
};
|
||||
|
||||
let attempts = 1;
|
||||
|
||||
totalImages++;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
const result = await cloudinary.uploader.upload(url, uploadOptions);
|
||||
uploadedImages++;
|
||||
if (
|
||||
uploadedImages == totalImages ||
|
||||
uploadedImages % Math.ceil(totalImages / 100) == 0
|
||||
)
|
||||
reporter.info(
|
||||
`[gatsby-transformer-cloudinary] Uploaded ${uploadedImages} of ${totalImages} images to Cloudinary. (${Math.round(
|
||||
(100 * uploadedImages) / totalImages
|
||||
)}%)`
|
||||
);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const stringifiedError = JSON.stringify(error, null, 2);
|
||||
if (attempts < 3) {
|
||||
attempts += 1;
|
||||
reporter.warn(
|
||||
`An error occurred when uploading ${url} to Cloudinary: ${stringifiedError}`
|
||||
);
|
||||
} else {
|
||||
reporter.panic(
|
||||
`Unable to upload ${url} to Cloudinary after ${attempts} attempts: ${stringifiedError}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
exports.uploadImageNodeToCloudinary = async ({ node, reporter }) => {
|
||||
const url = node.absolutePath;
|
||||
const relativePathWithoutExtension = node.relativePath.replace(
|
||||
/\.[^.]*$/,
|
||||
''
|
||||
);
|
||||
const publicId = relativePathWithoutExtension;
|
||||
const overwrite = getPluginOptions().overwriteExisting;
|
||||
const result = await exports.uploadImageToCloudinary({
|
||||
url,
|
||||
publicId,
|
||||
overwrite,
|
||||
reporter,
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
function verifyRequiredOptions(reporter) {
|
||||
const requiredOptions = ['apiKey', 'apiSecret', 'cloudName'];
|
||||
const pluginOptions = getPluginOptions();
|
||||
requiredOptions.forEach((optionKey) => {
|
||||
if (pluginOptions[optionKey] == null) {
|
||||
reporter.panic(
|
||||
`[gatsby-transformer-cloudinary] "${optionKey}" is a required plugin option. You can add it to the options object for "gatsby-transformer-cloudinary" in your gatsby-config file.`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
const {
|
||||
uploadImageToCloudinary,
|
||||
uploadImageNodeToCloudinary,
|
||||
} = require('./upload');
|
||||
|
||||
jest.mock('../options');
|
||||
jest.mock('cloudinary');
|
||||
|
||||
const { getPluginOptions } = require('../options');
|
||||
const cloudinary = require('cloudinary').v2;
|
||||
|
||||
const defaultPluginOptions = {
|
||||
apiKey: 'apiKey',
|
||||
apiSecret: 'apiSecret',
|
||||
cloudName: 'cloudName',
|
||||
};
|
||||
|
||||
describe('uploadImageToCloudinary', () => {
|
||||
function getDefaultArgs(args) {
|
||||
return {
|
||||
url: 'url',
|
||||
overwrite: 'overwrite',
|
||||
publicId: 'publicId',
|
||||
reporter: {
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
panic: jest.fn(),
|
||||
},
|
||||
...args,
|
||||
};
|
||||
}
|
||||
|
||||
function getDefaultOptions(options) {
|
||||
return {
|
||||
cloudName: 'cloudName',
|
||||
apiKey: 'apiKey',
|
||||
apiSecret: 'apiSecret',
|
||||
uploadFolder: 'uploadFolder',
|
||||
createDerived: false,
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
it('configures cloudinary with the appropriate plugin options', async () => {
|
||||
const cloudinaryConfig = jest.fn();
|
||||
cloudinary.config = cloudinaryConfig;
|
||||
|
||||
const options = getDefaultOptions();
|
||||
getPluginOptions.mockReturnValue(options);
|
||||
|
||||
const args = getDefaultArgs();
|
||||
await uploadImageToCloudinary(args);
|
||||
|
||||
const expected = {
|
||||
cloud_name: options.cloudName,
|
||||
api_key: options.apiKey,
|
||||
api_secret: options.apiSecret,
|
||||
};
|
||||
expect(cloudinaryConfig).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
|
||||
it('overwrites when passed overwrite:true', async () => {
|
||||
const cloudinaryUpload = jest.fn();
|
||||
cloudinary.uploader.upload = cloudinaryUpload;
|
||||
|
||||
const options = getDefaultOptions();
|
||||
getPluginOptions.mockReturnValue(options);
|
||||
|
||||
const args = getDefaultArgs({ overwrite: true });
|
||||
await uploadImageToCloudinary(args);
|
||||
|
||||
const expectedUrl = args.url;
|
||||
const expectedOptions = {
|
||||
folder: options.uploadFolder,
|
||||
overwrite: true,
|
||||
public_id: args.publicId,
|
||||
resource_type: 'auto',
|
||||
timeout: 5 * 60 * 1000,
|
||||
};
|
||||
expect(cloudinaryUpload).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
});
|
||||
|
||||
it('returns the result returned from the Cloudinary uploader', async () => {
|
||||
const cloudinaryUpload = jest.fn();
|
||||
const cloudinaryUploadResult = 'cloudinaryUploadResult';
|
||||
cloudinaryUpload.mockReturnValue(cloudinaryUploadResult);
|
||||
cloudinary.uploader.upload = cloudinaryUpload;
|
||||
|
||||
const options = getDefaultOptions();
|
||||
getPluginOptions.mockReturnValue(options);
|
||||
|
||||
const args = getDefaultArgs();
|
||||
expect(await uploadImageToCloudinary(args)).toEqual(cloudinaryUploadResult);
|
||||
});
|
||||
});
|
||||
|
||||
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',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('passes the overwrite setting from the plugin options', async () => {
|
||||
const cloudinaryUpload = jest.fn();
|
||||
cloudinary.uploader.upload = cloudinaryUpload;
|
||||
|
||||
const reporter = { info: jest.fn() };
|
||||
const node = {
|
||||
relativePath: 'relativePath.jpg',
|
||||
};
|
||||
const overwriteExisting = 'overwriteExistingDouble';
|
||||
getPluginOptions.mockReturnValue({
|
||||
...defaultPluginOptions,
|
||||
overwriteExisting,
|
||||
});
|
||||
|
||||
await uploadImageNodeToCloudinary({ node, reporter });
|
||||
|
||||
expect(cloudinaryUpload).toHaveBeenCalledWith(
|
||||
undefined,
|
||||
expect.objectContaining({
|
||||
overwrite: overwriteExisting,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('requires the apiKey option', async () => {
|
||||
const reporter = {
|
||||
panic: jest.fn(() => {
|
||||
throw Error();
|
||||
}),
|
||||
};
|
||||
const node = { relativePath: 'relativePath.jpg' };
|
||||
getPluginOptions.mockReturnValue({
|
||||
...defaultPluginOptions,
|
||||
apiKey: null,
|
||||
});
|
||||
|
||||
try {
|
||||
await uploadImageNodeToCloudinary({ node, reporter });
|
||||
} catch {}
|
||||
|
||||
expect(reporter.panic).toHaveBeenCalledWith(
|
||||
'[gatsby-transformer-cloudinary] "apiKey" is a required plugin option. You can add it to the options object for "gatsby-transformer-cloudinary" in your gatsby-config file.'
|
||||
);
|
||||
});
|
||||
|
||||
it('requires the apiSecret option', async () => {
|
||||
const reporter = {
|
||||
panic: jest.fn(() => {
|
||||
throw Error();
|
||||
}),
|
||||
};
|
||||
const node = { relativePath: 'relativePath.jpg' };
|
||||
getPluginOptions.mockReturnValue({
|
||||
...defaultPluginOptions,
|
||||
apiSecret: null,
|
||||
});
|
||||
|
||||
try {
|
||||
await uploadImageNodeToCloudinary({ node, reporter });
|
||||
} catch {}
|
||||
|
||||
expect(reporter.panic).toHaveBeenCalledWith(
|
||||
'[gatsby-transformer-cloudinary] "apiSecret" is a required plugin option. You can add it to the options object for "gatsby-transformer-cloudinary" in your gatsby-config file.'
|
||||
);
|
||||
});
|
||||
|
||||
it('requires the cloudName option', async () => {
|
||||
const reporter = {
|
||||
panic: jest.fn(() => {
|
||||
throw Error();
|
||||
}),
|
||||
};
|
||||
const node = { relativePath: 'relativePath.jpg' };
|
||||
getPluginOptions.mockReturnValue({
|
||||
...defaultPluginOptions,
|
||||
cloudName: null,
|
||||
});
|
||||
|
||||
try {
|
||||
await uploadImageNodeToCloudinary({ node, reporter });
|
||||
} catch {}
|
||||
|
||||
expect(reporter.panic).toHaveBeenCalledWith(
|
||||
'[gatsby-transformer-cloudinary] "cloudName" is a required plugin option. You can add it to the options object for "gatsby-transformer-cloudinary" in your gatsby-config file.'
|
||||
);
|
||||
});
|
||||
});
|
||||
28
plugins/gatsby-transformer-cloudinary/options.js
Normal file
28
plugins/gatsby-transformer-cloudinary/options.js
Normal file
@@ -0,0 +1,28 @@
|
||||
let options = {};
|
||||
|
||||
exports.initializaGlobalState = (_, pluginOptions) => {
|
||||
// Make options available for createRemoteImageNode
|
||||
// as it can be used outside of gatsby-node lifecycle hooks
|
||||
Object.assign(options, pluginOptions);
|
||||
};
|
||||
|
||||
exports.getPluginOptions = () => {
|
||||
return options;
|
||||
};
|
||||
|
||||
exports.getCoreSupportsOnPluginInit = () => {
|
||||
let coreSupportsOnPluginInit = undefined;
|
||||
try {
|
||||
const { isGatsbyNodeLifecycleSupported } = require(`gatsby-plugin-utils`);
|
||||
if (isGatsbyNodeLifecycleSupported(`onPluginInit`)) {
|
||||
coreSupportsOnPluginInit = 'stable';
|
||||
} else if (isGatsbyNodeLifecycleSupported(`unstable_onPluginInit`)) {
|
||||
coreSupportsOnPluginInit = 'unstable';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[gatsby-transformer-cloudinary] Cannot check if Gatsby supports onPluginInit`
|
||||
);
|
||||
}
|
||||
return coreSupportsOnPluginInit;
|
||||
};
|
||||
33
plugins/gatsby-transformer-cloudinary/package.json
Normal file
33
plugins/gatsby-transformer-cloudinary/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "gatsby-transformer-cloudinary",
|
||||
"version": "4.5.0",
|
||||
"description": "Transform local files into Cloudinary-managed assets for Gatsby sites.",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"homepage": "https://gatsby-transformer-cloudinary.netlify.com/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/cloudinary-devs/gatsby-transformer-cloudinary"
|
||||
},
|
||||
"keywords": [
|
||||
"gatsby",
|
||||
"gatsby-plugin",
|
||||
"image",
|
||||
"cloudinary"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "~1.1.3",
|
||||
"cloudinary": "^1.35.0",
|
||||
"fast-json-stable-stringify": "2.1.0",
|
||||
"gatsby-plugin-utils": "^4.0.0",
|
||||
"probe-image-size": "7.2.3"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "cp ../README.md ./README.md && cp ../CHANGELOG.md ./CHANGELOG.md"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"gatsby": "^3.0.0 || ^4.0.0 || ^5.0.0",
|
||||
"gatsby-plugin-image": "^1.14.0 || ^2.0.0 || ^3.0.0"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user