diff --git a/packages/macro/src/__snapshots__/macro.spec.ts.snap b/packages/macro/src/__snapshots__/macro.spec.ts.snap index 61022b8..d39f22d 100644 --- a/packages/macro/src/__snapshots__/macro.spec.ts.snap +++ b/packages/macro/src/__snapshots__/macro.spec.ts.snap @@ -56,6 +56,248 @@ css('Button', { }); +`; + +exports[`@trousers/macro correctly interpolates evaluations (BinaryExpression): correctly interpolates evaluations (BinaryExpression) 1`] = ` + +import { css } from './macro'; +const styles = css('Button', { color: 5+5 }); + +const App = () => +); + + ↓ ↓ ↓ ↓ ↓ ↓ + +/** @jsx jsx */ +import { css, jsx } from '@trousers/macro/runtime'; +const foo = 'blue'; +const styles = css('Button', { + '.Button-4214914708': 'color: var(--interpol0);', +}); + +const App = () => + /*#__PURE__*/ React.createElement( + 'button', + { + css: styles, + styles: { + '--interpol0': foo, + }, + }, + /*#__PURE__*/ React.createElement( + 'span', + { + css: styles, + styles: { + '--interpol0': foo, + }, + }, + 'Hello, World!', + ), + ); + + +`; + +exports[`@trousers/macro correctly interpolates styles used by nested elements: correctly interpolates styles used by nested elements 1`] = ` + +import { css } from './macro'; +const foo = 'blue'; +const bar = 'green'; +const styles = css('Button', { color: foo }); +const innerStyles = css('ButtonInner', { color: bar }); + +const App = () => ( + +); + + ↓ ↓ ↓ ↓ ↓ ↓ + +/** @jsx jsx */ +import { css, jsx } from '@trousers/macro/runtime'; +const foo = 'blue'; +const bar = 'green'; +const styles = css('Button', { + '.Button-4214914708': 'color: var(--interpol0);', +}); +const innerStyles = css('ButtonInner', { + '.ButtonInner-4214944499': 'color: var(--interpol1);', +}); + +const App = () => + /*#__PURE__*/ React.createElement( + 'button', + { + css: styles, + styles: { + '--interpol0': foo, + }, + }, + /*#__PURE__*/ React.createElement( + 'span', + { + css: innerStyles, + styles: { + '--interpol1': bar, + }, + }, + 'Hello, World!', + ), + ); + + +`; + +exports[`@trousers/macro correctly interpolates styles used by sibling elements: correctly interpolates styles used by sibling elements 1`] = ` + +import { css } from './macro'; +const foo = 'blue'; +const bar = 'green'; +const styles = css('Button', { color: foo }); +const siblingStyles = css('ButtonInner', { color: bar }); + +const App = () => ( +
+ + Hello, World! + + +
+); + + ↓ ↓ ↓ ↓ ↓ ↓ + +/** @jsx jsx */ +import { css, jsx } from '@trousers/macro/runtime'; +const foo = 'blue'; +const bar = 'green'; +const styles = css('Button', { + '.Button-4214914708': 'color: var(--interpol0);', +}); +const siblingStyles = css('ButtonInner', { + '.ButtonInner-4214944499': 'color: var(--interpol1);', +}); + +const App = () => + /*#__PURE__*/ React.createElement( + 'div', + null, + /*#__PURE__*/ React.createElement( + 'span', + { + css: siblingStyles, + styles: { + '--interpol1': bar, + }, + }, + 'Hello, World!', + ), + /*#__PURE__*/ React.createElement( + 'button', + { + css: styles, + styles: { + '--interpol0': foo, + }, + }, + 'Submit', + ), + ); + + +`; + +exports[`@trousers/macro correctly interpolates variables (Identifier): correctly interpolates variables (Identifier) 1`] = ` + +import { css } from './macro'; +const foo = 'blue'; +const styles = css('Button', { color: foo }); + +const App = () => + ); +} + + ↓ ↓ ↓ ↓ ↓ ↓ + +/** @jsx jsx */ +import { css, jsx } from '@trousers/macro/runtime'; +import React, { useState } from 'react'; + +const App = () => { + const [foo, setFoo] = useState('blue'); + return /*#__PURE__*/ React.createElement( + 'button', + { + css: css('Button', { + '.Button-4214914708': 'color: var(--interpol0);', + }), + styles: { + '--interpol0': foo, + }, + }, + 'Hello, World!', + ); +}; + + +`; + +exports[`@trousers/macro interpolations are correctly added to an in-use style attribute: interpolations are correctly added to an in-use style attribute 1`] = ` + +import { css } from './macro'; +const foo = 'blue'; +const styles = css('Button', { color: foo }); + +const App = () => ( + +); + + ↓ ↓ ↓ ↓ ↓ ↓ + +/** @jsx jsx */ +import { css, jsx } from '@trousers/macro/runtime'; +const foo = 'blue'; +const styles = css('Button', { + '.Button-4214914708': 'color: var(--interpol0);', +}); + +const App = () => + /*#__PURE__*/ React.createElement( + 'button', + { + css: styles, + styles: { + color: 'red', + '--interpol0': foo, + }, + }, + 'Hello, World!', + ); + + `; exports[`@trousers/macro many modifiers: many modifiers 1`] = ` diff --git a/packages/macro/src/macro.js b/packages/macro/src/macro.js index c286369..7ceb769 100644 --- a/packages/macro/src/macro.js +++ b/packages/macro/src/macro.js @@ -3,18 +3,31 @@ const { parse } = require('@babel/parser'); const { process, themify } = require('@trousers/core'); const hash = require('@trousers/hash').default; -const parseObject = objectExpression => +const parseObject = (objectExpression, onInterpolation = () => {}) => objectExpression.properties.reduce((accum, { key, value }) => { let parsedValue; + // Raw values if ( - value.type === 'StringLiteral' || - value.type === 'NumericLiteral' || - value.type === 'BooleanLiteral' + ['StringLiteral', 'NumericLiteral', 'BooleanLiteral'].includes( + value.type, + ) ) { parsedValue = value.value; - } else { - parsedValue = parseObject(value); + } + + // Variable & function interpolations + if ( + ['Identifier', 'BinaryExpression', 'CallExpression'].includes( + value.type, + ) + ) { + parsedValue = onInterpolation(value); + } + + // Object interpolations + if (value.type === 'ObjectExpression') { + parsedValue = parseObject(value, onInterpolation); } accum[key.name || key.value] = parsedValue; @@ -27,7 +40,12 @@ function macro({ references, babel }) { if (references.css.length === 0) return; + const program = references.css[0].findParent(path => path.isProgram()); + + let interpolationsCount = 0; + references.css.forEach(reference => { + const interpolations = []; const styleBlocks = []; const importName = reference.node.name; @@ -50,7 +68,14 @@ function macro({ references, babel }) { const { arguments: args, callee } = styleBlock.node; const objectExpression = args.length === 2 ? args[1] : args[0]; const type = callee.name || callee.property.name; - const rawStyleBlock = parseObject(objectExpression); + const rawStyleBlock = parseObject( + objectExpression, + interpolation => { + const id = `--interpol${interpolationsCount++}`; + interpolations.push({ reference, id, interpolation }); + return `var(${id})`; + }, + ); const hashedStyles = hash(JSON.stringify(rawStyleBlock)); let id = args.length === 2 ? args[0].value : ''; @@ -91,9 +116,73 @@ function macro({ references, babel }) { ]), ); }); + + // Dynamic interpolations + let jsxOpeningElements = []; + const parentJsxElement = reference.find(path => + path.isJSXOpeningElement(), + ); + if (parentJsxElement) jsxOpeningElements.push(parentJsxElement); + + if (!jsxOpeningElements.length) { + const styleVariable = reference.findParent( + path => path.type === 'VariableDeclarator', + ); + const styleVariableId = styleVariable && styleVariable.node.id.name; + + program.traverse({ + JSXOpeningElement: path => { + const cssAttr = path.node.attributes.find( + attr => + attr.name.name === 'css' && + attr.value.expression.name === styleVariableId, + ); + if (!cssAttr) return; + + jsxOpeningElements.push(path); + }, + }); + } + + jsxOpeningElements.forEach(jsxOpeningElement => { + const stylesAttr = jsxOpeningElement.node.attributes.find( + attr => attr.name.name === 'styles', + ); + + const styleProperties = stylesAttr + ? stylesAttr.value.expression.properties + : []; + + jsxOpeningElement.replaceWith( + t.jsxOpeningElement( + jsxOpeningElement.node.name, + [ + ...jsxOpeningElement.node.attributes.filter( + attr => attr.name.name !== 'styles', + ), + t.jsxAttribute( + t.jsxIdentifier('styles'), + t.jsxExpressionContainer( + t.objectExpression([ + ...styleProperties, + ...interpolations.map( + ({ id, interpolation }) => + t.objectProperty( + t.stringLiteral(id), + interpolation, + ), + ), + ]), + ), + ), + ], + jsxOpeningElement.node.selfClosing, + ), + ); + }); }); - const program = references.css[0].findParent(path => path.isProgram()); + // Import manipulation const importName = references.css[0].node.name; program.node.body.unshift( diff --git a/packages/macro/src/macro.spec.ts b/packages/macro/src/macro.spec.ts index d293327..a37e2a6 100644 --- a/packages/macro/src/macro.spec.ts +++ b/packages/macro/src/macro.spec.ts @@ -5,7 +5,10 @@ pluginTester({ plugin, title: '@trousers/macro', snapshot: true, - babelOptions: { filename: __filename }, + babelOptions: { + filename: __filename, + presets: ['@babel/preset-react'], + }, tests: [ { title: 'element', @@ -124,28 +127,132 @@ pluginTester({ css('Button', { color: 5 }); `, }, - // { - // title: 'correctly interpolates variables (Identifier)', - // code: ` - // import { css } from './macro'; - // const foo = 'blue'; - // css('Button', { color: foo }); - // `, - // }, - // { - // title: 'correctly interpolates functions (CallExpression)', - // code: ` - // import { css } from './macro'; - // const foo = () => 'blue'; - // css('Button', { color: foo() }); - // `, - // }, - // { - // title: 'correctly interpolates evaluations (BooleanExpression)', - // code: ` - // import { css } from './macro'; - // css('Button', { color: 5+5 }); - // `, - // }, + { + title: 'correctly interpolates variables (Identifier)', + code: ` + import { css } from './macro'; + const foo = 'blue'; + const styles = css('Button', { color: foo }); + + const App = () => + ); + `, + }, + { + title: 'correctly interpolates styles used by sibling elements', + code: ` + import { css } from './macro'; + const foo = 'blue'; + const bar = 'green'; + const styles = css('Button', { color: foo }); + const siblingStyles = css('ButtonInner', { color: bar }); + + const App = () => ( +
+ + Hello, World! + + +
+ ); + `, + }, + { + title: 'correctly interpolates reused styles', + code: ` + import { css } from './macro'; + const foo = 'blue'; + const styles = css('Button', { color: foo }); + + const App = () => ( + + ); + `, + }, + { + title: + 'interpolations are correctly added to an in-use style attribute', + code: ` + import { css } from './macro'; + const foo = 'blue'; + const styles = css('Button', { color: foo }); + + const App = () => ( + + ); + `, + }, + { + title: + 'interpolations are correctly added styles directly passed into the css', + code: ` + import React, { useState } from 'react'; + import { css } from './macro'; + + const App = () => { + const [foo, setFoo] = useState('blue'); + + return ( + + ); + } + `, + }, ], }); diff --git a/packages/macro/src/stub/jsx.tsx b/packages/macro/src/stub/jsx.tsx index b250e1e..121cfad 100644 --- a/packages/macro/src/stub/jsx.tsx +++ b/packages/macro/src/stub/jsx.tsx @@ -1,3 +1,5 @@ +/** TODO: this stub might be redundant because the global types should be enough in dev mode */ +/** IDEA: Maybe just re-export the react JSX module here. The code will never be run so who cares right? Also global types come for freee */ import { createElement, ElementType, ReactNode } from 'react'; import { TrousersProps } from '@trousers/react';