Skip to content

Commit

Permalink
tighten up cssMap type (#1502)
Browse files Browse the repository at this point in the history
* Fix cssmap type

* tighten up cssMap type

* Add stronger type checking, selectors object, split at-rules

* Remove template literal type from css map types

* Remove pseudos with vendor prefixes and pseudos that use information from other elements

* Add tests, minor refactor, improve error messages

* Update flow types

* Minor cleanup

* Update VR tests

* Minor cleanup: s/toThrowError/toThrow, improve comment

---------

Co-authored-by: Grant Wong <[email protected]>
  • Loading branch information
liamqma and dddlr authored Sep 20, 2023
1 parent 2e503a2 commit b6f3e41
Show file tree
Hide file tree
Showing 20 changed files with 1,006 additions and 76 deletions.
5 changes: 5 additions & 0 deletions .changeset/red-yaks-relate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@compiled/react': minor
---

Change `cssMap` types to use stricter type checking and only allowing a limited subset of whitelisted selectors (e.g. `&:hover`); implement syntax for at-rules (e.g. `@media`); implement `selectors` key for non-whitelisted selectors.
Binary file modified .loki/reference/chrome_laptop_css_map_Conditional_Styles.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .loki/reference/chrome_laptop_css_map_Dynamic_Variant.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .loki/reference/chrome_laptop_css_map_Merge_Styles.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .loki/reference/chrome_laptop_css_map_Variant_As_Prop.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 5 additions & 1 deletion examples/parcel/src/ui/css-map.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { css, cssMap } from '@compiled/react';

const base = css({
backgroundColor: 'blue',
});

const styles = cssMap({
danger: {
color: 'red',
Expand All @@ -9,4 +13,4 @@ const styles = cssMap({
},
});

export default ({ variant, children }) => <div css={css(styles[variant])}>{children}</div>;
export default ({ variant, children }) => <div css={[base, styles[variant]]}>{children}</div>;
6 changes: 5 additions & 1 deletion examples/webpack/src/ui/css-map.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { css, cssMap } from '@compiled/react';

const base = css({
backgroundColor: 'blue',
});

const styles = cssMap({
danger: {
color: 'red',
Expand All @@ -9,4 +13,4 @@ const styles = cssMap({
},
});

export default ({ variant, children }) => <div css={css(styles[variant])}>{children}</div>;
export default ({ variant, children }) => <div css={[base, styles[variant]]}>{children}</div>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
import type { TransformOptions } from '../../test-utils';
import { transform as transformCode } from '../../test-utils';
import { ErrorMessages } from '../../utils/css-map';

// Add an example element so we can check the raw CSS styles
const EXAMPLE_USAGE = 'const Element = (variant) => <div css={styles[variant]} />;';

describe('css map advanced functionality (at rules, selectors object)', () => {
const transform = (code: string, opts: TransformOptions = {}) =>
transformCode(code, { pretty: false, ...opts });

it('should parse a mix of at rules and the selectors object', () => {
const actual = transform(`
import { cssMap } from '@compiled/react';
const styles = cssMap({
success: {
color: '#0b0',
'&:hover': {
color: '#060',
},
'@media': {
'screen and (min-width: 500px)': {
fontSize: '10vw',
},
},
selectors: {
span: {
color: 'lightgreen',
'&:hover': {
color: '#090',
},
},
},
},
danger: {
color: 'red',
'&:hover': {
color: 'darkred',
},
'@media': {
'screen and (min-width: 500px)': {
fontSize: '20vw',
},
},
selectors: {
span: {
color: 'orange',
'&:hover': {
color: 'pink',
},
},
},
},
});
${EXAMPLE_USAGE}
`);

expect(actual).toIncludeMultiple([
// Styles from success variant
'._syazjafr{color:#0b0}',
'._30l3aebp:hover{color:#060}',
'@media screen and (min-width:500px){._1takoyl8{font-size:10vw}}',
'._1tjq1v9d span{color:lightgreen}',
'._yzbcy77s span:hover{color:#090}',

// Styles from danger variant
'._syaz5scu{color:red}',
'._30l3qaj3:hover{color:darkred}',
'@media screen and (min-width:500px){._1taki9ra{font-size:20vw}}',
'._1tjqruxl span{color:orange}',
'._yzbc32ev span:hover{color:pink}',

'const styles={success:"_syazjafr _30l3aebp _1takoyl8 _1tjq1v9d _yzbcy77s",danger:"_syaz5scu _30l3qaj3 _1taki9ra _1tjqruxl _yzbc32ev"}',
]);
});

it('should parse selectors object', () => {
const actual = transform(`
import { cssMap } from '@compiled/react';
const styles = cssMap({
success: {
color: '#0b0',
'&:hover': {
color: '#060',
},
},
danger: {
color: 'red',
selectors: {
'&:first-of-type': {
color: 'lightgreen',
'&:hover': {
color: '#090',
},
},
// Hover on child element
'& :hover': {
color: 'orange',
},
},
},
});
${EXAMPLE_USAGE}
`);

expect(actual).toIncludeMultiple([
// Styles from success variant
'._syazjafr{color:#0b0}',
'._30l3aebp:hover{color:#060}',

// Styles from danger variant
'._syaz5scu{color:red}',
'._pnmb1v9d:first-of-type{color:lightgreen}',
'._p685y77s:first-of-type:hover{color:#090}',
'._838lruxl :hover{color:orange}',

'const styles={success:"_syazjafr _30l3aebp",danger:"_syaz5scu _pnmb1v9d _p685y77s _838lruxl"}',
]);
});

it('should error if duplicate selectors passed (inside selectors object and outside)', () => {
expect(() => {
transform(`
import { cssMap } from '@compiled/react';
const styles = cssMap({
success: {
color: '#0b0',
'&:hover': {
color: '#060',
},
selectors: {
'&:hover': {
color: '#ff0',
},
},
},
});
`);
}).toThrow(ErrorMessages.DUPLICATE_SELECTOR);
});

it('should error if duplicate selectors passed using different formats (mixing an identifier and a string literal)', () => {
expect(() => {
transform(`
import { cssMap } from '@compiled/react';
const styles = cssMap({
success: {
color: '#0b0',
// This wouldn't pass the type-checking anyway
div: {
color: '#060',
},
selectors: {
'div': {
color: '#ff0',
},
},
},
});
`);
}).toThrow(ErrorMessages.DUPLICATE_SELECTOR);
});

it('should error if selector targeting current element is passed without ampersand at front', () => {
// :hover (by itself) is identical to &:hover, believe it or not!
// This is due to the parent-orphaned-pseudos plugin in @compiled/css.
expect(() => {
transform(`
import { cssMap } from '@compiled/react';
const styles = cssMap({
success: {
color: '#0b0',
selectors: {
':hover': {
color: 'aquamarine',
},
},
},
});
`);
}).toThrow(ErrorMessages.USE_SELECTORS_WITH_AMPERSAND);
});

it('should error if duplicate selectors passed using both the forms `&:hover` and `:hover`', () => {
expect(() => {
transform(`
import { cssMap } from '@compiled/react';
const styles = cssMap({
success: {
color: '#0b0',
'&:hover': {
color: 'cyan',
},
selectors: {
':hover': {
color: 'aquamarine',
},
},
},
});
`);
}).toThrow(ErrorMessages.USE_SELECTORS_WITH_AMPERSAND);
});

it('should not error if selector has same name as property', () => {
const actual = transform(`
import { cssMap } from '@compiled/react';
const styles = cssMap({
success: {
color: '#0b0',
// All bets are off when we do not know what constitutes
// a valid selector, so we give up in the selectors key
selectors: {
color: {
color: 'pink',
},
fontSize: {
background: 'blue',
},
},
fontSize: '50px',
},
});
${EXAMPLE_USAGE}
`);

expect(actual).toIncludeMultiple([
'._syazjafr{color:#0b0}',
'._14jq32ev color{color:pink}',
'._1wsc13q2 fontSize{background-color:blue}',
'._1wyb12am{font-size:50px}',

'const styles={success:"_syazjafr _1wyb12am _14jq32ev _1wsc13q2"}',
]);
});

it('should parse an at rule (@media)', () => {
const permutations: string[] = [`screen`, `'screen'`];

for (const secondHalf of permutations) {
const actual = transform(`
import { cssMap } from '@compiled/react';
const styles = cssMap({
success: {
color: 'red',
'@media': {
'screen and (min-width: 500px)': {
color: 'blue',
},
${secondHalf}: {
color: 'pink',
},
},
},
});
${EXAMPLE_USAGE}
`);

expect(actual).toIncludeMultiple([
'._syaz5scu{color:red}',
'@media screen and (min-width:500px){._1qhm13q2{color:blue}}',
'@media screen{._434732ev{color:pink}}',

'const styles={success:"_syaz5scu _1qhm13q2 _434732ev"}',
]);
}
});

it('should error if more than one selectors key passed', () => {
expect(() => {
transform(`
import { cssMap } from '@compiled/react';
const styles = cssMap({
success: {
color: 'red',
selectors: {
'&:hover': {
color: '#ff0',
},
},
selectors: {
'&:active': {
color: '#0ff',
},
},
},
});
`);
}).toThrow(ErrorMessages.DUPLICATE_SELECTORS_BLOCK);
});

it('should error if value of selectors key is not an object', () => {
expect(() => {
transform(`
import { cssMap } from '@compiled/react';
const styles = cssMap({
success: {
color: 'red',
selectors: 'blue',
},
});
`);
}).toThrow(ErrorMessages.SELECTORS_BLOCK_VALUE_TYPE);
});
});
16 changes: 14 additions & 2 deletions packages/babel-plugin/src/css-map/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { TransformOptions } from '../../test-utils';
import { transform as transformCode } from '../../test-utils';
import { ErrorMessages } from '../index';
import { ErrorMessages } from '../../utils/css-map';

describe('css map', () => {
describe('css map basic functionality', () => {
const transform = (code: string, opts: TransformOptions = {}) =>
transformCode(code, { pretty: false, ...opts });

Expand Down Expand Up @@ -93,6 +93,18 @@ describe('css map', () => {
}).toThrow(ErrorMessages.NO_OBJECT_METHOD);
});

it('should error out if empty object passed to variant', () => {
expect(() => {
transform(`
import { css, cssMap } from '@compiled/react';
const styles = cssMap({
danger: {}
});
`);
}).toThrow(ErrorMessages.EMPTY_VARIANT_OBJECT);
});

it('should error out if variant object is dynamic', () => {
expect(() => {
transform(`
Expand Down
Loading

0 comments on commit b6f3e41

Please sign in to comment.