import generate from '@babel/generator' ; import * as t from '@babel/types' ; import { addUnitIfNeeded , cssAffixInterpolation } from '@compiled/css' ; import { hash , kebabCase } from '@compiled/utils' ; import type { Metadata } from '../types' ; import { buildCodeFrameError , getKey } from './ast' ; import { evaluateExpression } from './evaluate-expression' ; import { isCompiledCSSCallExpression , isCompiledCSSTaggedTemplateExpression , isCompiledKeyframesCallExpression , isCompiledKeyframesTaggedTemplateExpression , } from './is-compiled' ; import { resolveBinding } from './resolve-binding' ; import type { CSSOutput , CssItem , LogicalCssItem , SheetCssItem , PartialBindingWithMeta , } from './types' ; /**
 * 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,
 * while not breaking how Emotion handles it.
 */ const normalizeContentValue = ( value ) => { if ( value . charAt ( 0 ) !== '"' && value . charAt ( 0 ) !== "'" ) { return `"${ value }"` ; } return value ; } ; param arr const mergeSubsequentUnconditionalCssItems = ( arr ) : CssItem [ ] => { const items = [ ] ; const sheets = [ ] ; for ( let index = 0 ; index < arr . length ; index ++ ) { const item = arr [ index ] ; if ( item . type === 'sheet' ) { sheets . push ( item ) ; continue ; } if ( item . type === 'unconditional' ) { for ( let subsequentIndex = index + 1 ; subsequentIndex < arr . length ; subsequentIndex ++ ) { const subsequentItem = arr [ subsequentIndex ] ; if ( subsequentItem . type === 'unconditional' ) { item . css += subsequentItem . css ; } else if ( subsequentItem . type === 'sheet' ) { sheets . push ( subsequentItem ) ; } else { break ; } index = subsequentIndex ; } } items . push ( item ) ; } return [ ... sheets , ... items ] ; } ; param item export const getItemCss = ( item ) : string => { return item . type === 'conditional' ? [ item . consequent , item . alternate ] . map ( getItemCss ) . join ( '' ) : item . css ; } ; param css param node param type const getLogicalItemFromConditionalExpression = ( css , node , type ) => { const expression = node . test ; return css . map ( ( item ) => { if ( item . type === 'conditional' ) { return item ; } if ( item . type === 'logical' ) { return { ... item , expression : t . logicalExpression ( item . operator , expression , item . expression ) , } ; } /**
         * Ensure that the CSS declarations in the alternate mixin are not evaluated as
         * unconditional rules.
         * Eg ${(props) => (props.isPrimary ? primary : secondary)}; where primary is
         * the consequent node and secondary is the alternate node
         * -> primary mixin will be applied only when props.isPrimary
         * -> secondary mixin will be applied only when !props.isPrimary
         *  */ const alternateExpression = t . unaryExpression ( '!' , expression ) ; const logicalItem = { type : 'logical' , css : getItemCss ( item ) , expression : type === 'consequent' ? expression : alternateExpression , operator : '&&' , } ; return logicalItem ; } ) ; } ; param selector param item const toCSSRuleInternal = ( selector , item ) : CssItem => item . type === 'conditional' ? { ... item , consequent : toCSSRuleInternal ( selector , item . consequent ) , alternate : toCSSRuleInternal ( selector , item . alternate ) , } : { ... item , css : `${ selector } { ${ getItemCss ( item ) } }` } ; param selector param result const toCSSRule = ( selector , result ) => ( { ... result , css : result . css . map ( ( item ) : CssItem => toCSSRuleInternal ( selector , item ) ) , } ) ; param key param items const toCSSDeclarationInternal = ( key , item ) : CssItem => { if ( item . type === 'sheet' ) { return item ; } else if ( item . type === 'conditional' ) { return { ... item , consequent : toCSSDeclarationInternal ( key , item . consequent ) , alternate : toCSSDeclarationInternal ( key , item . alternate ) , } ; } else { return { ... item , css : `${ kebabCase ( key ) }: ${ getItemCss ( item ) };` } ; } } ; param key param result const toCSSDeclaration = ( key , result ) => ( { ... result , css : result . css . map ( ( item ) : CssItem => toCSSDeclarationInternal ( key , item ) ) , } ) ; param node param meta { Metadata } const getVariableDeclaratorValueForOwnPath = ( node , meta ) => { let expression = node ; let variableName = generate ( node ) . code ; meta . ownPath ?. traverse ( { VariableDeclarator ( path ) { if ( t . isIdentifier ( node ) && t . isIdentifier ( path . node . id , { name : node . name } ) ) { const { init } = path . node ; expression = init as t . Expression ; if ( meta . context === 'keyframes' ) { variableName = `${ meta . keyframe }:${ node . name }` ; } else { variableName = init ? generate ( init ) . code : node . name ; } path . stop ( ) ; } } , } ) ; return { expression , variableName , } ; } ; param meta { Metadata } param next { Metadata } const callbackIfFileIncluded = ( meta , next ) => { if ( meta . state . filename !== next . state . filename ) { meta . state . includedFiles . push ( next . state . file . loc . filename ) ; } } ; /**
 * Check if an imported binding resulted in CSS variables after processing.
 *
 * Currently we throw if the found CSS has any variables found from an import
 * This is because we'd need to ensure all identifiers are added to the owning
 * file - if not done they would just error at runtime. Because this isn't a
 * required feature at the moment we're deprioritizing support for this.
 */ const assertNoImportedCssVariables = ( referenceNode , meta , resolvedBinding , buildCssResult ) => { if ( resolvedBinding . source === 'import' && buildCssResult . variables . length > 0 ) { throw buildCodeFrameError ( "Identifier contains values that can't be statically evaluated" , referenceNode , meta . parentPath ) ; } } ; param node param meta { Metadata } const extractConditionalExpression = ( node , meta ) : CSSOutput => { const conditionalPaths = [ 'consequent' , 'alternate' ] ; const css = [ ] ; const variables = [ ] ; const [ consequentCss , alternateCss ] = conditionalPaths . map ( ( path ) => { const pathNode = node [ path ] ; let cssOutput ; if ( t . isObjectExpression ( pathNode ) || ( t . isStringLiteral ( pathNode ) && pathNode . value . includes ( ':' ) ) || ( t . isTemplateLiteral ( pathNode ) && pathNode . quasis . some ( ( quasi ) => quasi . value . raw . includes ( ':' ) ) ) || isCompiledCSSTaggedTemplateExpression ( pathNode , meta . state ) || isCompiledCSSCallExpression ( pathNode , meta . state ) ) { cssOutput = buildCss ( pathNode , meta ) ; } else if ( t . isIdentifier ( pathNode ) ) { const resolved = resolveBinding ( pathNode . name , meta ) ; if ( resolved && t . isExpression ( resolved . node ) && ( isCompiledCSSTaggedTemplateExpression ( resolved . node , resolved . meta . state ) || isCompiledCSSCallExpression ( resolved . node , resolved . meta . state ) ) ) { cssOutput = buildCss ( resolved . node , resolved . meta ) ; assertNoImportedCssVariables ( pathNode , meta , resolved , cssOutput ) ; } } else if ( t . isConditionalExpression ( pathNode ) ) { cssOutput = extractConditionalExpression ( pathNode , meta ) ; } if ( cssOutput ) { variables . push ( ... cssOutput . variables ) ; const mergedOutput = mergeSubsequentUnconditionalCssItems ( cssOutput . css ) ; if ( mergedOutput . length > 1 ) { throw buildCodeFrameError ( 'Conditional branch contains unexpected expression' , node , meta . parentPath ) ; } return mergedOutput [ 0 ] ; } return undefined ; } ) ; if ( consequentCss && alternateCss ) { css . push ( { type : 'conditional' , test : node . test , consequent : consequentCss , alternate : alternateCss , } ) ; } else if ( consequentCss ) { css . push ( ... getLogicalItemFromConditionalExpression ( [ consequentCss ] , node , 'consequent' ) ) ; } else if ( alternateCss ) { css . push ( ... getLogicalItemFromConditionalExpression ( [ alternateCss ] , node , 'alternate' ) ) ; } return { css , variables } ; } ; param node param meta { Metadata } const extractLogicalExpression = ( node , meta ) : CSSOutput => { const variables = [ ] ; const css = [ ] ; if ( t . isExpression ( node . body ) ) { const { value : propValue , meta : updatedMeta } = evaluateExpression ( node . body , meta ) ; const result = buildCss ( propValue , updatedMeta ) ; callbackIfFileIncluded ( meta , updatedMeta ) ; css . push ( ... result . css ) ; variables . push ( ... result . variables ) ; } return { css : mergeSubsequentUnconditionalCssItems ( css ) , variables } ; } ; const extractKeyframes = ( expression , meta ) : CSSOutput => { const { prefix , suffix } = meta ; const name = `k${ hash ( generate ( expression ) . code ) }` ; const selector = `@keyframes ${ name }` ; const { css , variables } = toCSSRule ( selector , buildCss ( t . isCallExpression ( expression ) ? ( expression . arguments as t . Expression [ ] ) : expression . quasi , { ... meta , context : 'keyframes' , keyframe : name } ) ) ; const unexpectedCss = css . filter ( ( item ) => item . type !== 'unconditional' ) ; if ( unexpectedCss . length ) { throw buildCodeFrameError ( 'Keyframes contains unexpected CSS' , expression , meta . parentPath ) ; } return { css : [ { type : 'sheet' , css : css . map ( ( item ) => getItemCss ( item ) ) . join ( '' ) } , { type : 'unconditional' , css : prefix + name + suffix } , ] , variables , } ; } ; param node param meta { Metadata } const extractObjectExpression = ( node , meta ) : CSSOutput => { const css = [ ] ; const variables = [ ] ; node . properties . forEach ( ( prop ) => { if ( t . isObjectProperty ( prop ) ) { const { value : propValue , meta : updatedMeta } = evaluateExpression ( prop . value as t . Expression , meta ) ; callbackIfFileIncluded ( meta , updatedMeta ) ; const key = getKey ( prop . computed ? evaluateExpression ( prop . key , meta ) . value : prop . key ) ; let value = '' ; if ( t . isStringLiteral ( propValue ) ) { value = key === 'content' ? normalizeContentValue ( propValue . value ) : propValue . value ; } else if ( t . isNumericLiteral ( propValue ) ) { value = addUnitIfNeeded ( key , propValue . value ) ; } else if ( t . isObjectExpression ( propValue ) || t . isLogicalExpression ( propValue ) ) { const result = toCSSRule ( key , buildCss ( propValue , updatedMeta ) ) ; css . push ( ... result . css ) ; variables . push ( ... result . variables ) ; return ; } else if ( t . isTemplateLiteral ( propValue ) ) { const result = toCSSDeclaration ( key , extractTemplateLiteral ( propValue , updatedMeta ) ) ; css . push ( ... result . css ) ; variables . push ( ... result . variables ) ; return ; } else if ( isCompiledKeyframesCallExpression ( propValue , updatedMeta . state ) || isCompiledKeyframesTaggedTemplateExpression ( propValue , updatedMeta . state ) ) { const result = extractKeyframes ( propValue , { ... updatedMeta , prefix : `${ kebabCase ( key ) }: ` , suffix : ';' , } ) ; css . push ( ... result . css ) ; variables . push ( ... result . variables ) ; return ; } else { const { expression , variableName } = getVariableDeclaratorValueForOwnPath ( propValue , updatedMeta ) ; const name = `--_${ hash ( variableName ) }` ; variables . push ( { name , expression , } ) ; value = `var(${ name })` ; } css . push ( { type : 'unconditional' , css : `${ kebabCase ( key ) }: ${ value };` } ) ; } else if ( t . isSpreadElement ( prop ) ) { let resolvedBinding = undefined ; if ( t . isIdentifier ( prop . argument ) ) { resolvedBinding = resolveBinding ( prop . argument . name , meta ) ; if ( ! resolvedBinding ) { throw buildCodeFrameError ( 'Variable could not be found' , prop . argument , meta . parentPath ) ; } } const { value : propValue , meta : updatedMeta } = evaluateExpression ( prop . argument , meta ) ; const result = buildCss ( propValue , updatedMeta ) ; callbackIfFileIncluded ( meta , updatedMeta ) ; resolvedBinding && assertNoImportedCssVariables ( prop . argument , meta , resolvedBinding , result ) ; css . push ( ... result . css ) ; variables . push ( ... result . variables ) ; } } ) ; return { css : mergeSubsequentUnconditionalCssItems ( css ) , variables } ; } ; param node param meta { Metadata } const extractTemplateLiteral = ( node , meta ) : CSSOutput => { const css = [ ] ; const variables = [ ] ; const literalResult = node . quasis . reduce < string > ( ( acc , quasi , index ) : string => { const nodeExpression = node . expressions [ index ] as t . Expression | undefined ; if ( ! nodeExpression || ( t . isArrowFunctionExpression ( nodeExpression ) && t . isLogicalExpression ( nodeExpression . body ) ) ) { const suffix = meta . context === 'keyframes' ? '' : ';' ; return acc + quasi . value . raw + suffix ; } const { value : interpolation , meta : updatedMeta } = evaluateExpression ( nodeExpression , meta ) ; callbackIfFileIncluded ( meta , updatedMeta ) ; if ( t . isStringLiteral ( interpolation ) || t . isNumericLiteral ( interpolation ) ) { return acc + quasi . value . raw + interpolation . value ; } if ( t . isObjectExpression ( interpolation ) || isCompiledCSSTaggedTemplateExpression ( interpolation , meta . state ) || isCompiledCSSCallExpression ( interpolation , meta . state ) || ( t . isArrowFunctionExpression ( nodeExpression ) && t . isConditionalExpression ( nodeExpression . body ) ) ) { const result = buildCss ( interpolation , updatedMeta ) ; if ( result . css . length ) { css . push ( { type : 'unconditional' , css : acc + quasi . value . raw } , ... result . css ) ; variables . push ( ... result . variables ) ; return '' ; } } if ( isCompiledKeyframesCallExpression ( interpolation , updatedMeta . state ) || isCompiledKeyframesTaggedTemplateExpression ( interpolation , updatedMeta . state ) ) { const { css : [ keyframesSheet , unconditionalKeyframesItem ] , variables : keyframeVariables , } = extractKeyframes ( interpolation , { ... updatedMeta , prefix : quasi . value . raw , suffix : '' , } ) ; css . push ( keyframesSheet ) ; variables . push ( ... keyframeVariables ) ; return acc + getItemCss ( unconditionalKeyframesItem ) ; } const { expression , variableName } = getVariableDeclaratorValueForOwnPath ( nodeExpression , meta ) ; const name = `--_${ hash ( variableName ) }` ; const nextQuasis = node . quasis [ index + 1 ] ; const [ before , after ] = cssAffixInterpolation ( quasi . value . raw , nextQuasis . value . raw ) ; nextQuasis . value . raw = after . css ; variables . push ( { name , expression , prefix : before . variablePrefix , suffix : after . variableSuffix , } ) ; return acc + before . css + `var(${ name })` ; } , '' ) ; css . push ( { type : 'unconditional' , css : literalResult } ) ; node . expressions . forEach ( ( prop ) => { if ( t . isArrowFunctionExpression ( prop ) ) { if ( t . isLogicalExpression ( prop . body ) ) { const { value : propValue , meta : updatedMeta } = evaluateExpression ( prop . body , meta ) ; const result = buildCss ( propValue , updatedMeta ) ; callbackIfFileIncluded ( meta , updatedMeta ) ; css . push ( ... result . css ) ; variables . push ( ... result . variables ) ; } } } ) ; return { css : mergeSubsequentUnconditionalCssItems ( css ) , variables , } ; } ; param node param meta { Metadata } export const buildCss = ( node , meta ) : CSSOutput => { if ( t . isStringLiteral ( node ) ) { return { css : [ { type : 'unconditional' , css : node . value } ] , variables : [ ] } ; } if ( t . isTemplateLiteral ( node ) ) { return extractTemplateLiteral ( node , meta ) ; } if ( t . isObjectExpression ( node ) ) { return extractObjectExpression ( node , meta ) ; } if ( t . isArrowFunctionExpression ( node ) && t . isLogicalExpression ( node . body ) ) { return extractLogicalExpression ( node , meta ) ; } if ( t . isArrowFunctionExpression ( node ) && t . isConditionalExpression ( node . body ) ) { return extractConditionalExpression ( node . body , meta ) ; } if ( t . isIdentifier ( node ) ) { const resolvedBinding = resolveBinding ( node . name , meta ) ; if ( ! resolvedBinding ) { throw buildCodeFrameError ( 'Variable could not be found' , node , meta . parentPath ) ; } if ( ! t . isExpression ( resolvedBinding . node ) ) { throw buildCodeFrameError ( `${ resolvedBinding . node . type } isn't a supported CSS type - try using an object or string` , node , meta . parentPath ) ; } const result = buildCss ( resolvedBinding . node , resolvedBinding . meta ) ; assertNoImportedCssVariables ( node , meta , resolvedBinding , result ) ; return result ; } if ( t . isArrayExpression ( node ) || Array . isArray ( node ) ) { const css = [ ] ; const variables = [ ] ; const elements = t . isArrayExpression ( node ) ? node . elements : node ; elements . forEach ( ( element ) => { if ( ! t . isExpression ( element ) ) { throw buildCodeFrameError ( `${ element && element . type } isn't a supported CSS type - try using an object or string` , t . isArrayExpression ( node ) ? node : element , meta . parentPath ) ; } const result = t . isConditionalExpression ( element ) ? extractConditionalExpression ( element , meta ) : buildCss ( element , meta ) ; css . push ( ... result . css ) ; variables . push ( ... result . variables ) ; } ) ; return { css , variables , } ; } if ( t . isLogicalExpression ( node ) ) { const expression = node . left ; const result = buildCss ( node . right , meta ) ; const css = result . css . map ( ( item ) => { if ( item . type === 'logical' ) { return { ... item , expression : t . logicalExpression ( item . operator , expression , item . expression ) , } ; } const logicalItem = { type : 'logical' , css : getItemCss ( item ) , expression , operator : node . operator , } ; return logicalItem ; } ) ; return { css , variables : result . variables , } ; } if ( isCompiledCSSTaggedTemplateExpression ( node , meta . state ) ) { return buildCss ( node . quasi , meta ) ; } if ( isCompiledCSSCallExpression ( node , meta . state ) ) { return buildCss ( node . arguments [ 0 ] as t . ObjectExpression , meta ) ; } throw buildCodeFrameError ( `${ node . type } isn't a supported CSS type - try using an object or string` , node , meta . parentPath ) ; } ;