Skip to content

Commit 63c01ad

Browse files
Sprinkles: Improve error experience (#77)
Co-authored-by: Mark Dalgleish <[email protected]>
1 parent 3360bdf commit 63c01ad

File tree

3 files changed

+288
-24
lines changed

3 files changed

+288
-24
lines changed

.changeset/plenty-pianos-travel.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@vanilla-extract/sprinkles': patch
3+
---
4+
5+
Improve runtime errors
6+
7+
Sprinkles will now validate your `atoms` calls at runtime for a better developer experience. The validation code should be stripped from production bundles via a `process.env.NODE_ENV` check.
8+
9+
Example Error
10+
11+
```bash
12+
SprinklesError: "paddingTop" has no value "xlarge". Possible values are "small", "medium", "large"
13+
```

packages/sprinkles/src/createAtomsFn.ts

Lines changed: 148 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -126,40 +126,164 @@ export function createAtomsFn<Args extends ReadonlyArray<AtomicStyles>>(
126126
for (const prop in finalProps) {
127127
const propValue = finalProps[prop];
128128
const atomicProperty = atomicStyles[prop];
129+
try {
130+
if (atomicProperty.mappings) {
131+
// Skip shorthands
132+
continue;
133+
}
129134

130-
if (atomicProperty.mappings) {
131-
// Skip shorthands
132-
continue;
133-
}
135+
if (typeof propValue === 'string' || typeof propValue === 'number') {
136+
if (process.env.NODE_ENV !== 'production') {
137+
if (!atomicProperty.values[propValue].defaultClass) {
138+
throw new Error();
139+
}
140+
}
141+
classNames.push(atomicProperty.values[propValue].defaultClass);
142+
} else if (Array.isArray(propValue)) {
143+
for (const responsiveIndex in propValue) {
144+
const responsiveValue = propValue[responsiveIndex];
145+
146+
if (responsiveValue != null) {
147+
const conditionName =
148+
atomicProperty.responsiveArray[responsiveIndex];
149+
150+
if (process.env.NODE_ENV !== 'production') {
151+
if (
152+
!atomicProperty.values[responsiveValue].conditions[
153+
conditionName
154+
]
155+
) {
156+
throw new Error();
157+
}
158+
}
159+
160+
classNames.push(
161+
atomicProperty.values[responsiveValue].conditions[
162+
conditionName
163+
],
164+
);
165+
}
166+
}
167+
} else {
168+
for (const conditionName in propValue) {
169+
// Conditional style
170+
const value = propValue[conditionName];
134171

135-
if (typeof propValue === 'string' || typeof propValue === 'number') {
136-
classNames.push(atomicProperty.values[propValue].defaultClass);
137-
} else if (Array.isArray(propValue)) {
138-
for (const responsiveIndex in propValue) {
139-
const responsiveValue = propValue[responsiveIndex];
140-
141-
if (
142-
typeof responsiveValue === 'string' ||
143-
typeof responsiveValue === 'number'
144-
) {
145-
const conditionName =
146-
atomicProperty.responsiveArray[responsiveIndex];
172+
if (process.env.NODE_ENV !== 'production') {
173+
if (!atomicProperty.values[value].conditions[conditionName]) {
174+
throw new Error();
175+
}
176+
}
147177
classNames.push(
148-
atomicProperty.values[responsiveValue].conditions[conditionName],
178+
atomicProperty.values[value].conditions[conditionName],
149179
);
150180
}
151181
}
152-
} else {
153-
for (const conditionName in propValue) {
154-
// Conditional style
155-
const value = propValue[conditionName];
182+
} catch (e) {
183+
if (process.env.NODE_ENV !== 'production') {
184+
class SprinklesError extends Error {
185+
constructor(message: string) {
186+
super(message);
187+
this.name = 'SprinklesError';
188+
}
189+
}
156190

157-
if (typeof value === 'string' || typeof value === 'number') {
158-
classNames.push(
159-
atomicProperty.values[value].conditions[conditionName],
191+
const format = (v: string | number) =>
192+
typeof v === 'string' ? `"${v}"` : v;
193+
194+
const invalidPropValue = (
195+
prop: string,
196+
value: string | number,
197+
possibleValues: Array<string | number>,
198+
) => {
199+
throw new SprinklesError(
200+
`"${prop}" has no value ${format(
201+
value,
202+
)}. Possible values are ${Object.keys(possibleValues)
203+
.map(format)
204+
.join(', ')}`,
160205
);
206+
};
207+
208+
if (!atomicProperty) {
209+
throw new SprinklesError(`"${prop}" is not a valid atom property`);
210+
}
211+
212+
if (typeof propValue === 'string' || typeof propValue === 'number') {
213+
if (!(propValue in atomicProperty.values)) {
214+
invalidPropValue(prop, propValue, atomicProperty.values);
215+
}
216+
if (!atomicProperty.values[propValue].defaultClass) {
217+
throw new SprinklesError(
218+
`"${prop}" has no default condition. You must specify which conditions to target explicitly. Possible options are ${Object.keys(
219+
atomicProperty.values[propValue].conditions,
220+
)
221+
.map(format)
222+
.join(', ')}`,
223+
);
224+
}
225+
}
226+
227+
if (typeof propValue === 'object') {
228+
if (
229+
!(
230+
'conditions' in
231+
atomicProperty.values[Object.keys(atomicProperty.values)[0]]
232+
)
233+
) {
234+
throw new SprinklesError(
235+
`"${prop}" is not a conditional property`,
236+
);
237+
}
238+
239+
if (Array.isArray(propValue)) {
240+
if (!('responsiveArray' in atomicProperty)) {
241+
throw new SprinklesError(
242+
`"${prop}" does not support responsive arrays`,
243+
);
244+
}
245+
246+
const breakpointCount = atomicProperty.responsiveArray.length;
247+
if (breakpointCount < propValue.length) {
248+
throw new SprinklesError(
249+
`"${prop}" only supports up to ${breakpointCount} breakpoints. You passed ${propValue.length}`,
250+
);
251+
}
252+
253+
for (const responsiveValue of propValue) {
254+
if (!atomicProperty.values[responsiveValue]) {
255+
invalidPropValue(
256+
prop,
257+
responsiveValue,
258+
atomicProperty.values,
259+
);
260+
}
261+
}
262+
} else {
263+
for (const conditionName in propValue) {
264+
const value = propValue[conditionName];
265+
266+
if (!atomicProperty.values[value]) {
267+
invalidPropValue(prop, value, atomicProperty.values);
268+
}
269+
270+
if (!atomicProperty.values[value].conditions[conditionName]) {
271+
throw new SprinklesError(
272+
`"${prop}" has no condition named ${format(
273+
conditionName,
274+
)}. Possible values are ${Object.keys(
275+
atomicProperty.values[value].conditions,
276+
)
277+
.map(format)
278+
.join(', ')}`,
279+
);
280+
}
281+
}
282+
}
161283
}
162284
}
285+
286+
throw e;
163287
}
164288
}
165289

tests/sprinkles/sprinkles.test.ts

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import {
55
atomicWithPaddingShorthandStyles,
66
atomicWithShorthandStyles,
77
conditionalAtomicStyles,
8+
conditionalStylesWithoutDefaultCondition,
9+
conditionalStylesWithoutResponsiveArray,
810
} from './index.css';
911

1012
describe('sprinkles', () => {
@@ -241,6 +243,131 @@ describe('sprinkles', () => {
241243
});
242244
});
243245

246+
describe('errors', () => {
247+
it('should handle invalid properties', () => {
248+
const atoms = createAtomsFn(conditionalAtomicStyles);
249+
250+
expect(() =>
251+
atoms({
252+
// @ts-expect-error
253+
paddingLefty: 'small',
254+
}),
255+
).toThrowErrorMatchingInlineSnapshot(
256+
`"\\"paddingLefty\\" is not a valid atom property"`,
257+
);
258+
});
259+
260+
it('should handle invalid property values', () => {
261+
const atoms = createAtomsFn(conditionalAtomicStyles);
262+
263+
expect(() =>
264+
atoms({
265+
// @ts-expect-error
266+
paddingLeft: 'xsmall',
267+
}),
268+
).toThrowErrorMatchingInlineSnapshot(
269+
`"\\"paddingLeft\\" is not a valid atom property"`,
270+
);
271+
});
272+
273+
it('should handle conditional objects to unconditional values', () => {
274+
const atoms = createAtomsFn(atomicStyles);
275+
276+
expect(() =>
277+
atoms({
278+
// @ts-expect-error
279+
color: {
280+
mobile: 'red',
281+
},
282+
}),
283+
).toThrowErrorMatchingInlineSnapshot(
284+
`"\\"color\\" is not a conditional property"`,
285+
);
286+
});
287+
288+
it('should handle missing responsive arrays definitions', () => {
289+
const atoms = createAtomsFn(conditionalStylesWithoutResponsiveArray);
290+
291+
expect(() =>
292+
atoms({
293+
// @ts-expect-error
294+
marginTop: ['small'],
295+
}),
296+
).toThrowErrorMatchingInlineSnapshot(
297+
`"\\"marginTop\\" does not support responsive arrays"`,
298+
);
299+
});
300+
301+
it('should handle invalid responsive arrays values', () => {
302+
const atoms = createAtomsFn(conditionalAtomicStyles);
303+
304+
expect(() =>
305+
atoms({
306+
// @ts-expect-error
307+
paddingTop: ['xsmall'],
308+
}),
309+
).toThrowErrorMatchingInlineSnapshot(
310+
`"\\"paddingTop\\" has no value \\"xsmall\\". Possible values are \\"small\\", \\"medium\\", \\"large\\""`,
311+
);
312+
});
313+
314+
it('should handle responsive arrays with too many values', () => {
315+
const atoms = createAtomsFn(conditionalAtomicStyles);
316+
317+
expect(() =>
318+
atoms({
319+
// @ts-expect-error
320+
paddingTop: ['small', 'medium', 'large', 'small'],
321+
}),
322+
).toThrowErrorMatchingInlineSnapshot(
323+
`"\\"paddingTop\\" only supports up to 3 breakpoints. You passed 4"`,
324+
);
325+
});
326+
327+
it('should handle invalid conditional property values', () => {
328+
const atoms = createAtomsFn(conditionalAtomicStyles);
329+
330+
expect(() =>
331+
atoms({
332+
// @ts-expect-error
333+
paddingTop: {
334+
mobile: 'xlarge',
335+
},
336+
}),
337+
).toThrowErrorMatchingInlineSnapshot(
338+
`"\\"paddingTop\\" has no value \\"xlarge\\". Possible values are \\"small\\", \\"medium\\", \\"large\\""`,
339+
);
340+
});
341+
342+
it('should handle properties with no default condition', () => {
343+
const atoms = createAtomsFn(conditionalStylesWithoutDefaultCondition);
344+
345+
expect(() =>
346+
atoms({
347+
// @ts-expect-error
348+
transform: 'shrink',
349+
}),
350+
).toThrowErrorMatchingInlineSnapshot(
351+
`"\\"transform\\" has no default condition. You must specify which conditions to target explicitly. Possible options are \\"active\\""`,
352+
);
353+
});
354+
355+
it('should handle invalid conditions', () => {
356+
const atoms = createAtomsFn(conditionalAtomicStyles);
357+
358+
expect(() =>
359+
atoms({
360+
paddingTop: {
361+
// @ts-expect-error
362+
ultraWide: 'large',
363+
},
364+
}),
365+
).toThrowErrorMatchingInlineSnapshot(
366+
`"\\"paddingTop\\" has no condition named \\"ultraWide\\". Possible values are \\"mobile\\", \\"tablet\\", \\"desktop\\""`,
367+
);
368+
});
369+
});
370+
244371
it('should create atomic styles', () => {
245372
expect(atomicWithShorthandStyles).toMatchInlineSnapshot(`
246373
Object {

0 commit comments

Comments
 (0)