-
Notifications
You must be signed in to change notification settings - Fork 67
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
392 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { css, cssMap } from '@compiled/react'; | ||
|
||
const styles = cssMap({ | ||
danger: { | ||
color: 'red', | ||
}, | ||
success: { | ||
color: 'green', | ||
}, | ||
}); | ||
|
||
export default ({ variant, children }) => <div css={css(styles[variant])}>{children}</div>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
142 changes: 142 additions & 0 deletions
142
packages/babel-plugin/src/css-prop/__tests__/css-map.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
import type { TransformOptions } from '../../test-utils'; | ||
import { transform as transformCode } from '../../test-utils'; | ||
|
||
describe('css map behaviour', () => { | ||
beforeAll(() => { | ||
process.env.AUTOPREFIXER = 'off'; | ||
}); | ||
|
||
afterAll(() => { | ||
delete process.env.AUTOPREFIXER; | ||
}); | ||
|
||
const transform = (code: string, opts: TransformOptions = {}) => | ||
transformCode(code, { pretty: false, ...opts }); | ||
|
||
const styles = ` | ||
import { css, cssMap } from '@compiled/react'; | ||
const styles = cssMap({ | ||
danger: { | ||
color: 'red', | ||
backgroundColor: 'red' | ||
}, | ||
success: { | ||
color: 'green', | ||
backgroundColor: 'green' | ||
} | ||
}); | ||
`; | ||
|
||
const defaultErrorMessage = 'Using a CSS Map in this manner is incorrect.'; | ||
const nestedErrorMessage = 'You cannot access a nested CSS Map'; | ||
|
||
describe('valid syntax', () => { | ||
it.only('should evulate css map when variant is a runtime variable', () => { | ||
const actual = transform(` | ||
${styles} | ||
<div css={css(styles[variant])} />; | ||
`); | ||
|
||
expect(actual).toInclude( | ||
'<div className={ax([variant==="danger"&&"_syaz5scu _bfhk5scu",variant==="success"&&"_syazbf54 _bfhkbf54"])}/>' | ||
); | ||
}); | ||
|
||
it('should evulate css map when variant is statically defined', () => { | ||
const actual = transform(` | ||
${styles} | ||
<div css={css(styles.success)} />; | ||
<div css={css(styles['danger'])} />; | ||
`); | ||
|
||
expect(actual).toInclude( | ||
'<div className={ax(["success"==="danger"&&"_syaz5scu _bfhk5scu","success"==="success"&&"_syazbf54 _bfhkbf54"])}/>' | ||
); | ||
expect(actual).toInclude( | ||
'<div className={ax([\'danger\'==="danger"&&"_syaz5scu _bfhk5scu",\'danger\'==="success"&&"_syazbf54 _bfhkbf54"])}/>' | ||
); | ||
}); | ||
|
||
it('should combine CSS Map with other styles', () => { | ||
const actual = transform( | ||
` | ||
${styles} | ||
<div css={css([styles[variant], { color: 'blue' }])} />; | ||
`, | ||
{ pretty: true } | ||
); | ||
|
||
console.log(actual); | ||
|
||
expect(actual).toInclude( | ||
'<div className={ax([variant==="danger"&&"_syaz5scu _bfhk5scu",variant==="success"&&"_syazbf54 _bfhkbf54","_syaz13q2"])}/>' | ||
); | ||
}); | ||
}); | ||
|
||
describe('invalid syntax', () => { | ||
it('does not support TemplateLiteral as object property', () => { | ||
expect(() => { | ||
transform(` | ||
${styles} | ||
<div css={css(styles[\`danger\`])} />; | ||
`); | ||
}).toThrow(defaultErrorMessage); | ||
}); | ||
|
||
it('does not support Expression as object property', () => { | ||
expect(() => { | ||
transform(` | ||
${styles} | ||
<div css={css(styles['dang' + 'er'])} />; | ||
`); | ||
}).toThrow(defaultErrorMessage); | ||
}); | ||
|
||
it('does not support BinaryExpression as object property', () => { | ||
expect(() => { | ||
transform(` | ||
${styles} | ||
<div css={css(styles['dang' + 'er'])} />; | ||
`); | ||
}).toThrow(defaultErrorMessage); | ||
}); | ||
|
||
it('does not support MemberExpression as object property', () => { | ||
expect(() => { | ||
transform(` | ||
${styles} | ||
<div css={css(styles[props.variant])} />; | ||
`); | ||
}).toThrow(defaultErrorMessage); | ||
}); | ||
|
||
it('does not support CallExpression as object property', () => { | ||
expect(() => { | ||
transform(` | ||
${styles} | ||
<div css={css(styles()[variant])} />; | ||
`); | ||
}).toThrow(defaultErrorMessage); | ||
}); | ||
|
||
it('does not support nesting', () => { | ||
expect(() => { | ||
transform(` | ||
${styles} | ||
<div css={css(styles.danger.veryDanger)} />; | ||
`); | ||
}).toThrow(nestedErrorMessage); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
import * as t from '@babel/types'; | ||
|
||
import type { Metadata } from '../types'; | ||
|
||
import { buildCodeFrameError } from './ast'; | ||
import { createResultPair } from './create-result-pair'; | ||
import { evaluateIdentifier } from './traverse-expression/traverse-member-expression/traverse-access-path/resolve-expression/identifier'; | ||
import type { EvaluateExpression } from './types'; | ||
|
||
const createErrorMessage = (message?: string): string => { | ||
return ` | ||
${ | ||
message || 'Using a CSS Map in this manner is incorrect.' | ||
} To correctly implement a CSS Map, follow the syntax below: | ||
\`\`\` | ||
import { css, cssMap } from '@compiled/react'; | ||
const borderStyleMap = cssMap({ | ||
none: { borderStyle: 'none' }, | ||
solid: { borderStyle: 'solid' }, | ||
}); | ||
const Component = ({ borderStyle }) => <div css={css(borderStyleMap[borderStyle])} /> | ||
\`\`\` | ||
`; | ||
}; | ||
|
||
/** | ||
* Retrieves the leftmost identity from a given expression. | ||
* | ||
* For example: | ||
* Given a member expression "colors.primary.500", the function will return "colors". | ||
* | ||
* @param expression The expression to be evaluated. | ||
* @returns {string} The leftmost identity in the expression. | ||
*/ | ||
const findBindingIdentifier = ( | ||
expression: t.Expression | t.V8IntrinsicIdentifier | ||
): t.Identifier | undefined => { | ||
if (t.isIdentifier(expression)) { | ||
return expression; | ||
} else if (t.isCallExpression(expression)) { | ||
return findBindingIdentifier(expression.callee); | ||
} else if (t.isMemberExpression(expression)) { | ||
return findBindingIdentifier(expression.object); | ||
} | ||
|
||
return undefined; | ||
}; | ||
|
||
/** | ||
* Retrieves the CSS Map related information from a given expression. | ||
* | ||
* @param expression The expression to be evaluated. | ||
* @param meta {Metadata} Useful metadata that can be used during the transformation | ||
* @param evaluateExpression {EvaluateExpression} Function that evaluates an expression | ||
*/ | ||
const getCSSmap = ( | ||
expression: t.Expression, | ||
meta: Metadata, | ||
evaluateExpression: EvaluateExpression | ||
): | ||
| { | ||
value: t.ObjectExpression; | ||
meta: Metadata; | ||
property: t.Identifier | t.StringLiteral; | ||
computed: boolean; | ||
} | ||
| undefined => { | ||
// Bail out early if cssMap callExpression doesn't exist in the file | ||
if (!meta.state.compiledImports?.cssMap) return undefined; | ||
|
||
// We only care about member expressions. e.g. variants[variant] | ||
if (!t.isMemberExpression(expression)) return undefined; | ||
|
||
const bindingIdentifier = findBindingIdentifier(expression.object); | ||
|
||
if (!bindingIdentifier) return undefined; | ||
|
||
// Evaluate the binding identifier to get the value of the CSS Map | ||
const { value, meta: updatedMeta } = evaluateIdentifier( | ||
bindingIdentifier, | ||
meta, | ||
evaluateExpression | ||
); | ||
|
||
// Ensure cssMap is used in a correct format. | ||
if ( | ||
t.isCallExpression(value) && | ||
t.isIdentifier(value.callee) && | ||
value.callee.name === meta.state.compiledImports?.cssMap && | ||
value.arguments.length > 0 && | ||
t.isObjectExpression(value.arguments[0]) | ||
) { | ||
// It's CSS Map! We now need to check if the use of the CSS Map is correct. | ||
if (t.isCallExpression(expression.object)) { | ||
throw buildCodeFrameError(createErrorMessage(), expression, updatedMeta.parentPath); | ||
} | ||
|
||
if (t.isMemberExpression(expression.object)) { | ||
throw buildCodeFrameError( | ||
createErrorMessage('You cannot access a nested CSS Map.'), | ||
expression, | ||
updatedMeta.parentPath | ||
); | ||
} | ||
|
||
if (!t.isIdentifier(expression.property) && !t.isStringLiteral(expression.property)) { | ||
throw buildCodeFrameError(createErrorMessage(), expression, updatedMeta.parentPath); | ||
} | ||
|
||
return { | ||
value: value.arguments[0], | ||
property: expression.property, | ||
computed: expression.computed, | ||
meta: updatedMeta, | ||
}; | ||
} | ||
|
||
// It's not a CSS Map, let other code handle it | ||
return undefined; | ||
}; | ||
|
||
export const isCSSMap = ( | ||
expression: t.Expression, | ||
meta: Metadata, | ||
evaluateExpression: EvaluateExpression | ||
): boolean => { | ||
return getCSSmap(expression, meta, evaluateExpression) !== undefined; | ||
}; | ||
|
||
/** | ||
* Transform expression that uses a CSS Map into an array of logical expressions. | ||
* For example: | ||
* ```js | ||
* const borderStyleMap = cssMap({ | ||
* none: { borderStyle: 'none' }, | ||
* solid: { borderStyle: 'solid' }, | ||
* }); | ||
* const Component = ({ borderStyle }) => <div css={css(borderStyleMap[borderStyle])} /> | ||
* ``` | ||
* gets transformed into: | ||
* ```js | ||
* const Component = ({ borderStyle }) => <div css={css([ | ||
* borderStyle === 'none' && { borderStyle: 'none' }, | ||
* borderStyle === 'solid' && { borderStyle: 'solid'} | ||
* ])} /> | ||
* ``` | ||
* Throw an error if a valid CSS Map is not provided. | ||
* | ||
* @param expression Expression we want to evulate. | ||
* @param meta {Metadata} Useful metadata that can be used during the transformation | ||
*/ | ||
export const evaluateCSSMap = ( | ||
expression: t.Expression, | ||
meta: Metadata, | ||
evaluateExpression: EvaluateExpression | ||
): ReturnType<typeof createResultPair> => { | ||
const result = getCSSmap(expression, meta, evaluateExpression); | ||
|
||
// It should never happen because `isCSSMap` should have been checked already. | ||
if (!result) throw buildCodeFrameError(createErrorMessage(), expression, meta.parentPath); | ||
|
||
const { value: objectExpression, property: objectProperty, computed, meta: updatedMeta } = result; | ||
|
||
return createResultPair( | ||
t.arrayExpression( | ||
objectExpression.properties.map((property) => { | ||
if ( | ||
!t.isObjectProperty(property) || | ||
!t.isIdentifier(property.key) || | ||
!t.isExpression(property.value) | ||
) | ||
throw buildCodeFrameError(createErrorMessage(), expression, updatedMeta.parentPath); | ||
|
||
return t.logicalExpression( | ||
'&&', | ||
t.binaryExpression( | ||
'===', | ||
t.isStringLiteral(objectProperty) | ||
? objectProperty | ||
: computed | ||
? objectProperty | ||
: t.stringLiteral(objectProperty.name), | ||
t.stringLiteral(property.key.name) | ||
), | ||
property.value | ||
); | ||
}) | ||
), | ||
updatedMeta | ||
); | ||
}; |
Oops, something went wrong.