diff --git a/examples/webpack/src/app.jsx b/examples/webpack/src/app.jsx
index 3933d16648..9e40ec9eed 100644
--- a/examples/webpack/src/app.jsx
+++ b/examples/webpack/src/app.jsx
@@ -4,6 +4,7 @@ import { Suspense, lazy } from 'react';
import { primary } from './common/constants';
import Annotated from './ui/annotated';
+import CSSMap from './ui/css-map';
import {
CustomFileExtensionStyled,
customFileExtensionCss,
@@ -23,5 +24,6 @@ export const App = () => (
Custom File Extension Styled
Custom File Extension CSS
+ CSS Map
>
);
diff --git a/examples/webpack/src/ui/css-map.jsx b/examples/webpack/src/ui/css-map.jsx
new file mode 100644
index 0000000000..1c7f3bbc1a
--- /dev/null
+++ b/examples/webpack/src/ui/css-map.jsx
@@ -0,0 +1,12 @@
+import { css, cssMap } from '@compiled/react';
+
+const styles = cssMap({
+ danger: {
+ color: 'red',
+ },
+ success: {
+ color: 'green',
+ },
+});
+
+export default ({ variant, children }) => {children}
;
diff --git a/packages/babel-plugin/src/babel-plugin.ts b/packages/babel-plugin/src/babel-plugin.ts
index 3e4635315f..7b9c004b0c 100644
--- a/packages/babel-plugin/src/babel-plugin.ts
+++ b/packages/babel-plugin/src/babel-plugin.ts
@@ -20,6 +20,7 @@ import {
isCompiledKeyframesTaggedTemplateExpression,
isCompiledStyledCallExpression,
isCompiledStyledTaggedTemplateExpression,
+ isCompiledCSSMapCallExpression,
} from './utils/is-compiled';
import { normalizePropsUsage } from './utils/normalize-props-usage';
@@ -150,7 +151,7 @@ export default declare((api) => {
return;
}
- (['styled', 'ClassNames', 'css', 'keyframes'] as const).forEach((apiName) => {
+ (['styled', 'ClassNames', 'css', 'keyframes', 'cssMap'] as const).forEach((apiName) => {
if (
state.compiledImports &&
t.isIdentifier(specifier.node?.imported) &&
@@ -185,7 +186,8 @@ export default declare((api) => {
isCompiledCSSTaggedTemplateExpression(path.node, state) ||
isCompiledKeyframesTaggedTemplateExpression(path.node, state) ||
isCompiledCSSCallExpression(path.node, state) ||
- isCompiledKeyframesCallExpression(path.node, state);
+ isCompiledKeyframesCallExpression(path.node, state) ||
+ isCompiledCSSMapCallExpression(path.node, state);
if (isCompiledUtil) {
state.pathsToCleanup.push({ path, action: 'replace' });
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
new file mode 100644
index 0000000000..d00834f476
--- /dev/null
+++ b/packages/babel-plugin/src/css-prop/__tests__/css-map.test.ts
@@ -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}
+
+ ;
+ `);
+
+ expect(actual).toInclude(
+ ''
+ );
+ });
+
+ it('should evulate 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}
+
+ ;
+ `,
+ { pretty: true }
+ );
+
+ console.log(actual);
+
+ expect(actual).toInclude(
+ ''
+ );
+ });
+ });
+
+ describe('invalid syntax', () => {
+ it('does not support TemplateLiteral as object property', () => {
+ expect(() => {
+ transform(`
+ ${styles}
+
+ ;
+ `);
+ }).toThrow(defaultErrorMessage);
+ });
+
+ it('does not support Expression as object property', () => {
+ expect(() => {
+ transform(`
+ ${styles}
+
+ ;
+ `);
+ }).toThrow(defaultErrorMessage);
+ });
+
+ it('does not support BinaryExpression as object property', () => {
+ expect(() => {
+ transform(`
+ ${styles}
+
+ ;
+ `);
+ }).toThrow(defaultErrorMessage);
+ });
+
+ it('does not support MemberExpression as object property', () => {
+ expect(() => {
+ transform(`
+ ${styles}
+
+ ;
+ `);
+ }).toThrow(defaultErrorMessage);
+ });
+
+ it('does not support CallExpression as object property', () => {
+ expect(() => {
+ transform(`
+ ${styles}
+
+ ;
+ `);
+ }).toThrow(defaultErrorMessage);
+ });
+
+ it('does not support nesting', () => {
+ expect(() => {
+ transform(`
+ ${styles}
+
+ ;
+ `);
+ }).toThrow(nestedErrorMessage);
+ });
+ });
+});
diff --git a/packages/babel-plugin/src/types.ts b/packages/babel-plugin/src/types.ts
index 0a6b2a95a5..79356d5af3 100644
--- a/packages/babel-plugin/src/types.ts
+++ b/packages/babel-plugin/src/types.ts
@@ -98,6 +98,7 @@ export interface State extends PluginPass {
css?: string;
keyframes?: string;
styled?: string;
+ cssMap?: string;
};
importedCompiledImports?: {
diff --git a/packages/babel-plugin/src/utils/css-map.ts b/packages/babel-plugin/src/utils/css-map.ts
new file mode 100644
index 0000000000..cb2d3323ed
--- /dev/null
+++ b/packages/babel-plugin/src/utils/css-map.ts
@@ -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 }) =>
+\`\`\`
+ `;
+};
+
+/**
+ * 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 }) =>
+ * ```
+ * 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.
+ 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
+ );
+};
diff --git a/packages/babel-plugin/src/utils/evaluate-expression.ts b/packages/babel-plugin/src/utils/evaluate-expression.ts
index c2f140ea0a..0b2f83982a 100644
--- a/packages/babel-plugin/src/utils/evaluate-expression.ts
+++ b/packages/babel-plugin/src/utils/evaluate-expression.ts
@@ -5,6 +5,7 @@ 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,
@@ -137,7 +138,9 @@ export const evaluateExpression = (
// there is something we could do better here.
// --------------
- if (t.isIdentifier(targetExpression)) {
+ if (isCSSMap(targetExpression, updatedMeta, evaluateExpression)) {
+ return evaluateCSSMap(targetExpression, updatedMeta, evaluateExpression);
+ } else if (t.isIdentifier(targetExpression)) {
({ value, meta: updatedMeta } = traverseIdentifier(
targetExpression,
updatedMeta,
diff --git a/packages/babel-plugin/src/utils/is-compiled.ts b/packages/babel-plugin/src/utils/is-compiled.ts
index 65a7ecf34b..ddc022018e 100644
--- a/packages/babel-plugin/src/utils/is-compiled.ts
+++ b/packages/babel-plugin/src/utils/is-compiled.ts
@@ -47,6 +47,21 @@ export const isCompiledKeyframesCallExpression = (
t.isIdentifier(node.callee) &&
node.callee.name === state.compiledImports?.keyframes;
+/**
+ * Returns `true` if the node is using `cssMap` from `@compiled/react` as a call expression
+ *
+ * @param node {t.Node} The node that is being checked
+ * @param state {State} Plugin state
+ * @returns {boolean} Whether the node is a compiled cssMap
+ */
+export const isCompiledCSSMapCallExpression = (
+ node: t.Node,
+ state: State
+): node is t.CallExpression =>
+ t.isCallExpression(node) &&
+ t.isIdentifier(node.callee) &&
+ node.callee.name === state.compiledImports?.cssMap;
+
/**
* Returns `true` if the node is using `keyframes` from `@compiled/react` as a tagged template expression
*
diff --git a/packages/react/src/css-map/index.js.flow b/packages/react/src/css-map/index.js.flow
new file mode 100644
index 0000000000..2f39ede368
--- /dev/null
+++ b/packages/react/src/css-map/index.js.flow
@@ -0,0 +1,10 @@
+/**
+ * Flowtype definitions for index
+ * Generated by Flowgen from a Typescript Definition
+ * Flowgen v1.20.1
+ * @flow
+ */
+import type { CSSProps, CssObject } from '../types';
+declare export default function cssMap(_styles: {
+ [key: T]: CssObject | CssObject[],
+}): { [key: T]: CSSProps };
diff --git a/packages/react/src/css-map/index.ts b/packages/react/src/css-map/index.ts
new file mode 100644
index 0000000000..ac2233b8be
--- /dev/null
+++ b/packages/react/src/css-map/index.ts
@@ -0,0 +1,8 @@
+import type { CSSProps, CssObject } from '../types';
+import { createSetupError } from '../utils/error';
+
+export default function cssMap(
+ _styles: Record | CssObject[]>
+): Record> {
+ throw createSetupError();
+}
diff --git a/packages/react/src/index.js.flow b/packages/react/src/index.js.flow
index d6183f0b23..1937810a72 100644
--- a/packages/react/src/index.js.flow
+++ b/packages/react/src/index.js.flow
@@ -10,3 +10,4 @@ declare export { keyframes } from './keyframes';
declare export { styled } from './styled';
declare export { ClassNames } from './class-names';
declare export { default as css } from './css';
+declare export { default as cssMap } from './css-map';
diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts
index 7c038675f8..8b114db02c 100644
--- a/packages/react/src/index.ts
+++ b/packages/react/src/index.ts
@@ -9,6 +9,7 @@ export { keyframes } from './keyframes';
export { styled } from './styled';
export { ClassNames } from './class-names';
export { default as css } from './css';
+export { default as cssMap } from './css-map';
// Pass through the (classic) jsx runtime.
// Compiled currently doesn't define its own and uses this purely to enable a local jsx namespace.