diff --git a/packages/pigment-css-react/src/processors/sx.ts b/packages/pigment-css-react/src/processors/sx.ts index 8cac56b2..401d71ed 100644 --- a/packages/pigment-css-react/src/processors/sx.ts +++ b/packages/pigment-css-react/src/processors/sx.ts @@ -1,12 +1,5 @@ import type { NodePath } from '@babel/core'; -import type { - CallExpression, - Expression, - JSXAttribute, - JSXOpeningElement, - ObjectExpression, - ObjectProperty, -} from '@babel/types'; +import type { CallExpression, Expression } from '@babel/types'; import { validateParams, type Params, @@ -18,6 +11,7 @@ import type { IOptions } from './styled'; import { processCssObject } from '../utils/processCssObject'; import { cssFnValueToVariable } from '../utils/cssFnValueToVariable'; import BaseProcessor from './base-processor'; +import spreadSxProp from '../utils/spreadSxProp'; export class SxProcessor extends BaseProcessor { sxArguments: ExpressionValue[] = []; @@ -153,103 +147,10 @@ export class SxProcessor extends BaseProcessor { */ this.replacer((_tagPath) => { const tagPath = _tagPath as NodePath; - const target = tagPath.findParent( - (p) => p.isJSXOpeningElement() || p.isObjectExpression(), - ) as NodePath | NodePath | null; - if (target?.isJSXOpeningElement()) { - const attributes: any[] = []; - let sxAttribute: undefined | NodePath; - (target.get('attributes') as NodePath[]).forEach((attr) => { - if (attr.isJSXAttribute() && attr.node.name.name === 'sx') { - sxAttribute = attr; - } else if (attr.isJSXSpreadAttribute()) { - let containRuntimeSx = false; - attr.traverse({ - CallExpression(path) { - const callee = path.get('callee'); - if (callee.isIdentifier() && callee.node.name.startsWith('_sx')) { - containRuntimeSx = true; - } - }, - }); - if (!containRuntimeSx) { - attributes.push(t.spreadElement(attr.node.argument)); - } - } else if ( - attr.isJSXAttribute() && - (attr.node.name.name === 'className' || attr.node.name.name === 'style') - ) { - const value = attr.get('value'); - if (value.isJSXExpressionContainer()) { - attributes.push( - t.objectProperty( - t.identifier(attr.node.name.name), - value.get('expression').node as any, - ), - ); - } else { - attributes.push( - t.objectProperty(t.identifier(attr.node.name.name), attr.node.value as any), - ); - } - } - }); - if (sxAttribute) { - const expContainer = sxAttribute.get('value'); - if (expContainer.isJSXExpressionContainer()) { - target.node.attributes.push( - t.jsxSpreadAttribute(expContainer.node.expression as Expression), - ); - } - sxAttribute.remove(); - } - tagPath.node.arguments.push(t.objectExpression(attributes)); - return tagPath.node; - } + spreadSxProp(tagPath); - if (target?.isObjectExpression()) { - const properties: any[] = []; - let sxProperty: undefined | NodePath; - (target.get('properties') as NodePath[]).forEach((prop) => { - if ( - prop.isObjectProperty() && - prop.node.key.type === 'Identifier' && - prop.node.key.name === 'sx' - ) { - sxProperty = prop; - } else if (prop.isSpreadElement()) { - let containRuntimeSx = false; - prop.traverse({ - CallExpression(path) { - const callee = path.get('callee'); - if (callee.isIdentifier() && callee.node.name.startsWith('_sx')) { - containRuntimeSx = true; - } - }, - }); - if (!containRuntimeSx) { - properties.push(t.spreadElement(prop.node.argument)); - } - } else if ( - prop.isObjectProperty() && - prop.node.key.type === 'Identifier' && - (prop.node.key.name === 'className' || prop.node.key.name === 'style') - ) { - properties.push( - t.objectProperty(t.identifier(prop.node.key.name), prop.node.value as any), - ); - } - }); - if (sxProperty) { - const expression = sxProperty.get('value'); - target.node.properties.push(t.spreadElement(expression.node as CallExpression)); - sxProperty.remove(); - } - tagPath.node.arguments.push(t.objectExpression(properties)); - return tagPath.node; - } - throw tagPath.buildCodeFrameError('`sx` prop must be used with a JSX element or an object.'); + return tagPath.node; }, false); } diff --git a/packages/pigment-css-react/src/utils/spreadSxProp.ts b/packages/pigment-css-react/src/utils/spreadSxProp.ts new file mode 100644 index 00000000..57caff21 --- /dev/null +++ b/packages/pigment-css-react/src/utils/spreadSxProp.ts @@ -0,0 +1,130 @@ +import { NodePath, types as astService } from '@babel/core'; +import { + CallExpression, + Expression, + JSXAttribute, + JSXOpeningElement, + JSXSpreadAttribute, + ObjectExpression, + ObjectMethod, + ObjectProperty, + SpreadElement, +} from '@babel/types'; + +function isSxProp(path: NodePath) { + if (path.isJSXAttribute() && path.node.name.name === 'sx') { + return true; + } + if ( + path.isObjectProperty() && + path.node.key.type === 'Identifier' && + path.node.key.name === 'sx' + ) { + return true; + } + return false; +} + +function isSpreadExpression(path: NodePath) { + return path.isSpreadElement() || path.isJSXSpreadAttribute(); +} + +function getProps(paths: NodePath[]) { + const props: Array = []; + let sxPath: undefined | NodePath | NodePath; + paths.forEach((attr) => { + if (isSxProp(attr)) { + sxPath = attr as NodePath | NodePath; + } else if (isSpreadExpression(attr)) { + let containRuntimeSx = false; + attr.traverse({ + CallExpression(path) { + const callee = path.get('callee'); + if (callee.isIdentifier() && callee.node.name.startsWith('_sx')) { + containRuntimeSx = true; + } + }, + }); + if (!containRuntimeSx) { + props.push( + astService.spreadElement( + (attr as NodePath).node.argument, + ), + ); + } + } else if ( + attr.isJSXAttribute() && + (attr.node.name.name === 'className' || attr.node.name.name === 'style') + ) { + const value = attr.get('value'); + if (value.isJSXExpressionContainer()) { + props.push( + astService.objectProperty( + astService.identifier(attr.node.name.name), + value.get('expression').node as any, + ), + ); + } else { + props.push( + astService.objectProperty( + astService.identifier(attr.node.name.name), + attr.node.value as any, + ), + ); + } + } else if ( + attr.isObjectProperty() && + attr.node.key.type === 'Identifier' && + (attr.node.key.name === 'className' || attr.node.key.name === 'style') + ) { + props.push( + astService.objectProperty( + astService.identifier(attr.node.key.name), + attr.node.value as any, + ), + ); + } + }); + return { props, sxPath }; +} + +/** + * Convert the sx prop that contains the sx() call to runtime {...sx()} spread. + * + * It will try to find the sibling `className` and `style` props and put them in the second argument of + * the runtime sx call. + */ +export default function spreadSxProp(tagPath: NodePath) { + const target = tagPath.findParent((p) => p.isJSXOpeningElement() || p.isObjectExpression()) as + | NodePath + | NodePath + | null; + if (!target) { + return; + } + let paths: + | NodePath[] + | NodePath[] = []; + if (target.isJSXOpeningElement()) { + paths = target.get('attributes'); + } + if (target.isObjectExpression()) { + paths = target.get('properties'); + } + const { props, sxPath } = getProps(paths); + if (sxPath) { + const expression = sxPath.get('value'); + if ('node' in expression) { + if (target.isObjectExpression()) { + target.node.properties.push(astService.spreadElement(expression.node as Expression)); + } + if (target.isJSXOpeningElement() && expression.isJSXExpressionContainer()) { + target.node.attributes.push( + astService.jsxSpreadAttribute(expression.node.expression as Expression), + ); + } + } + sxPath.remove(); + } + tagPath.node.arguments.push(astService.objectExpression(props)); +}