Skip to content

Commit acf28f9

Browse files
authored
feat: allow invalid source data by making gatsbyImageData nullable (#218)
When sourcing data every sourced item might _not_ be be valid. Instead of stopping the build an error will be logged. Make sure to check if `gatsbyImageData` is not null before using in the `GatsbyImage` component. Closes #214
1 parent 15f0682 commit acf28f9

File tree

6 files changed

+129
-37
lines changed

6 files changed

+129
-37
lines changed

demo/gatsby-config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ module.exports = {
3131
'CloudinaryAsset',
3232
'ExistingDataExampleImage',
3333
'ExistingDataNestedExampleImage',
34+
'SomeBadImageData',
3435
],
3536
},
3637
},

demo/gatsby-node.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,25 @@ exports.sourceNodes = (gatsbyUtils) => {
3939

4040
reporter.info(`[site] Create ExistingData node # 1`);
4141

42+
createNode({
43+
id: createNodeId(`GoodData >>> 1`),
44+
name: 'GoodData',
45+
...cloudinaryData1,
46+
internal: {
47+
type: 'SomeBadImageData',
48+
contentDigest: createContentDigest('GoodData' + cloudinaryData1),
49+
},
50+
});
51+
52+
createNode({
53+
id: createNodeId(`BadData >>> 2`),
54+
name: 'BadData',
55+
internal: {
56+
type: 'SomeBadImageData',
57+
contentDigest: createContentDigest('BadData'),
58+
},
59+
});
60+
4261
const cloudinaryData2 = {
4362
cloudName: 'jlengstorf',
4463
publicId: 'gatsby-cloudinary/jason',

demo/src/pages/somebaddata.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import React from 'react';
2+
import { graphql, useStaticQuery } from 'gatsby';
3+
import { GatsbyImage, getImage } from 'gatsby-plugin-image';
4+
5+
const ProblemExample = () => {
6+
const data = useStaticQuery(graphql`
7+
query {
8+
allSomeBadImageData {
9+
nodes {
10+
name
11+
gatsbyImageData(height: 200)
12+
}
13+
}
14+
}
15+
`);
16+
17+
return data.allSomeBadImageData.nodes.map((node, index) => {
18+
const gatsbyImage = getImage(node);
19+
20+
if (gatsbyImage) {
21+
return <GatsbyImage key={index} image={gatsbyImage} alt={node.name} />;
22+
} else {
23+
return <div>No image for node with name: {node.name}</div>;
24+
}
25+
});
26+
};
27+
28+
export default ProblemExample;

plugin/gatsby-plugin-image/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ exports.createGatsbyImageDataResolver = (gatsbyUtils, pluginOptions) => {
1010

1111
if (gatsbyImageResolver) {
1212
const resolvers = {};
13+
// Make the resolver nullable, createGatsbyPluginImageResolver sets the type to 'GatsbyImageData!'
14+
gatsbyImageResolver.type = 'GatsbyImageData';
1315

1416
transformTypes.forEach((type) => {
1517
// Add gatsbyImageData resolver

plugin/gatsby-plugin-image/resolve-asset.js

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ const {
1010
getAssetMetadata,
1111
} = require('./asset-data');
1212

13-
exports._generateCloudinaryAssetSource = (
13+
const generateCloudinaryAssetSource = (
1414
filename,
1515
width,
1616
height,
1717
format,
18-
fit,
18+
_fit,
1919
options
2020
) => {
2121
const [cloudName, publicId] = filename.split('>>>');
@@ -38,9 +38,21 @@ exports._generateCloudinaryAssetSource = (
3838
return imageSource;
3939
};
4040

41+
exports._generateCloudinaryAssetSource = generateCloudinaryAssetSource;
42+
4143
exports.createResolveCloudinaryAssetData =
42-
(gatsbyUtils) => async (source, args) => {
44+
(gatsbyUtils) => async (source, args, _context, info) => {
4345
const { reporter } = gatsbyUtils;
46+
const transformType = info.parentType || 'UnknownTransformType';
47+
48+
const hasRequiredData = source.cloudName && source.publicId;
49+
50+
if (!hasRequiredData) {
51+
reporter.error(
52+
`[gatsby-transformer-cloudinary] Missing required fields on ${transformType}: cloudName=${source.cloudName}, publicId=${source.cloudName}`
53+
);
54+
return null;
55+
}
4456

4557
let metadata = {
4658
width: source.originalWidth,
@@ -54,18 +66,16 @@ exports.createResolveCloudinaryAssetData =
5466
if (!hasSizingAndFormatMetadata) {
5567
// Lacking metadata, so lets request it from Cloudinary
5668
try {
69+
metadata = await getAssetMetadata({ source, args });
5770
reporter.verbose(
58-
`[gatsby-transformer-cloudinary] Missing metadata on ${source.cloudName} > ${source.publicId}`
59-
);
60-
reporter.verbose(
61-
`[gatsby-transformer-cloudinary] >>> To save on network requests add originalWidth, originalHeight and originalFormat to the asset data`
71+
`[gatsby-transformer-cloudinary] Missing metadata fields on ${transformType} for ${source.cloudName} > ${source.publicId}
72+
>>> To save on network requests add originalWidth, originalHeight and originalFormat to ${transformType}`
6273
);
63-
metadata = await getAssetMetadata({ source, args });
6474
} catch (error) {
65-
reporter.panic(
66-
`[gatsby-transformer-cloudinary] Could not get metadata for ${source.cloudName} > ${source.publicId}:`,
67-
error
75+
reporter.error(
76+
`[gatsby-transformer-cloudinary] Could not get metadata for ${transformType} for ${source.cloudName} > ${source.publicId}: ${error.message}`
6877
);
78+
return null;
6979
}
7080
}
7181

@@ -75,7 +85,7 @@ exports.createResolveCloudinaryAssetData =
7585
// Passing the plugin name allows for better error messages
7686
pluginName: `gatsby-transformer-cloudinary`,
7787
sourceMetadata: metadata,
78-
generateImageSource: this._generateCloudinaryAssetSource,
88+
generateImageSource: generateCloudinaryAssetSource,
7989
options: args,
8090
};
8191

@@ -98,8 +108,7 @@ exports.createResolveCloudinaryAssetData =
98108
}
99109
} catch (error) {
100110
reporter.error(
101-
`[gatsby-transformer-cloudinary] Could not generate placeholder (${args.placeholder}) for ${source.cloudName} > ${source.publicId}:`,
102-
error
111+
`[gatsby-transformer-cloudinary] Could not generate placeholder (${args.placeholder}) for ${source.cloudName} > ${source.publicId}: ${error.message}`
103112
);
104113
}
105114

plugin/gatsby-plugin-image/resolve-asset.test.js

Lines changed: 56 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ jest.mock('./asset-data');
33

44
const gatsbyUtilsMocks = {
55
reporter: {
6-
error: jest.fn(),
76
panic: jest.fn(),
7+
error: jest.fn(),
8+
info: jest.fn(),
89
verbose: jest.fn(),
910
},
1011
};
@@ -58,18 +59,22 @@ describe('generateCloudinaryAssetSource', () => {
5859
});
5960

6061
describe('resolveCloudinaryAssetData', () => {
61-
const source = {
62+
const sourceWithMetadata = {
6263
publicId: 'public-id',
6364
cloudName: 'cloud-name',
6465
originalWidth: '600',
6566
originalHeight: '300',
6667
originalFormat: 'jpg',
6768
};
6869

69-
const args = {
70-
transformations: ['e_grayscale'],
70+
const sourceWithoutMeta = {
71+
publicId: 'public-id',
72+
cloudName: 'cloud-name',
7173
};
7274

75+
const context = {}; // Never used
76+
const info = {};
77+
7378
beforeEach(() => {
7479
getAssetMetadata.mockResolvedValue({
7580
width: 100,
@@ -85,13 +90,15 @@ describe('resolveCloudinaryAssetData', () => {
8590
jest.clearAllMocks();
8691
});
8792

88-
it('calls gatsby-plugin-image -> generateImageData', async () => {
89-
await resolveCloudinaryAssetData(source, args);
93+
it('calls gatsby-plugin-image -> generateImageData once', async () => {
94+
const args = {};
95+
await resolveCloudinaryAssetData(sourceWithMetadata, args, context, info);
9096
expect(generateImageData).toBeCalledTimes(1);
9197
});
9298

9399
it('calls gatsby-plugin-image -> generateImageData with correct data', async () => {
94-
await resolveCloudinaryAssetData(source, args);
100+
const args = { transformations: ['e_grayscale'] };
101+
await resolveCloudinaryAssetData(sourceWithMetadata, args, context, info);
95102
expect(generateImageData).toBeCalledWith({
96103
filename: 'cloud-name>>>public-id',
97104
generateImageSource: _generateCloudinaryAssetSource,
@@ -109,12 +116,13 @@ describe('resolveCloudinaryAssetData', () => {
109116
});
110117

111118
it('fetches metadata when not present on source', async () => {
112-
await resolveCloudinaryAssetData(source, {});
113-
await resolveCloudinaryAssetData(
114-
{ publicId: 'public-id', cloudName: 'cloud-name' },
115-
{}
116-
);
119+
const args = {};
120+
await resolveCloudinaryAssetData(sourceWithMetadata, args, context, info);
121+
await resolveCloudinaryAssetData(sourceWithoutMeta, args, context, info);
122+
// getAssetMetadata should only be called for sourceWithoutMeta
117123
expect(getAssetMetadata).toBeCalledTimes(1);
124+
expect(gatsbyUtilsMocks.reporter.verbose).toBeCalledTimes(1);
125+
// gatsby-plugin-image -> generateImageData should be called for both
118126
expect(generateImageData).toHaveBeenNthCalledWith(2, {
119127
filename: 'cloud-name>>>public-id',
120128
generateImageSource: _generateCloudinaryAssetSource,
@@ -129,7 +137,8 @@ describe('resolveCloudinaryAssetData', () => {
129137
});
130138

131139
it('fetches and adds correct "blurred" placeholder', async () => {
132-
await resolveCloudinaryAssetData(source, { placeholder: 'blurred' });
140+
const args = { placeholder: 'blurred' };
141+
await resolveCloudinaryAssetData(sourceWithMetadata, args, context, info);
133142

134143
expect(getLowResolutionImageURL).toBeCalledTimes(1);
135144
expect(getUrlAsBase64Image).toBeCalledTimes(1);
@@ -149,7 +158,8 @@ describe('resolveCloudinaryAssetData', () => {
149158
});
150159

151160
it('fetches and adds correct "tracedSVG" placeholder', async () => {
152-
await resolveCloudinaryAssetData(source, { placeholder: 'tracedSVG' });
161+
const args = { placeholder: 'tracedSVG' };
162+
await resolveCloudinaryAssetData(sourceWithMetadata, args, context, info);
153163

154164
expect(getAssetAsTracedSvg).toBeCalledTimes(1);
155165
expect(generateImageData).toHaveBeenCalledWith({
@@ -167,6 +177,22 @@ describe('resolveCloudinaryAssetData', () => {
167177
});
168178
});
169179

180+
describe('when missing required data', () => {
181+
it('calls reporter.error and returns null', async () => {
182+
const source = {};
183+
const args = {};
184+
const result = await resolveCloudinaryAssetData(
185+
source,
186+
args,
187+
context,
188+
info
189+
);
190+
expect(generateImageData).toBeCalledTimes(0);
191+
expect(gatsbyUtilsMocks.reporter.error).toBeCalledTimes(1);
192+
expect(result).toBe(null);
193+
});
194+
});
195+
170196
describe('when error fetching asset data', () => {
171197
beforeEach(() => {
172198
getAssetMetadata.mockImplementation(() => {
@@ -185,17 +211,23 @@ describe('resolveCloudinaryAssetData', () => {
185211
jest.clearAllMocks();
186212
});
187213

188-
it('reporter.panic on fetching metdata error', async () => {
189-
await resolveCloudinaryAssetData(
190-
{ publicId: 'public-id', cloudName: 'cloud-name' },
191-
{}
214+
it('calls reporter.error on fetching metadata error and returns null', async () => {
215+
const args = {};
216+
const result = await resolveCloudinaryAssetData(
217+
sourceWithoutMeta,
218+
args,
219+
context,
220+
info
192221
);
193222
expect(getAssetMetadata).toBeCalledTimes(1);
194-
expect(gatsbyUtilsMocks.reporter.panic).toBeCalledTimes(1);
223+
expect(generateImageData).toBeCalledTimes(0);
224+
expect(gatsbyUtilsMocks.reporter.error).toBeCalledTimes(1);
225+
expect(result).toBe(null);
195226
});
196227

197-
it('reporter.errror on fetching blurred placeholder error', async () => {
198-
await resolveCloudinaryAssetData(source, { placeholder: 'blurred' });
228+
it('calls reporter.error on fetching blurred placeholder error', async () => {
229+
const args = { placeholder: 'blurred' };
230+
await resolveCloudinaryAssetData(sourceWithMetadata, args, context, info);
199231
expect(getLowResolutionImageURL).toBeCalledTimes(1);
200232
expect(getUrlAsBase64Image).toBeCalledTimes(1);
201233
expect(gatsbyUtilsMocks.reporter.error).toBeCalledTimes(1);
@@ -214,8 +246,9 @@ describe('resolveCloudinaryAssetData', () => {
214246
});
215247
});
216248

217-
it('reporter.errror on fetching tracedSVG placeholder error', async () => {
218-
await resolveCloudinaryAssetData(source, { placeholder: 'tracedSVG' });
249+
it('calls reporter.error on fetching tracedSVG placeholder error', async () => {
250+
const args = { placeholder: 'tracedSVG' };
251+
await resolveCloudinaryAssetData(sourceWithMetadata, args, context, info);
219252
expect(getAssetAsTracedSvg).toBeCalledTimes(1);
220253
expect(gatsbyUtilsMocks.reporter.error).toBeCalledTimes(1);
221254
expect(generateImageData).toHaveBeenCalledWith({

0 commit comments

Comments
 (0)