diff --git a/package.json b/package.json index 95689f6..96f2c17 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "start": "run-p start:*", "start:watch": "preconstruct watch", "start:stories": "start-storybook -p 9000 -s assets", - "clean": "rm -rf node_modules/.cache && rimraf packages/*/{tsconfig.tsbuildinfo,lib,dist}", + "clean": "rm -rf node_modules/.cache && rimraf packages/**/{tsconfig.tsbuildinfo,lib,dist}", "validate": "yarn build && yarn lint && yarn monorepo:check && preconstruct validate", "postinstall": "preconstruct dev && yarn monorepo:check", "monorepo:check": "manypkg check", diff --git a/packages/macro/src/macro.js b/packages/macro/src/macro.js index f66f930..3fd6a12 100644 --- a/packages/macro/src/macro.js +++ b/packages/macro/src/macro.js @@ -3,6 +3,14 @@ const { parse } = require('@babel/parser'); const { process, themify } = require('@trousers/core'); const hash = require('@trousers/hash').default; +const libraryMeta = { + runtimeComponent: 'TrousersNested', + runtimeModulePath: '@trousers/macro/runtime', +}; + +const findJsxAttribute = (path, attributeName) => + path.node.attributes.find(attr => attr.name.name === attributeName); + const parseObject = (objectExpression, onInterpolation = () => {}) => objectExpression.properties.reduce((accum, { key, value }) => { let parsedValue; @@ -41,11 +49,9 @@ function macro({ references, babel }) { if (references.css.length === 0) return; const program = references.css[0].findParent(path => path.isProgram()); + const interpolations = []; - let interpolationsCount = 0; - - references.css.forEach(reference => { - const interpolations = []; + references.css.forEach((reference, index) => { const styleBlocks = []; const importName = reference.node.name; @@ -71,8 +77,12 @@ function macro({ references, babel }) { const rawStyleBlock = parseObject( objectExpression, interpolation => { - const id = `--interpol${interpolationsCount++}`; - interpolations.push({ reference, id, interpolation }); + const id = `--interpol${interpolations.length}`; + interpolations.push({ + referenceIndex: index, + id, + value: interpolation, + }); return `var(${id})`; }, ); @@ -84,8 +94,8 @@ function macro({ references, babel }) { switch (type) { case importName: - elementId = id; - className = `.${id && id + '-'}${hashedStyles}`; + elementId = `${id && id + '-'}${hashedStyles}`; + className = `.${elementId}`; processedStyles = process(className, rawStyleBlock); break; case 'modifier': @@ -116,76 +126,80 @@ 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', - ); + program.traverse({ + JSXOpeningElement: path => { + const cssAttr = findJsxAttribute(path, 'css'); + if (!cssAttr) return; + const cssPropExpression = cssAttr.value.expression; + const stylesAttr = findJsxAttribute(path, 'styles'); const styleProperties = stylesAttr ? stylesAttr.value.expression.properties : []; - // ERROR: This code runs over opening elements multiple times.... - // need to refactor this whole damn thing - jsxOpeningElement.replaceWith( + const interpolationProperties = interpolations + .filter(({ referenceIndex }) => { + let matchedReferenceIndex = -1; + references.css.forEach((referencePath, i) => { + // collector passed directly into css prop + if ( + cssPropExpression.callee && + cssPropExpression.callee.name === + referencePath.node.name && + cssPropExpression.callee.start === + referencePath.node.start + ) { + matchedReferenceIndex = i; + } + + // collector variable passed into css prop + const variableDeclarator = referencePath.find(p => + p.isVariableDeclarator(), + ); + + if ( + variableDeclarator.node.id.name === + cssPropExpression.name + ) { + matchedReferenceIndex = i; + } + }); + + return referenceIndex === matchedReferenceIndex; + }) + .map(({ id, value }) => + t.objectProperty(t.stringLiteral(id), value), + ); + + path.replaceWith( t.jsxOpeningElement( - t.jsxIdentifier('TrousersNested'), + t.jsxIdentifier(libraryMeta.runtimeComponent), [ - ...jsxOpeningElement.node.attributes.filter( + ...path.node.attributes.filter( attr => attr.name.name !== 'styles', ), t.jsxAttribute( t.jsxIdentifier('elementType'), - t.stringLiteral(jsxOpeningElement.node.name.name), + t.stringLiteral(path.node.name.name), ), t.jsxAttribute( - t.jsxIdentifier('styles'), + t.jsxIdentifier('style'), t.jsxExpressionContainer( t.objectExpression([ ...styleProperties, - ...interpolations.map( - ({ id, interpolation }) => - t.objectProperty( - t.stringLiteral(id), - interpolation, - ), - ), + ...interpolationProperties, ]), ), ), ], - jsxOpeningElement.node.selfClosing, + path.node.selfClosing, ), ); - }); + + path.skip(); + }, }); // Import manipulation @@ -199,11 +213,11 @@ function macro({ references, babel }) { t.identifier(importName), ), t.importSpecifier( - t.identifier('TrousersNested'), - t.identifier('TrousersNested'), + t.identifier(libraryMeta.runtimeComponent), + t.identifier(libraryMeta.runtimeComponent), ), ], - t.stringLiteral('@trousers/macro/runtime'), + t.stringLiteral(libraryMeta.runtimeModulePath), ), ); diff --git a/packages/macro/src/macro.spec.ts b/packages/macro/src/macro.spec.ts index f27f338..f5a91b0 100644 --- a/packages/macro/src/macro.spec.ts +++ b/packages/macro/src/macro.spec.ts @@ -17,6 +17,7 @@ describe('macro', () => { describe('when parsing css collectors', () => { it('should process element collector', () => { const result = transform` + import React from 'react'; import { css } from './macro'; const styles = css('Button', { color: 'blue' }); const App = () => ; @@ -25,6 +26,7 @@ describe('macro', () => { expect(result).toMatchInlineSnapshot(` "import { jsx as _jsx } from \\"react/jsx-runtime\\"; import { css, TrousersNested } from \\"@trousers/macro/runtime\\"; + import React from 'react'; const styles = css(\\"Button\\", { \\".Button-2561700995\\": \\"color: blue;\\" }); @@ -32,7 +34,7 @@ describe('macro', () => { const App = () => /*#__PURE__*/_jsx(TrousersNested, { css: styles, elementType: \\"button\\", - styles: {}, + style: {}, children: \\"Submit\\" });" `); @@ -40,6 +42,7 @@ describe('macro', () => { it('should process element collector without an identifier', () => { const result = transform` + import React from 'react'; import { css } from './macro'; const styles = css({ color: 'blue' }); const App = () => ; @@ -48,6 +51,7 @@ describe('macro', () => { expect(result).toMatchInlineSnapshot(` "import { jsx as _jsx } from \\"react/jsx-runtime\\"; import { css, TrousersNested } from \\"@trousers/macro/runtime\\"; + import React from 'react'; const styles = css(\\"\\", { \\".2561700995\\": \\"color: blue;\\" }); @@ -55,7 +59,7 @@ describe('macro', () => { const App = () => /*#__PURE__*/_jsx(TrousersNested, { css: styles, elementType: \\"button\\", - styles: {}, + style: {}, children: \\"Submit\\" });" `); @@ -63,6 +67,7 @@ describe('macro', () => { it('should process collector with a single modifier', () => { const result = transform` + import React from 'react'; import { css } from './macro'; const styles = css('Button', { color: 'blue' }) .modifier('primary', { color: 'brown' }); @@ -72,17 +77,18 @@ describe('macro', () => { expect(result).toMatchInlineSnapshot(` "import { jsx as _jsx } from \\"react/jsx-runtime\\"; import { css, TrousersNested } from \\"@trousers/macro/runtime\\"; + import React from 'react'; const styles = css(\\"Button\\", { \\".Button-2561700995\\": \\"color: blue;\\" }).modifier(\\"primary\\", { - \\".Button--primary-2270159875\\": \\"color: brown;\\" + \\".Button-2561700995--primary-2270159875\\": \\"color: brown;\\" }); const App = () => /*#__PURE__*/_jsx(TrousersNested, { css: styles, $primary: true, elementType: \\"button\\", - styles: {}, + style: {}, children: \\"Submit\\" });" `); @@ -90,6 +96,7 @@ describe('macro', () => { it('should process collector with multiple modifiers', () => { const result = transform` + import React from 'react'; import { css } from './macro'; const styles = css('Button', { color: 'blue' }) .modifier('primary', { color: 'brown' }) @@ -104,12 +111,13 @@ describe('macro', () => { expect(result).toMatchInlineSnapshot(` "import { jsx as _jsx } from \\"react/jsx-runtime\\"; import { css, TrousersNested } from \\"@trousers/macro/runtime\\"; + import React from 'react'; const styles = css(\\"Button\\", { \\".Button-2561700995\\": \\"color: blue;\\" }).modifier(\\"primary\\", { - \\".Button--primary-2270159875\\": \\"color: brown;\\" + \\".Button-2561700995--primary-2270159875\\": \\"color: brown;\\" }).modifier(\\"secondary\\", { - \\".Button--secondary-3026956261\\": \\"color: purple;\\" + \\".Button-2561700995--secondary-3026956261\\": \\"color: purple;\\" }); const App = ({ @@ -122,7 +130,7 @@ describe('macro', () => { $primary: primary, $secondary: secondary, elementType: \\"button\\", - styles: {}, + style: {}, children: \\"Submit\\" }); };" @@ -131,6 +139,7 @@ describe('macro', () => { it('should process collector with many modifiers', () => { const result = transform` + import React from 'react'; import { css } from './macro'; const styles = css('Button', { color: 'blue' }) .modifier('primary', { color: 'brown' }) @@ -143,22 +152,23 @@ describe('macro', () => { expect(result).toMatchInlineSnapshot(` "import { jsx as _jsx } from \\"react/jsx-runtime\\"; import { css, TrousersNested } from \\"@trousers/macro/runtime\\"; + import React from 'react'; const styles = css(\\"Button\\", { \\".Button-2561700995\\": \\"color: blue;\\" }).modifier(\\"primary\\", { - \\".Button--primary-2270159875\\": \\"color: brown;\\" + \\".Button-2561700995--primary-2270159875\\": \\"color: brown;\\" }).modifier(\\"secondary\\", { - \\".Button--secondary-3026956261\\": \\"color: purple;\\" + \\".Button-2561700995--secondary-3026956261\\": \\"color: purple;\\" }).modifier(\\"tertiary\\", { - \\".Button--tertiary-41860765\\": \\"color: yellow;\\" + \\".Button-2561700995--tertiary-41860765\\": \\"color: yellow;\\" }).modifier(\\"quaternary\\", { - \\".Button--quaternary-2402939536\\": \\"color: green;\\" + \\".Button-2561700995--quaternary-2402939536\\": \\"color: green;\\" }); const App = () => /*#__PURE__*/_jsx(TrousersNested, { css: styles, elementType: \\"button\\", - styles: {}, + style: {}, children: \\"Submit\\" });" `); @@ -166,6 +176,7 @@ describe('macro', () => { it('should process collector with a theme', () => { const result = transform` + import React from 'react'; import { css } from './macro'; const styles = css('Button', { color: 'var(--brand-background)' }) .theme({ brand: { background: 'green' }}); @@ -175,16 +186,17 @@ describe('macro', () => { expect(result).toMatchInlineSnapshot(` "import { jsx as _jsx } from \\"react/jsx-runtime\\"; import { css, TrousersNested } from \\"@trousers/macro/runtime\\"; + import React from 'react'; const styles = css(\\"Button\\", { \\".Button-2140373281\\": \\"color: var(--brand-background);\\" }).theme(\\"\\", { - \\".theme-Button-1537299292\\": \\"--brand-background: green;\\" + \\".theme-Button-2140373281-1537299292\\": \\"--brand-background: green;\\" }); const App = () => /*#__PURE__*/_jsx(TrousersNested, { css: styles, elementType: \\"button\\", - styles: {}, + style: {}, children: \\"Submit\\" });" `); @@ -192,6 +204,7 @@ describe('macro', () => { it('should process collector with a deeply nested theme', () => { const result = transform` + import React from 'react'; import { css } from './macro'; const styles = css('Button', { color: 'var(--brand-background)' }) .theme({ @@ -207,16 +220,17 @@ describe('macro', () => { expect(result).toMatchInlineSnapshot(` "import { jsx as _jsx } from \\"react/jsx-runtime\\"; import { css, TrousersNested } from \\"@trousers/macro/runtime\\"; + import React from 'react'; const styles = css(\\"Button\\", { \\".Button-2140373281\\": \\"color: var(--brand-background);\\" }).theme(\\"\\", { - \\".theme-Button-3270977004\\": \\"--neutral: #fff;--brand-forground: yellow;--brand-background: green;\\" + \\".theme-Button-2140373281-3270977004\\": \\"--neutral: #fff;--brand-forground: yellow;--brand-background: green;\\" }); const App = () => /*#__PURE__*/_jsx(TrousersNested, { css: styles, elementType: \\"button\\", - styles: {}, + style: {}, children: \\"Submit\\" });" `); @@ -224,6 +238,7 @@ describe('macro', () => { it('should process collector with a global', () => { const result = transform` + import React from 'react'; import { css } from './macro'; const styles = css('Button', {}) .global({ @@ -237,14 +252,15 @@ describe('macro', () => { expect(result).toMatchInlineSnapshot(` "import { jsx as _jsx } from \\"react/jsx-runtime\\"; import { css, TrousersNested } from \\"@trousers/macro/runtime\\"; - const styles = css(\\"Button\\", {}).global(\\"global-Button-480010618\\", { + import React from 'react'; + const styles = css(\\"Button\\", {}).global(\\"global-Button-3938-480010618\\", { \\":root\\": \\"background-color: red;\\" }); const App = () => /*#__PURE__*/_jsx(TrousersNested, { css: styles, elementType: \\"button\\", - styles: {}, + style: {}, children: \\"Submit\\" });" `); @@ -268,7 +284,7 @@ describe('macro', () => { const App = () => /*#__PURE__*/_jsx(TrousersNested, { css: styles, elementType: \\"button\\", - styles: {}, + style: {}, children: \\"Submit\\" });" `); @@ -276,12 +292,14 @@ describe('macro', () => { it('should do nothing in the case of an unused import', () => { const result = transform` + import React from 'react'; import { css } from './macro'; const App = () => ; `; expect(result).toMatchInlineSnapshot(` "import { jsx as _jsx } from \\"react/jsx-runtime\\"; + import React from 'react'; const App = () => /*#__PURE__*/_jsx(\\"button\\", { children: \\"Submit\\" @@ -291,8 +309,9 @@ describe('macro', () => { }); describe('when interpolations are detected', () => { - it('should correctly render elements without interpolations', () => { + it('should render elements without interpolations', () => { const result = transform` + import React from 'react'; import { css } from './macro'; const styles = css('Button', { color: 'red' }); const App = () => ; @@ -301,6 +320,7 @@ describe('macro', () => { expect(result).toMatchInlineSnapshot(` "import { jsx as _jsx } from \\"react/jsx-runtime\\"; import { css, TrousersNested } from \\"@trousers/macro/runtime\\"; + import React from 'react'; const styles = css(\\"Button\\", { \\".Button-2313942302\\": \\"color: red;\\" }); @@ -308,14 +328,15 @@ describe('macro', () => { const App = () => /*#__PURE__*/_jsx(TrousersNested, { css: styles, elementType: \\"button\\", - styles: {}, + style: {}, children: \\"Submit\\" });" `); }); - it('should correctly interpolate booleans (BooleanLiteral)', () => { + it('should interpolate booleans (BooleanLiteral)', () => { const result = transform` + import React from 'react'; import { css } from './macro'; const styles = css('Button', { color: true }); const App = () => ; @@ -324,6 +345,7 @@ describe('macro', () => { expect(result).toMatchInlineSnapshot(` "import { jsx as _jsx } from \\"react/jsx-runtime\\"; import { css, TrousersNested } from \\"@trousers/macro/runtime\\"; + import React from 'react'; const styles = css(\\"Button\\", { \\".Button-3336976155\\": \\"color: true;\\" }); @@ -331,14 +353,15 @@ describe('macro', () => { const App = () => /*#__PURE__*/_jsx(TrousersNested, { css: styles, elementType: \\"button\\", - styles: {}, + style: {}, children: \\"Submit\\" });" `); }); - it('should correctly interpolate numbers (NumericLiteral)', () => { + it('should interpolate numbers (NumericLiteral)', () => { const result = transform` + import React from 'react'; import { css } from './macro'; const styles = css('Button', { color: 5 }); const App = () => ; @@ -347,6 +370,7 @@ describe('macro', () => { expect(result).toMatchInlineSnapshot(` "import { jsx as _jsx } from \\"react/jsx-runtime\\"; import { css, TrousersNested } from \\"@trousers/macro/runtime\\"; + import React from 'react'; const styles = css(\\"Button\\", { \\".Button-1906181116\\": \\"color: 5;\\" }); @@ -354,14 +378,15 @@ describe('macro', () => { const App = () => /*#__PURE__*/_jsx(TrousersNested, { css: styles, elementType: \\"button\\", - styles: {}, + style: {}, children: \\"Submit\\" });" `); }); - it('should correctly interpolate variables (Identifier)', () => { + it('should interpolate variables (Identifier)', () => { const result = transform` + import React from 'react'; import { css } from './macro'; const foo = 'blue'; const styles = css('Button', { color: foo }); @@ -371,6 +396,7 @@ describe('macro', () => { expect(result).toMatchInlineSnapshot(` "import { jsx as _jsx } from \\"react/jsx-runtime\\"; import { css, TrousersNested } from \\"@trousers/macro/runtime\\"; + import React from 'react'; const foo = 'blue'; const styles = css(\\"Button\\", { \\".Button-4214914708\\": \\"color: var(--interpol0);\\" @@ -379,7 +405,7 @@ describe('macro', () => { const App = () => /*#__PURE__*/_jsx(TrousersNested, { css: styles, elementType: \\"button\\", - styles: { + style: { \\"--interpol0\\": foo }, children: \\"Submit\\" @@ -387,8 +413,9 @@ describe('macro', () => { `); }); - it('should correctly interpolate functions (CallExpression)', () => { + it('should interpolate functions (CallExpression)', () => { const result = transform` + import React from 'react'; import { css } from './macro'; const styles = css('Button', { color: foo() }); const App = () => ; @@ -397,6 +424,7 @@ describe('macro', () => { expect(result).toMatchInlineSnapshot(` "import { jsx as _jsx } from \\"react/jsx-runtime\\"; import { css, TrousersNested } from \\"@trousers/macro/runtime\\"; + import React from 'react'; const styles = css(\\"Button\\", { \\".Button-4214914708\\": \\"color: var(--interpol0);\\" }); @@ -404,7 +432,7 @@ describe('macro', () => { const App = () => /*#__PURE__*/_jsx(TrousersNested, { css: styles, elementType: \\"button\\", - styles: { + style: { \\"--interpol0\\": foo() }, children: \\"Submit\\" @@ -412,8 +440,9 @@ describe('macro', () => { `); }); - it('should correctly interpolate evaluations (BinaryExpression)', () => { + it('should interpolate evaluations (BinaryExpression)', () => { const result = transform` + import React from 'react'; import { css } from './macro'; const styles = css('Button', { color: 5+5 }); const App = () => ; @@ -422,6 +451,7 @@ describe('macro', () => { expect(result).toMatchInlineSnapshot(` "import { jsx as _jsx } from \\"react/jsx-runtime\\"; import { css, TrousersNested } from \\"@trousers/macro/runtime\\"; + import React from 'react'; const styles = css(\\"Button\\", { \\".Button-4214914708\\": \\"color: var(--interpol0);\\" }); @@ -429,7 +459,7 @@ describe('macro', () => { const App = () => /*#__PURE__*/_jsx(TrousersNested, { css: styles, elementType: \\"button\\", - styles: { + style: { \\"--interpol0\\": 5 + 5 }, children: \\"Submit\\" @@ -439,6 +469,7 @@ describe('macro', () => { it('should not add interpolations to jsx element if styles are not in use', () => { const result = transform` + import React from 'react'; import { css } from './macro'; const foo = 'blue'; const styles = css('Button', { color: foo }); @@ -449,6 +480,7 @@ describe('macro', () => { expect(result).toMatchInlineSnapshot(` "import { jsx as _jsx } from \\"react/jsx-runtime\\"; import { css, TrousersNested } from \\"@trousers/macro/runtime\\"; + import React from 'react'; const foo = 'blue'; const styles = css(\\"Button\\", { \\".Button-4214914708\\": \\"color: var(--interpol0);\\" @@ -460,26 +492,28 @@ describe('macro', () => { `); }); - it('should correctly interpolate styles used by nested elements', () => { + it('should interpolate styles used by nested elements', () => { const result = transform` + import React from 'react'; import { css } from './macro'; - const foo = 'blue'; - const bar = 'green'; - const styles = css('Button', { color: foo }); - const innerStyles = css('ButtonInner', { color: bar }); + const foo = 'blue'; + const bar = 'green'; + const styles = css('Button', { color: foo }); + const innerStyles = css('ButtonInner', { color: bar }); - const App = () => ( - - ); + const App = () => ( + + ); `; expect(result).toMatchInlineSnapshot(` "import { jsx as _jsx } from \\"react/jsx-runtime\\"; import { css, TrousersNested } from \\"@trousers/macro/runtime\\"; + import React from 'react'; const foo = 'blue'; const bar = 'green'; const styles = css(\\"Button\\", { @@ -492,13 +526,13 @@ describe('macro', () => { const App = () => /*#__PURE__*/_jsx(TrousersNested, { css: styles, elementType: \\"button\\", - styles: { + style: { \\"--interpol0\\": foo }, children: /*#__PURE__*/_jsx(TrousersNested, { css: innerStyles, elementType: \\"span\\", - styles: { + style: { \\"--interpol1\\": bar }, children: \\"Hello, World!\\" @@ -507,30 +541,32 @@ describe('macro', () => { `); }); - it('should correctly interpolate styles used by sibling elements', () => { + it('should interpolate styles used by sibling elements', () => { const result = transform` + import React from 'react'; import { css } from './macro'; - const foo = 'blue'; - const bar = 'green'; - const styles = css('Button', { color: foo }); - const siblingStyles = css('ButtonInner', { color: bar }); + const foo = 'blue'; + const bar = 'green'; + const styles = css('Button', { color: foo }); + const siblingStyles = css('ButtonInner', { color: bar }); - const App = () => ( -