From 4738f0d589deca813e9a58b0988125c4b1a1e223 Mon Sep 17 00:00:00 2001 From: Liam Ma Date: Wed, 23 Aug 2023 15:14:11 +1000 Subject: [PATCH] Implement alternative compilation method --- packages/babel-plugin/src/babel-plugin.ts | 10 +- .../src/css-map/__tests__/index.test.ts | 135 +++++++++ packages/babel-plugin/src/css-map/index.ts | 153 ++++++++++ .../src/css-prop/__tests__/css-map.test.ts | 216 ++++---------- packages/babel-plugin/src/types.ts | 5 + .../babel-plugin/src/utils/css-builders.ts | 38 +++ packages/babel-plugin/src/utils/css-map.ts | 263 ------------------ .../src/utils/evaluate-expression.ts | 5 +- .../src/utils/transform-css-items.ts | 6 + packages/babel-plugin/src/utils/types.ts | 15 +- 10 files changed, 411 insertions(+), 435 deletions(-) create mode 100644 packages/babel-plugin/src/css-map/__tests__/index.test.ts create mode 100644 packages/babel-plugin/src/css-map/index.ts delete mode 100644 packages/babel-plugin/src/utils/css-map.ts diff --git a/packages/babel-plugin/src/babel-plugin.ts b/packages/babel-plugin/src/babel-plugin.ts index 7b9c004b0..10d2e0a1c 100644 --- a/packages/babel-plugin/src/babel-plugin.ts +++ b/packages/babel-plugin/src/babel-plugin.ts @@ -8,6 +8,7 @@ import * as t from '@babel/types'; import { unique, preserveLeadingComments } from '@compiled/utils'; import { visitClassNamesPath } from './class-names'; +import { visitCssMapPath } from './css-map'; import { visitCssPropPath } from './css-prop'; import { visitStyledPath } from './styled'; import type { State } from './types'; @@ -40,6 +41,7 @@ export default declare((api) => { inherits: jsxSyntax, pre() { this.sheets = {}; + this.cssMap = {}; let cache: Cache; if (this.opts.cache === true) { @@ -172,6 +174,11 @@ export default declare((api) => { path: NodePath | NodePath, state: State ) { + if (isCompiledCSSMapCallExpression(path.node, state)) { + visitCssMapPath(path, { context: 'root', state, parentPath: path }); + return; + } + const hasStyles = isCompiledCSSTaggedTemplateExpression(path.node, state) || isCompiledStyledTaggedTemplateExpression(path.node, state) || @@ -186,8 +193,7 @@ export default declare((api) => { isCompiledCSSTaggedTemplateExpression(path.node, state) || isCompiledKeyframesTaggedTemplateExpression(path.node, state) || isCompiledCSSCallExpression(path.node, state) || - isCompiledKeyframesCallExpression(path.node, state) || - isCompiledCSSMapCallExpression(path.node, state); + isCompiledKeyframesCallExpression(path.node, state); if (isCompiledUtil) { state.pathsToCleanup.push({ path, action: 'replace' }); diff --git a/packages/babel-plugin/src/css-map/__tests__/index.test.ts b/packages/babel-plugin/src/css-map/__tests__/index.test.ts new file mode 100644 index 000000000..d93df96f9 --- /dev/null +++ b/packages/babel-plugin/src/css-map/__tests__/index.test.ts @@ -0,0 +1,135 @@ +import type { TransformOptions } from '../../test-utils'; +import { transform as transformCode } from '../../test-utils'; +import { ErrorMessages } from '../index'; + +describe('css map', () => { + const transform = (code: string, opts: TransformOptions = {}) => + transformCode(code, { pretty: false, ...opts }); + + const styles = `{ + danger: { + color: 'red', + backgroundColor: 'red' + }, + success: { + color: 'green', + backgroundColor: 'green' + } + }`; + + it('should transform css map', () => { + const actual = transform(` + import { cssMap } from '@compiled/react'; + + const styles = cssMap(${styles}); + `); + + expect(actual).toInclude( + 'const styles={danger:"_syaz5scu _bfhk5scu",success:"_syazbf54 _bfhkbf54"};' + ); + }); + + it('should error out if variants are not defined at the top-most scope of the module.', () => { + expect(() => { + transform(` + import { cssMap } from '@compiled/react'; + + const styles = { + map1: cssMap(${styles}), + } + `); + }).toThrow(ErrorMessages.DEFINE_MAP); + + expect(() => { + transform(` + import { cssMap } from '@compiled/react'; + + const styles = () => cssMap(${styles}) + `); + }).toThrow(ErrorMessages.DEFINE_MAP); + }); + + it('should error out if cssMap receives more than one argument', () => { + expect(() => { + transform(` + import { cssMap } from '@compiled/react'; + + const styles = cssMap(${styles}, ${styles}) + `); + }).toThrow(ErrorMessages.NUMBER_OF_ARGUMENT); + }); + + it('should error out if cssMap does not receive an object', () => { + expect(() => { + transform(` + import { cssMap } from '@compiled/react'; + + const styles = cssMap('color: red') + `); + }).toThrow(ErrorMessages.ARGUMENT_TYPE); + }); + + it('should error out if spread element is used', () => { + expect(() => { + transform(` + import { css, cssMap } from '@compiled/react'; + + const styles = cssMap({ + ...base + }); + `); + }).toThrow(ErrorMessages.NO_SPREAD_ELEMENT); + }); + + it('should error out if object method is used', () => { + expect(() => { + transform(` + import { css, cssMap } from '@compiled/react'; + + const styles = cssMap({ + danger() {} + }); + `); + }).toThrow(ErrorMessages.NO_OBJECT_METHOD); + }); + + it('should error out if variant object is dynamic', () => { + expect(() => { + transform(` + import { css, cssMap } from '@compiled/react'; + + const styles = cssMap({ + danger: otherStyles + }); + `); + }).toThrow(ErrorMessages.STATIC_VARIANT_OBJECT); + }); + + it('should error out if styles include runtime variables', () => { + expect(() => { + transform(` + import { css, cssMap } from '@compiled/react'; + + const styles = cssMap({ + danger: { + color: canNotBeStaticallyEvulated + } + }); + `); + }).toThrow(ErrorMessages.STATIC_VARIANT_OBJECT); + }); + + it('should error out if styles include conditional CSS', () => { + expect(() => { + transform(` + import { css, cssMap } from '@compiled/react'; + + const styles = cssMap({ + danger: { + color: canNotBeStaticallyEvulated ? 'red' : 'blue' + } + }); + `); + }).toThrow(ErrorMessages.STATIC_VARIANT_OBJECT); + }); +}); diff --git a/packages/babel-plugin/src/css-map/index.ts b/packages/babel-plugin/src/css-map/index.ts new file mode 100644 index 000000000..21857c7c4 --- /dev/null +++ b/packages/babel-plugin/src/css-map/index.ts @@ -0,0 +1,153 @@ +import type { NodePath } from '@babel/core'; +import * as t from '@babel/types'; + +import type { Metadata } from '../types'; +import { buildCodeFrameError } from '../utils/ast'; +import { buildCss } from '../utils/css-builders'; +import { transformCssItems } from '../utils/transform-css-items'; + +// The messages are exported for testing. +export enum ErrorMessages { + NO_TAGGED_TEMPLATE = 'cssMap function cannot be used as a tagged template expression.', + NUMBER_OF_ARGUMENT = 'cssMap function can only receive one argument.', + ARGUMENT_TYPE = 'cssMap function can only receive an object.', + DEFINE_MAP = 'CSS Map must be declared at the top-most scope of the module.', + NO_SPREAD_ELEMENT = 'Spread element is not supported in CSS Map.', + NO_OBJECT_METHOD = 'Object method is not supported in CSS Map.', + STATIC_VARIANT_OBJECT = 'The variant object must be statically defined.', +} + +const createErrorMessage = (message: string): string => { + return ` +${message} +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 }) =>
+\`\`\` + `; +}; + +/** + * Takes `cssMap` function expression and then transforms it to a record of class names and sheets. + * + * For example: + * ``` + * const styles = cssMap({ + * none: { color: 'red' }, + * solid: { color: 'green' }, + * }); + * ``` + * gets transformed to + * ``` + * const styles = { + * danger: "_syaz5scu", + * success: "_syazbf54", + * }; + * ``` + * + * @param path {NodePath} The path to be evaluated. + * @param meta {Metadata} Useful metadata that can be used during the transformation + */ +export const visitCssMapPath = ( + path: NodePath | NodePath, + meta: Metadata +): void => { + // We don't support tagged template expressions. + if (t.isTaggedTemplateExpression(path.node)) { + throw buildCodeFrameError( + createErrorMessage(ErrorMessages.DEFINE_MAP), + path.node, + meta.parentPath + ); + } + + // We need to ensure CSS Map is declared at the top-most scope of the module. + if (!t.isVariableDeclarator(path.parent) || !t.isIdentifier(path.parent.id)) { + throw buildCodeFrameError( + createErrorMessage(ErrorMessages.DEFINE_MAP), + path.node, + meta.parentPath + ); + } + + // We need to ensure cssMap receives only one argument. + if (path.node.arguments.length !== 1) { + throw buildCodeFrameError( + createErrorMessage(ErrorMessages.NUMBER_OF_ARGUMENT), + path.node, + meta.parentPath + ); + } + + // We need to ensure the argument is an objectExpression. + if (!t.isObjectExpression(path.node.arguments[0])) { + throw buildCodeFrameError( + createErrorMessage(ErrorMessages.ARGUMENT_TYPE), + path.node, + meta.parentPath + ); + } + + const totalSheets: string[] = []; + path.replaceWith( + t.objectExpression( + path.node.arguments[0].properties.map((property) => { + if (t.isSpreadElement(property)) { + throw buildCodeFrameError( + createErrorMessage(ErrorMessages.NO_SPREAD_ELEMENT), + property.argument, + meta.parentPath + ); + } + + if (t.isObjectMethod(property)) { + throw buildCodeFrameError( + createErrorMessage(ErrorMessages.NO_OBJECT_METHOD), + property.key, + meta.parentPath + ); + } + + if (!t.isObjectExpression(property.value)) { + throw buildCodeFrameError( + createErrorMessage(ErrorMessages.STATIC_VARIANT_OBJECT), + property.value, + meta.parentPath + ); + } + + const { css, variables } = buildCss(property.value, meta); + + if (variables.length) { + throw buildCodeFrameError( + createErrorMessage(ErrorMessages.STATIC_VARIANT_OBJECT), + property.value, + meta.parentPath + ); + } + + const { sheets, classNames } = transformCssItems(css, meta); + totalSheets.push(...sheets); + + if (classNames.length !== 1) { + throw buildCodeFrameError( + createErrorMessage(ErrorMessages.STATIC_VARIANT_OBJECT), + property, + meta.parentPath + ); + } + + return t.objectProperty(property.key, classNames[0]); + }) + ) + ); + + // We store sheets in the meta state so that we can use it later to generate Compiled component. + meta.state.cssMap[path.parent.id.name] = totalSheets; +}; diff --git a/packages/babel-plugin/src/css-prop/__tests__/css-map.test.ts b/packages/babel-plugin/src/css-prop/__tests__/css-map.test.ts index ccc2f6a23..64127ff02 100644 --- a/packages/babel-plugin/src/css-prop/__tests__/css-map.test.ts +++ b/packages/babel-plugin/src/css-prop/__tests__/css-map.test.ts @@ -1,6 +1,5 @@ import type { TransformOptions } from '../../test-utils'; import { transform as transformCode } from '../../test-utils'; -import { ErrorMessages } from '../../utils/css-map'; describe('css map behaviour', () => { beforeAll(() => { @@ -29,171 +28,58 @@ describe('css map behaviour', () => { }); `; - describe('valid syntax', () => { - it('should evaluate css map when variant is a runtime variable', () => { - const actual = transform(` + it('should evaluate css map with various syntactic patterns', () => { + const actual = transform( + ` ${styles} - -
; +
; + `, + { pretty: true } + ); + + expect(actual).toMatchInlineSnapshot(` + "import * as React from "react"; + import { ax, ix, CC, CS } from "@compiled/react/runtime"; + const _5 = "._syaz13q2{color:blue}"; + const _4 = "._bfhkbf54{background-color:green}"; + const _3 = "._syazbf54{color:green}"; + const _2 = "._bfhk5scu{background-color:red}"; + const _ = "._syaz5scu{color:red}"; + const styles = { + danger: "_syaz5scu _bfhk5scu", + success: "_syazbf54 _bfhkbf54", + }; + + {[_, _2, _3, _4, _5]} + { +
+ } + ; + " `); - - expect(actual).toInclude( - '
' - ); - }); - - it('should evaluate css map when variant is statically defined', () => { - const actual = transform(` - ${styles} - -
; -
; - `); - - expect(actual).toInclude( - '
' - ); - expect(actual).toInclude( - '
' - ); - }); - - it('should combine CSS Map with other styles', () => { - const actual = transform( - ` - ${styles} - -
; - ` - ); - - expect(actual).toInclude( - '
' - ); - }); - }); - - describe('invalid syntax', () => { - it('does not support TemplateLiteral as object property', () => { - expect(() => { - transform(` - ${styles} - -
; - `); - }).toThrow(ErrorMessages.VARIANT_ACCESS); - }); - - it('does not support BinaryExpression as object property', () => { - expect(() => { - transform(` - ${styles} - -
; - `); - }).toThrow(ErrorMessages.VARIANT_ACCESS); - - expect(() => { - transform(` - ${styles} - -
; - `); - }).toThrow(ErrorMessages.VARIANT_ACCESS); - }); - - it('does not support MemberExpression as object property', () => { - expect(() => { - transform(` - ${styles} - -
; - `); - }).toThrow(ErrorMessages.VARIANT_ACCESS); - }); - - it('does not support CallExpression as object property', () => { - expect(() => { - transform(` - ${styles} - -
; - `); - }).toThrow(ErrorMessages.VARIANT_CALL_EXPRESSION); - }); - - it('does not support nesting', () => { - expect(() => { - transform(` - ${styles} - -
; - `); - }).toThrow(ErrorMessages.NESTED_VARIANT); - }); - - it('should error out if cssMap does not receive any argument', () => { - expect(() => { - transform(` - import { css, cssMap } from '@compiled/react'; - - const styles = cssMap(); - -
; - `); - }).toThrow(ErrorMessages.NUMBER_OF_ARGUMENT); - }); - - it('should error out if cssMap receives more than one argument', () => { - expect(() => { - transform(` - import { css, cssMap } from '@compiled/react'; - - const styles = cssMap({}, {}); - -
; - `); - }).toThrow(ErrorMessages.NUMBER_OF_ARGUMENT); - }); - - it('should error out if cssMap does not receive an object', () => { - expect(() => { - transform(` - import { css, cssMap } from '@compiled/react'; - - const styles = cssMap('color: red'); - -
; - `); - }).toThrow(ErrorMessages.ARGUMENT_TYPE); - }); - - it('should error out if spread element is used', () => { - expect(() => { - transform(` - import { css, cssMap } from '@compiled/react'; - - const styles = cssMap({ - ...base - }); - -
; - `); - }).toThrow(ErrorMessages.STATIC_VARIANT_OBJECT); - }); - - it('should error out if css map object key is dynamic', () => { - expect(() => { - transform(` - import { css, cssMap } from '@compiled/react'; - - const styles = cssMap({ - [variantName]: { color: 'red' } - }); - -
; - `); - }).toThrow(ErrorMessages.STATIC_VARIANT_KEY); - }); }); }); diff --git a/packages/babel-plugin/src/types.ts b/packages/babel-plugin/src/types.ts index 79356d5af..025fcefbd 100644 --- a/packages/babel-plugin/src/types.ts +++ b/packages/babel-plugin/src/types.ts @@ -142,6 +142,11 @@ export interface State extends PluginPass { * Files that have been included in this pass. */ includedFiles: string[]; + + /** + * Holds a record of currently evulated CSS Map and its sheets in the module. + */ + cssMap: Record; } interface CommonMetadata { diff --git a/packages/babel-plugin/src/utils/css-builders.ts b/packages/babel-plugin/src/utils/css-builders.ts index 062f6d7e5..0691ffe79 100644 --- a/packages/babel-plugin/src/utils/css-builders.ts +++ b/packages/babel-plugin/src/utils/css-builders.ts @@ -28,9 +28,33 @@ import type { CssItem, LogicalCssItem, SheetCssItem, + CssMapItem, PartialBindingWithMeta, } from './types'; +/** + * 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; +}; + /** * Will normalize the value of a `content` CSS property to ensure it has quotations around it. * This is done to replicate both how Styled Components behaves, @@ -804,6 +828,13 @@ export const buildCss = (node: t.Expression | t.Expression[], meta: Metadata): C } if (t.isMemberExpression(node)) { + const bindingIdentifier = findBindingIdentifier(node); + if (bindingIdentifier && meta.state.cssMap[bindingIdentifier.name]) { + return { + css: [{ type: 'map', expression: node, name: bindingIdentifier.name, css: '' }], + variables: [], + }; + } const { value, meta: updatedMeta } = evaluateExpression(node, meta); return buildCss(value, updatedMeta); } @@ -877,6 +908,13 @@ export const buildCss = (node: t.Expression | t.Expression[], meta: Metadata): C }; } + if (item.type === 'map') { + return { + ...item, + expression: t.logicalExpression(node.operator, expression, item.expression), + } as CssMapItem; + } + const logicalItem: LogicalCssItem = { type: 'logical', css: getItemCss(item), diff --git a/packages/babel-plugin/src/utils/css-map.ts b/packages/babel-plugin/src/utils/css-map.ts deleted file mode 100644 index 4ef21e125..000000000 --- a/packages/babel-plugin/src/utils/css-map.ts +++ /dev/null @@ -1,263 +0,0 @@ -import assert from 'assert'; - -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'; - -// The messages are exported for testing. -export enum ErrorMessages { - NUMBER_OF_ARGUMENT = 'You must provide cssMap with only one argument.', - ARGUMENT_TYPE = 'cssMap can only receive an object.', - NESTED_VARIANT = 'You cannot access a nested CSS Map.', - VARIANT_CALL_EXPRESSION = 'You cannot use a function call to access a CSS Map.', - VARIANT_ACCESS = "You cannot access a CSS Map this way. Please use a string literal (e.g. styles['variantName']) or an identifier (e.g. styles[variantName]).", - STATIC_VARIANT_OBJECT = 'The variant object must be statically defined.', - STATIC_VARIANT_KEY = 'The variant key must be statically defined.', -} - -const createErrorMessage = (message: string): string => { - return ` -${message} -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 }) =>
-\`\`\` - `; -}; - -/** - * 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 key from a given expression or property from a given member expression. - * - * For example: - * Given a member expression `colors[variant]`, the function will return `variant` (as t.Identifier). - * Given a member expression `colors["variant"]`, the function will return `"variant"` (as t.StringLiteral). - * Given a member expression `colors.variant, the function will return `"variant"` (as t.StringLiteral). - * Given a member expression `colors.variant.nested, the function will return `undefined`. - * Given a object property `{ variant: 'something' }`, the function will return `"variant"` (as t.StringLiteral). - * Given a object property `{ "variant": 'something' }`, the function will return `"variant"` (as t.StringLiteral). - * Given a object property `{ [someDynamicVariable]: 'something' }`, the function will return `undefined`. - * - * @param {t.ObjectProperty | t.MemberExpression} The expression to be evaluated. - * @returns {t.Identifier | t.StringLiteral | undefined} The key or property from the expression. - */ -const getKeyOrProperty = ( - value: t.ObjectProperty | t.MemberExpression -): t.Identifier | t.StringLiteral | undefined => { - const keyOrProperty = t.isObjectProperty(value) ? value.key : value.property; - - if (t.isStringLiteral(keyOrProperty)) { - return keyOrProperty; - } - - if (t.isIdentifier(keyOrProperty)) { - if (t.isObjectProperty(value) && value.computed) return undefined; - return value.computed ? keyOrProperty : t.stringLiteral(keyOrProperty.name); - } - - 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; - property: t.Identifier | t.StringLiteral; - meta: Metadata; - } - | 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 - ) { - // It's CSS Map! We now need to check a few things to ensure CSS Map is correctly used. - // We need to ensure cssMap receives only one argument. - if (value.arguments.length !== 1) { - throw buildCodeFrameError( - createErrorMessage(ErrorMessages.NUMBER_OF_ARGUMENT), - value, - updatedMeta.parentPath - ); - } - - // We need to ensure the argument is an objectExpression. - if (!t.isObjectExpression(value.arguments[0])) { - throw buildCodeFrameError( - createErrorMessage(ErrorMessages.ARGUMENT_TYPE), - value, - updatedMeta.parentPath - ); - } - - // We need to ensure callExpression isn't used to access a variant. - if (t.isCallExpression(expression.object)) { - throw buildCodeFrameError( - createErrorMessage(ErrorMessages.VARIANT_CALL_EXPRESSION), - expression.object, - meta.parentPath - ); - } - - // We disallows a nested CSS Map. e.g. { danger: { veryDanger: { ... } } } - if (t.isMemberExpression(expression.object)) { - throw buildCodeFrameError( - createErrorMessage(ErrorMessages.NESTED_VARIANT), - expression.object, - meta.parentPath - ); - } - - // We need to ensure the cssMap is accessed using a string literal or an identifier. - const property = getKeyOrProperty(expression); - if (!property) { - throw buildCodeFrameError( - createErrorMessage(ErrorMessages.VARIANT_ACCESS), - expression.property, - meta.parentPath - ); - } - - return { - value: value.arguments[0], - property, - 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 }) =>
- * ``` - * gets transformed into: - * ```js - * const Component = ({ borderStyle }) =>
- * ``` - * 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 => { - const result = getCssMap(expression, meta, evaluateExpression); - - // It should never happen because `isCSSMap` should have been checked already. - assert(result !== undefined); - - const { value: cssMapObjectExpression, property: selectedVariant, meta: updatedMeta } = result; - - return createResultPair( - t.arrayExpression( - cssMapObjectExpression.properties.map((property) => { - if (!t.isObjectProperty(property) || !t.isExpression(property.value)) - throw buildCodeFrameError( - createErrorMessage(ErrorMessages.STATIC_VARIANT_OBJECT), - cssMapObjectExpression, - updatedMeta.parentPath - ); - - const variant = getKeyOrProperty(property); - - if (!variant) - throw buildCodeFrameError( - createErrorMessage(ErrorMessages.STATIC_VARIANT_KEY), - cssMapObjectExpression, - updatedMeta.parentPath - ); - - return t.logicalExpression( - '&&', - t.binaryExpression('===', selectedVariant, variant), - property.value - ); - }) - ), - updatedMeta - ); -}; diff --git a/packages/babel-plugin/src/utils/evaluate-expression.ts b/packages/babel-plugin/src/utils/evaluate-expression.ts index 0b2f83982..c2f140ea0 100644 --- a/packages/babel-plugin/src/utils/evaluate-expression.ts +++ b/packages/babel-plugin/src/utils/evaluate-expression.ts @@ -5,7 +5,6 @@ import type { Metadata } from '../types'; import { getPathOfNode } from './ast'; import { createResultPair } from './create-result-pair'; -import { isCSSMap, evaluateCSSMap } from './css-map'; import { isCompiledKeyframesCallExpression } from './is-compiled'; import { traverseBinaryExpression, @@ -138,9 +137,7 @@ export const evaluateExpression = ( // there is something we could do better here. // -------------- - if (isCSSMap(targetExpression, updatedMeta, evaluateExpression)) { - return evaluateCSSMap(targetExpression, updatedMeta, evaluateExpression); - } else if (t.isIdentifier(targetExpression)) { + if (t.isIdentifier(targetExpression)) { ({ value, meta: updatedMeta } = traverseIdentifier( targetExpression, updatedMeta, diff --git a/packages/babel-plugin/src/utils/transform-css-items.ts b/packages/babel-plugin/src/utils/transform-css-items.ts index 02a1cc131..196a3e199 100644 --- a/packages/babel-plugin/src/utils/transform-css-items.ts +++ b/packages/babel-plugin/src/utils/transform-css-items.ts @@ -74,6 +74,12 @@ const transformCssItem = ( ), }; + case 'map': + return { + sheets: meta.state.cssMap[item.name], + classExpression: item.expression, + }; + default: const css = transformCss(getItemCss(item), meta.state.opts); const className = compressClassNamesForRuntime( diff --git a/packages/babel-plugin/src/utils/types.ts b/packages/babel-plugin/src/utils/types.ts index 0bbe4e1ef..f753012f3 100644 --- a/packages/babel-plugin/src/utils/types.ts +++ b/packages/babel-plugin/src/utils/types.ts @@ -27,7 +27,20 @@ export interface SheetCssItem { css: string; } -export type CssItem = UnconditionalCssItem | ConditionalCssItem | LogicalCssItem | SheetCssItem; +export interface CssMapItem { + name: string; + type: 'map'; + expression: t.Expression; + // We can remove this once we local transform other CssItem types + css: string; +} + +export type CssItem = + | UnconditionalCssItem + | ConditionalCssItem + | LogicalCssItem + | SheetCssItem + | CssMapItem; export type Variable = { name: string;