Skip to content

Commit dccb71e

Browse files
authored
XCSS prop options arg (#1541)
* fix: Fixes empty inline object throwing * feat: add type helpers to flag properties and pseudos as required * chore: update * chore: update jsdoc
1 parent 4caa678 commit dccb71e

File tree

5 files changed

+156
-14
lines changed

5 files changed

+156
-14
lines changed

.changeset/purple-flowers-draw.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@compiled/babel-plugin': patch
3+
'@compiled/react': patch
4+
---
5+
6+
Adds third generic for XCSSProp type for declaring what properties and pseudos should be required.

packages/babel-plugin/src/xcss-prop/__tests__/transformation.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,4 +190,22 @@ describe('xcss prop transformation', () => {
190190
"
191191
`);
192192
});
193+
194+
it('should not blow up transforming an empty xcss object', () => {
195+
const result = transform(
196+
`
197+
<Component xcss={{}} />
198+
`
199+
);
200+
201+
expect(result).toMatchInlineSnapshot(`
202+
"import * as React from "react";
203+
import { ax, ix, CC, CS } from "@compiled/react/runtime";
204+
<CC>
205+
<CS>{[]}</CS>
206+
{<Component xcss={undefined} />}
207+
</CC>;
208+
"
209+
`);
210+
});
193211
});

packages/babel-plugin/src/xcss-prop/index.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,26 @@ export const visitXcssPropPath = (path: NodePath<t.JSXOpeningElement>, meta: Met
6969
const cssOutput = buildCss(container.expression, meta);
7070
const { sheets, classNames } = transformCssItems(cssOutput.css, meta);
7171

72-
// Replace xcss prop with class names
73-
// The object has a type constraint to always be a basic object with no values.
74-
container.expression = classNames[0];
72+
switch (classNames.length) {
73+
case 1:
74+
// Replace xcss prop with class names
75+
// Remeber: The object has a type constraint to always be a basic object with no values.
76+
container.expression = classNames[0];
77+
break;
78+
79+
case 0:
80+
// No styles were merged so we replace with an undefined identifier.
81+
container.expression = t.identifier('undefined');
82+
break;
83+
84+
default:
85+
throw buildCodeFrameError(
86+
'Unexpected count of class names please raise an issue on Github',
87+
prop.node,
88+
meta.parentPath
89+
);
90+
}
91+
7592
path.parentPath.replaceWith(compiledTemplate(jsxElementNode, sheets, meta));
7693
} else {
7794
const sheets = collectPassStyles(meta);

packages/react/src/xcss-prop/__tests__/xcss-prop.test.tsx

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,79 @@ describe('xcss prop', () => {
205205
},
206206
});
207207

208-
// @ts-expect-error — Type 'CompiledStyles<{ selectors: { '&:not(:first-child):last-child': { color: "red"; }; }; }>' is not assignable to type 'XCSSProp<"color", "&:hover">'.
209-
expectTypeOf(<CSSPropComponent xcss={styles.primary} />).toBeObject();
208+
expectTypeOf(
209+
<CSSPropComponent
210+
// @ts-expect-error — Type 'CompiledStyles<{ '&:not(:first-child):last-child': { color: "red"; }; }>' is not assignable to type 'undefined'.
211+
xcss={styles.primary}
212+
/>
213+
).toBeObject();
214+
});
215+
216+
it('should mark a xcss prop as required', () => {
217+
function CSSPropComponent({
218+
xcss,
219+
}: {
220+
xcss: XCSSProp<
221+
'color' | 'backgroundColor',
222+
'&:hover',
223+
{ requiredProperties: 'color'; requiredPseudos: never }
224+
>;
225+
}) {
226+
return <div className={xcss}>foo</div>;
227+
}
228+
229+
expectTypeOf(
230+
<CSSPropComponent
231+
// @ts-expect-error — Type '{}' is not assignable to type 'XCSSProp<"backgroundColor" | "color", "&:hover", { requiredProperties: "color"; }>'.
232+
xcss={{}}
233+
/>
234+
).toBeObject();
235+
});
236+
237+
it('should mark a xcss prop inside a pseudo as required', () => {
238+
function CSSPropComponent({
239+
xcss,
240+
}: {
241+
xcss: XCSSProp<
242+
'color' | 'backgroundColor',
243+
'&:hover',
244+
{ requiredProperties: 'color'; requiredPseudos: never }
245+
>;
246+
}) {
247+
return <div className={xcss}>foo</div>;
248+
}
249+
250+
expectTypeOf(
251+
<CSSPropComponent
252+
xcss={{
253+
color: 'red',
254+
// @ts-expect-error — Property 'color' is missing in type '{}' but required in type '{ readonly color: string | number | CompiledPropertyDeclarationReference; }'.
255+
'&:hover': {},
256+
}}
257+
/>
258+
).toBeObject();
259+
});
260+
261+
it('should mark a xcss prop pseudo as required', () => {
262+
function CSSPropComponent({
263+
xcss,
264+
}: {
265+
xcss: XCSSProp<
266+
'color' | 'backgroundColor',
267+
'&:hover',
268+
{ requiredProperties: never; requiredPseudos: '&:hover' }
269+
>;
270+
}) {
271+
return <div className={xcss}>foo</div>;
272+
}
273+
274+
expectTypeOf(
275+
<CSSPropComponent
276+
// @ts-expect-error — Property '"&:hover"' is missing in type '{ color: string; }' but required in type '{ "&:hover": MarkAsRequired<XCSSItem<"backgroundColor" | "color">, never>; }'.
277+
xcss={{
278+
color: 'red',
279+
}}
280+
/>
281+
).toBeObject();
210282
});
211283
});

packages/react/src/xcss-prop/index.ts

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,28 @@ type XCSSItem<TStyleDecl extends keyof CSSProperties> = {
99
: never;
1010
};
1111

12-
type XCSSPseudos<K extends keyof CSSProperties, TPseudos extends CSSPseudos> = {
13-
[Q in CSSPseudos]?: Q extends TPseudos ? XCSSItem<K> : never;
12+
type XCSSPseudos<
13+
TAllowedProperties extends keyof CSSProperties,
14+
TAllowedPseudos extends CSSPseudos,
15+
TRequiredProperties extends { requiredProperties: TAllowedProperties }
16+
> = {
17+
[Q in CSSPseudos]?: Q extends TAllowedPseudos
18+
? MarkAsRequired<XCSSItem<TAllowedProperties>, TRequiredProperties['requiredProperties']>
19+
: never;
1420
};
1521

1622
/**
17-
* We currently block all at rules from xcss prop.
18-
* This needs us to decide on what the final API is across Compiled to be able to set.
23+
* These APIs we don't want to allow to be passed through the `xcss` prop but we also
24+
* must declare them so the (lack-of a) excess property check doesn't bite us and allow
25+
* unexpected values through.
1926
*/
20-
type XCSSAtRules = {
27+
type BlockedRules = {
28+
selectors?: never;
29+
} & {
30+
/**
31+
* We currently block all at rules from xcss prop.
32+
* This needs us to decide on what the final API is across Compiled to be able to set.
33+
*/
2134
[Q in CSS.AtRules]?: never;
2235
};
2336

@@ -68,10 +81,12 @@ export type XCSSAllPseudos = CSSPseudos;
6881
* it means products only pay for styles they use as they're now the ones who declare
6982
* the styles!
7083
*
71-
* The {@link XCSSProp} type has generics which must be defined — of which should be what you
72-
* explicitly want to maintain as API. Use {@link XCSSAllProperties} and {@link XCSSAllPseudos}
84+
* The {@link XCSSProp} type has generics two of which must be defined — use to explicitly
85+
* set want you to maintain as API. Use {@link XCSSAllProperties} and {@link XCSSAllPseudos}
7386
* to enable all properties and pseudos.
7487
*
88+
* The third generic is used to declare what properties and pseudos should be required.
89+
*
7590
* @example
7691
* ```
7792
* interface MyComponentProps {
@@ -86,6 +101,9 @@ export type XCSSAllPseudos = CSSPseudos;
86101
*
87102
* // All properties are accepted, only the hover pseudo is accepted.
88103
* xcss?: XCSSProp<XCSSAllProperties, '&:hover'>;
104+
*
105+
* // The xcss prop is required as well as the color property. No pseudos are required.
106+
* xcss: XCSSProp<XCSSAllProperties, '&:hover', { requiredProperties: 'color', requiredPseudos: never }>;
89107
* }
90108
*
91109
* function MyComponent({ xcss }: MyComponentProps) {
@@ -109,13 +127,24 @@ export type XCSSAllPseudos = CSSPseudos;
109127
*/
110128
export type XCSSProp<
111129
TAllowedProperties extends keyof CSSProperties,
112-
TAllowedPseudos extends CSSPseudos
130+
TAllowedPseudos extends CSSPseudos,
131+
TRequiredProperties extends {
132+
requiredProperties: TAllowedProperties;
133+
requiredPseudos: TAllowedPseudos;
134+
} = never
113135
> =
114-
| (XCSSItem<TAllowedProperties> & XCSSPseudos<TAllowedProperties, TAllowedPseudos> & XCSSAtRules)
136+
| (MarkAsRequired<XCSSItem<TAllowedProperties>, TRequiredProperties['requiredProperties']> &
137+
MarkAsRequired<
138+
XCSSPseudos<TAllowedProperties, TAllowedPseudos, TRequiredProperties>,
139+
TRequiredProperties['requiredPseudos']
140+
> &
141+
BlockedRules)
115142
| false
116143
| null
117144
| undefined;
118145

146+
type MarkAsRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };
147+
119148
/**
120149
* ## cx
121150
*

0 commit comments

Comments
 (0)