Skip to content

Commit

Permalink
XCSS prop options arg (#1541)
Browse files Browse the repository at this point in the history
* fix: Fixes empty inline object throwing

* feat: add type helpers to flag properties and pseudos as required

* chore: update

* chore: update jsdoc
  • Loading branch information
itsdouges authored Oct 26, 2023
1 parent 4caa678 commit dccb71e
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 14 deletions.
6 changes: 6 additions & 0 deletions .changeset/purple-flowers-draw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@compiled/babel-plugin': patch
'@compiled/react': patch
---

Adds third generic for XCSSProp type for declaring what properties and pseudos should be required.
Original file line number Diff line number Diff line change
Expand Up @@ -190,4 +190,22 @@ describe('xcss prop transformation', () => {
"
`);
});

it('should not blow up transforming an empty xcss object', () => {
const result = transform(
`
<Component xcss={{}} />
`
);

expect(result).toMatchInlineSnapshot(`
"import * as React from "react";
import { ax, ix, CC, CS } from "@compiled/react/runtime";
<CC>
<CS>{[]}</CS>
{<Component xcss={undefined} />}
</CC>;
"
`);
});
});
23 changes: 20 additions & 3 deletions packages/babel-plugin/src/xcss-prop/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,26 @@ export const visitXcssPropPath = (path: NodePath<t.JSXOpeningElement>, meta: Met
const cssOutput = buildCss(container.expression, meta);
const { sheets, classNames } = transformCssItems(cssOutput.css, meta);

// Replace xcss prop with class names
// The object has a type constraint to always be a basic object with no values.
container.expression = classNames[0];
switch (classNames.length) {
case 1:
// Replace xcss prop with class names
// Remeber: The object has a type constraint to always be a basic object with no values.
container.expression = classNames[0];
break;

case 0:
// No styles were merged so we replace with an undefined identifier.
container.expression = t.identifier('undefined');
break;

default:
throw buildCodeFrameError(
'Unexpected count of class names please raise an issue on Github',
prop.node,
meta.parentPath
);
}

path.parentPath.replaceWith(compiledTemplate(jsxElementNode, sheets, meta));
} else {
const sheets = collectPassStyles(meta);
Expand Down
76 changes: 74 additions & 2 deletions packages/react/src/xcss-prop/__tests__/xcss-prop.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,79 @@ describe('xcss prop', () => {
},
});

// @ts-expect-error — Type 'CompiledStyles<{ selectors: { '&:not(:first-child):last-child': { color: "red"; }; }; }>' is not assignable to type 'XCSSProp<"color", "&:hover">'.
expectTypeOf(<CSSPropComponent xcss={styles.primary} />).toBeObject();
expectTypeOf(
<CSSPropComponent
// @ts-expect-error — Type 'CompiledStyles<{ '&:not(:first-child):last-child': { color: "red"; }; }>' is not assignable to type 'undefined'.
xcss={styles.primary}
/>
).toBeObject();
});

it('should mark a xcss prop as required', () => {
function CSSPropComponent({
xcss,
}: {
xcss: XCSSProp<
'color' | 'backgroundColor',
'&:hover',
{ requiredProperties: 'color'; requiredPseudos: never }
>;
}) {
return <div className={xcss}>foo</div>;
}

expectTypeOf(
<CSSPropComponent
// @ts-expect-error — Type '{}' is not assignable to type 'XCSSProp<"backgroundColor" | "color", "&:hover", { requiredProperties: "color"; }>'.
xcss={{}}
/>
).toBeObject();
});

it('should mark a xcss prop inside a pseudo as required', () => {
function CSSPropComponent({
xcss,
}: {
xcss: XCSSProp<
'color' | 'backgroundColor',
'&:hover',
{ requiredProperties: 'color'; requiredPseudos: never }
>;
}) {
return <div className={xcss}>foo</div>;
}

expectTypeOf(
<CSSPropComponent
xcss={{
color: 'red',
// @ts-expect-error — Property 'color' is missing in type '{}' but required in type '{ readonly color: string | number | CompiledPropertyDeclarationReference; }'.
'&:hover': {},
}}
/>
).toBeObject();
});

it('should mark a xcss prop pseudo as required', () => {
function CSSPropComponent({
xcss,
}: {
xcss: XCSSProp<
'color' | 'backgroundColor',
'&:hover',
{ requiredProperties: never; requiredPseudos: '&:hover' }
>;
}) {
return <div className={xcss}>foo</div>;
}

expectTypeOf(
<CSSPropComponent
// @ts-expect-error — Property '"&:hover"' is missing in type '{ color: string; }' but required in type '{ "&:hover": MarkAsRequired<XCSSItem<"backgroundColor" | "color">, never>; }'.
xcss={{
color: 'red',
}}
/>
).toBeObject();
});
});
47 changes: 38 additions & 9 deletions packages/react/src/xcss-prop/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,28 @@ type XCSSItem<TStyleDecl extends keyof CSSProperties> = {
: never;
};

type XCSSPseudos<K extends keyof CSSProperties, TPseudos extends CSSPseudos> = {
[Q in CSSPseudos]?: Q extends TPseudos ? XCSSItem<K> : never;
type XCSSPseudos<
TAllowedProperties extends keyof CSSProperties,
TAllowedPseudos extends CSSPseudos,
TRequiredProperties extends { requiredProperties: TAllowedProperties }
> = {
[Q in CSSPseudos]?: Q extends TAllowedPseudos
? MarkAsRequired<XCSSItem<TAllowedProperties>, TRequiredProperties['requiredProperties']>
: never;
};

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

Expand Down Expand Up @@ -68,10 +81,12 @@ export type XCSSAllPseudos = CSSPseudos;
* it means products only pay for styles they use as they're now the ones who declare
* the styles!
*
* The {@link XCSSProp} type has generics which must be defined — of which should be what you
* explicitly want to maintain as API. Use {@link XCSSAllProperties} and {@link XCSSAllPseudos}
* The {@link XCSSProp} type has generics two of which must be defined — use to explicitly
* set want you to maintain as API. Use {@link XCSSAllProperties} and {@link XCSSAllPseudos}
* to enable all properties and pseudos.
*
* The third generic is used to declare what properties and pseudos should be required.
*
* @example
* ```
* interface MyComponentProps {
Expand All @@ -86,6 +101,9 @@ export type XCSSAllPseudos = CSSPseudos;
*
* // All properties are accepted, only the hover pseudo is accepted.
* xcss?: XCSSProp<XCSSAllProperties, '&:hover'>;
*
* // The xcss prop is required as well as the color property. No pseudos are required.
* xcss: XCSSProp<XCSSAllProperties, '&:hover', { requiredProperties: 'color', requiredPseudos: never }>;
* }
*
* function MyComponent({ xcss }: MyComponentProps) {
Expand All @@ -109,13 +127,24 @@ export type XCSSAllPseudos = CSSPseudos;
*/
export type XCSSProp<
TAllowedProperties extends keyof CSSProperties,
TAllowedPseudos extends CSSPseudos
TAllowedPseudos extends CSSPseudos,
TRequiredProperties extends {
requiredProperties: TAllowedProperties;
requiredPseudos: TAllowedPseudos;
} = never
> =
| (XCSSItem<TAllowedProperties> & XCSSPseudos<TAllowedProperties, TAllowedPseudos> & XCSSAtRules)
| (MarkAsRequired<XCSSItem<TAllowedProperties>, TRequiredProperties['requiredProperties']> &
MarkAsRequired<
XCSSPseudos<TAllowedProperties, TAllowedPseudos, TRequiredProperties>,
TRequiredProperties['requiredPseudos']
> &
BlockedRules)
| false
| null
| undefined;

type MarkAsRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };

/**
* ## cx
*
Expand Down

0 comments on commit dccb71e

Please sign in to comment.