Repository: styled-components/babel-plugin-styled-components
Pull request #229 (2019-06-25 17:56:11)

fix object syntax css prop, support more types of components & custom elements
Reduced version of #228 since the injection location code isn't totally ready yet

1. fix transpilation of objects being passed into the css prop

2. modifications from @bpierre to support custom elements, lowercase component variable names with dots, nested component names with dots


Patch:
diff --git a/src/visitors/transpileCssProp.js b/src/visitors/transpileCssProp.js
index 7c89c0d..4f77033 100644
--- a/src/visitors/transpileCssProp.js
+++ b/src/visitors/transpileCssProp.js
@@ -4,6 +4,8 @@ import { addDefault } from '@babel/helper-module-imports'
 import { importLocalName } from '../utils/detectors'
 import { useCssProp } from '../utils/options'
 
+const TAG_NAME_REGEXP = /^[a-z][a-z\d]*(\-[a-z][a-z\d]*)?$/
+
 const getName = (node, t) => {
   if (typeof node.name === 'string') return node.name
   if (t.isJSXMemberExpression(node)) {
@@ -44,7 +46,7 @@ export default t => (path, state) => {
 
   let styled
 
-  if (/^[a-z]/.test(name)) {
+  if (TAG_NAME_REGEXP.test(name)) {
     styled = t.memberExpression(importName, t.identifier(name))
   } else {
     styled = t.callExpression(importName, [t.identifier(name)])
@@ -70,6 +72,8 @@ export default t => (path, state) => {
       path.node.value.expression.tag.name === 'css'
     ) {
       css = path.node.value.expression.quasi
+    } else if (t.isObjectExpression(path.node.value.expression)) {
+      css = path.node.value.expression
     } else {
       css = t.templateLiteral(
         [
@@ -90,34 +94,101 @@ export default t => (path, state) => {
     elem.parentPath.node.closingElement.name = t.jSXIdentifier(id.name)
   }
 
-  css.expressions = css.expressions.reduce((acc, ex) => {
-    if (
-      Object.keys(bindings).some(key =>
-        bindings[key].referencePaths.find(p => p.node === ex)
-      ) ||
-      t.isFunctionExpression(ex) ||
-      t.isArrowFunctionExpression(ex)
+  // object syntax
+  if (t.isObjectExpression(css)) {
+    /**
+     * for objects as CSS props, we have to recurse through the object and replace any
+     * object value scope references with generated props similar to how the template
+     * literal transform above creates dynamic interpolations
+     */
+    const p = t.identifier('p')
+    let replaceObjectWithPropFunction = false
+
+    css.properties = css.properties.reduce(function propertiesReducer(
+      acc,
+      property
     ) {
-      acc.push(ex)
-    } else {
-      const name = path.scope.generateUidIdentifier('css')
-      const p = t.identifier('p')
-
-      elem.node.attributes.push(
-        t.jSXAttribute(t.jSXIdentifier(name.name), t.jSXExpressionContainer(ex))
-      )
-
-      acc.push(t.arrowFunctionExpression([p], t.memberExpression(p, name)))
+      if (t.isObjectExpression(property.value)) {
+        // recurse for objects within objects (e.g. {'::before': { content: x }})
+        property.value.properties = property.value.properties.reduce(
+          propertiesReducer,
+          []
+        )
+
+        acc.push(property)
+      } else if (
+        // if a non-primitive value we have to interpolate it
+        [
+          t.isBigIntLiteral,
+          t.isBooleanLiteral,
+          t.isNullLiteral,
+          t.isNumericLiteral,
+          t.isStringLiteral,
+        ].every(x => !x(property.value))
+      ) {
+        replaceObjectWithPropFunction = true
+
+        const name = path.scope.generateUidIdentifier('css')
+
+        elem.node.attributes.push(
+          t.jSXAttribute(
+            t.jSXIdentifier(name.name),
+            t.jSXExpressionContainer(property.value)
+          )
+        )
+
+        acc.push(t.objectProperty(property.key, t.memberExpression(p, name)))
+      } else {
+        // some sort of primitive which is safe to pass through as-is
+        acc.push(property)
+      }
+
+      return acc
+    },
+    [])
+
+    if (replaceObjectWithPropFunction) {
+      css = t.arrowFunctionExpression([p], css)
     }
-
-    return acc
-  }, [])
+  } else {
+    // tagged template literal
+    css.expressions = css.expressions.reduce((acc, ex) => {
+      if (
+        Object.keys(bindings).some(key =>
+          bindings[key].referencePaths.find(p => p.node === ex)
+        ) ||
+        t.isFunctionExpression(ex) ||
+        t.isArrowFunctionExpression(ex)
+      ) {
+        acc.push(ex)
+      } else {
+        const name = path.scope.generateUidIdentifier('css')
+        const p = t.identifier('p')
+
+        elem.node.attributes.push(
+          t.jSXAttribute(
+            t.jSXIdentifier(name.name),
+            t.jSXExpressionContainer(ex)
+          )
+        )
+
+        acc.push(t.arrowFunctionExpression([p], t.memberExpression(p, name)))
+      }
+
+      return acc
+    }, [])
+  }
 
   // Add the tagged template expression and then requeue the newly added node
   // so Babel runs over it again
   const length = program.node.body.push(
     t.variableDeclaration('var', [
-      t.variableDeclarator(id, t.taggedTemplateExpression(styled, css)),
+      t.variableDeclarator(
+        id,
+        t.isObjectExpression(css) || t.isArrowFunctionExpression(css)
+          ? t.callExpression(styled, [css])
+          : t.taggedTemplateExpression(styled, css)
+      ),
     ])
   )
 


Test patch:
diff --git a/test/fixtures/transpile-css-prop-all-options-on/code.js b/test/fixtures/transpile-css-prop-all-options-on/code.js
index 4fe8f8f..00c8898 100644
--- a/test/fixtures/transpile-css-prop-all-options-on/code.js
+++ b/test/fixtures/transpile-css-prop-all-options-on/code.js
@@ -106,3 +106,30 @@ const CustomCompWithDot = p => <Button.Ghost css="flex: 1">H</Button.Ghost>
 const NestedCompWithDot = p => (
   <Button.Ghost.New css="flex: 1">H</Button.Ghost.New>
 )
+
+const CustomCompWithDotLowerCase = p => (
+  <button.ghost css="flex: 1">H</button.ghost>
+)
+
+const CustomElement = p => <button-ghost css="flex: 1">H</button-ghost>
+
+const globalVar = '"foo"'
+const getAfterValue = () => '"bar"'
+
+const ObjectPropMixedInputs = p => {
+  const color = 'red'
+
+  return (
+    <p
+      css={{
+        background: p.background,
+        color,
+        textAlign: 'left',
+        '::before': { content: globalVar },
+        '::after': { content: getAfterValue() },
+      }}
+    >
+      A
+    </p>
+  )
+}
diff --git a/test/fixtures/transpile-css-prop-all-options-on/output.js b/test/fixtures/transpile-css-prop-all-options-on/output.js
index 8bdaf89..2c87f9b 100644
--- a/test/fixtures/transpile-css-prop-all-options-on/output.js
+++ b/test/fixtures/transpile-css-prop-all-options-on/output.js
@@ -31,9 +31,7 @@ var StaticTemplate = function StaticTemplate(p) {
 };
 
 var ObjectProp = function ObjectProp(p) {
-  return <_StyledP3 _css={{
-    color: 'blue'
-  }}>A</_StyledP3>;
+  return <_StyledP3>A</_StyledP3>;
 };
 
 var NoChildren = function NoChildren(p) {
@@ -55,11 +53,11 @@ var CustomComp = function CustomComp(p) {
 };
 
 var DynamicProp = function DynamicProp(p) {
-  return <_StyledP6 _css2={props.cssText}>H</_StyledP6>;
+  return <_StyledP6 _css={props.cssText}>H</_StyledP6>;
 };
 
 var LocalInterpolation = function LocalInterpolation(p) {
-  return <_StyledP7 _css3={props.bg}>
+  return <_StyledP7 _css2={props.bg}>
     H
   </_StyledP7>;
 };
@@ -79,7 +77,7 @@ var GlobalInterpolation = function GlobalInterpolation(p) {
 };
 
 var LocalCssHelperProp = function LocalCssHelperProp(p) {
-  return <_StyledP10 _css4={p.color}>
+  return <_StyledP10 _css3={p.color}>
     A
   </_StyledP10>;
 };
@@ -98,6 +96,27 @@ var NestedCompWithDot = function NestedCompWithDot(p) {
   return <_StyledButtonGhostNew>H</_StyledButtonGhostNew>;
 };
 
+var CustomCompWithDotLowerCase = function CustomCompWithDotLowerCase(p) {
+  return <_StyledButtonGhost2>H</_StyledButtonGhost2>;
+};
+
+var CustomElement = function CustomElement(p) {
+  return <_StyledButtonGhost3>H</_StyledButtonGhost3>;
+};
+
+var globalVar = '"foo"';
+
+var getAfterValue = function getAfterValue() {
+  return '"bar"';
+};
+
+var ObjectPropMixedInputs = function ObjectPropMixedInputs(p) {
+  var color = 'red';
+  return <_StyledP12 _css4={p.background} _css5={color} _css6={globalVar} _css7={getAfterValue()}>
+      A
+    </_StyledP12>;
+};
+
 var _StyledP = _styledComponents["default"].p.withConfig({
   displayName: "code___StyledP",
   componentId: "sc-7evkve-2"
@@ -111,8 +130,8 @@ var _StyledP2 = _styledComponents["default"].p.withConfig({
 var _StyledP3 = _styledComponents["default"].p.withConfig({
   displayName: "code___StyledP3",
   componentId: "sc-7evkve-4"
-})(["", ""], function (p) {
-  return p._css;
+})({
+  color: 'blue'
 });
 
 var _StyledP4 = _styledComponents["default"].p.withConfig({
@@ -134,14 +153,14 @@ var _StyledP6 = _styledComponents["default"].p.withConfig({
   displayName: "code___StyledP6",
   componentId: "sc-7evkve-8"
 })(["", ""], function (p) {
-  return p._css2;
+  return p._css;
 });
 
 var _StyledP7 = _styledComponents["default"].p.withConfig({
   displayName: "code___StyledP7",
   componentId: "sc-7evkve-9"
 })(["background:", ";"], function (p) {
-  return p._css3;
+  return p._css2;
 });
 
 var _StyledP8 = _styledComponents["default"].p.withConfig({
@@ -160,7 +179,7 @@ var _StyledP10 = _styledComponents["default"].p.withConfig({
   displayName: "code___StyledP10",
   componentId: "sc-7evkve-12"
 })(["color:", ";"], function (p) {
-  return p._css4;
+  return p._css3;
 });
 
 var _StyledP11 = _styledComponents["default"].p.withConfig({
@@ -179,3 +198,30 @@ var _StyledButtonGhostNew = (0, _styledComponents["default"])(Button.Ghost.New).
   displayName: "code___StyledButtonGhostNew",
   componentId: "sc-7evkve-15"
 })(["flex:1"]);
+
+var _StyledButtonGhost2 = (0, _styledComponents["default"])(button.ghost).withConfig({
+  displayName: "code___StyledButtonGhost2",
+  componentId: "sc-7evkve-16"
+})(["flex:1"]);
+
+var _StyledButtonGhost3 = _styledComponents["default"]["button-ghost"].withConfig({
+  displayName: "code___StyledButtonGhost3",
+  componentId: "sc-7evkve-17"
+})(["flex:1"]);
+
+var _StyledP12 = _styledComponents["default"].p.withConfig({
+  displayName: "code___StyledP12",
+  componentId: "sc-7evkve-18"
+})(function (p) {
+  return {
+    background: p._css4,
+    color: p._css5,
+    textAlign: 'left',
+    '::before': {
+      content: p._css6
+    },
+    '::after': {
+      content: p._css7
+    }
+  };
+});
diff --git a/test/fixtures/transpile-css-prop/code.js b/test/fixtures/transpile-css-prop/code.js
index fdf823b..35d17d3 100644
--- a/test/fixtures/transpile-css-prop/code.js
+++ b/test/fixtures/transpile-css-prop/code.js
@@ -92,3 +92,9 @@ const CustomCompWithDot = p => <Button.Ghost css="flex: 1">H</Button.Ghost>
 const NestedCompWithDot = p => (
   <Button.Ghost.New css="flex: 1">H</Button.Ghost.New>
 )
+
+const CustomCompWithDotLowerCase = p => (
+  <button.ghost css="flex: 1">H</button.ghost>
+)
+
+const CustomElement = p => <button-ghost css="flex: 1">H</button-ghost>
diff --git a/test/fixtures/transpile-css-prop/output.js b/test/fixtures/transpile-css-prop/output.js
index fe8a977..524d47d 100644
--- a/test/fixtures/transpile-css-prop/output.js
+++ b/test/fixtures/transpile-css-prop/output.js
@@ -4,6 +4,16 @@ var _styledComponents = _interopRequireDefault(require("styled-components"));
 
 function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
 
+function _templateObject15() {
+  var data = _taggedTemplateLiteral(["flex: 1"]);
+
+  _templateObject15 = function _templateObject15() {
+    return data;
+  };
+
+  return data;
+}
+
 function _templateObject14() {
   var data = _taggedTemplateLiteral(["flex: 1"]);
 
@@ -25,7 +35,7 @@ function _templateObject13() {
 }
 
 function _templateObject12() {
-  var data = _taggedTemplateLiteral(["\n      color: ", ";\n    "]);
+  var data = _taggedTemplateLiteral(["flex: 1"]);
 
   _templateObject12 = function _templateObject12() {
     return data;
@@ -45,7 +55,7 @@ function _templateObject11() {
 }
 
 function _templateObject10() {
-  var data = _taggedTemplateLiteral(["\n      border-radius: ", "px;\n    "]);
+  var data = _taggedTemplateLiteral(["\n      color: ", ";\n    "]);
 
   _templateObject10 = function _templateObject10() {
     return data;
@@ -55,7 +65,7 @@ function _templateObject10() {
 }
 
 function _templateObject9() {
-  var data = _taggedTemplateLiteral(["\n      color: ", ";\n    "]);
+  var data = _taggedTemplateLiteral(["\n      border-radius: ", "px;\n    "]);
 
   _templateObject9 = function _templateObject9() {
     return data;
@@ -65,7 +75,7 @@ function _templateObject9() {
 }
 
 function _templateObject8() {
-  var data = _taggedTemplateLiteral(["\n      background: ", ";\n    "]);
+  var data = _taggedTemplateLiteral(["\n      color: ", ";\n    "]);
 
   _templateObject8 = function _templateObject8() {
     return data;
@@ -75,7 +85,7 @@ function _templateObject8() {
 }
 
 function _templateObject7() {
-  var data = _taggedTemplateLiteral(["", ""]);
+  var data = _taggedTemplateLiteral(["\n      background: ", ";\n    "]);
 
   _templateObject7 = function _templateObject7() {
     return data;
@@ -85,7 +95,7 @@ function _templateObject7() {
 }
 
 function _templateObject6() {
-  var data = _taggedTemplateLiteral(["flex: 1"]);
+  var data = _taggedTemplateLiteral(["", ""]);
 
   _templateObject6 = function _templateObject6() {
     return data;
@@ -95,7 +105,7 @@ function _templateObject6() {
 }
 
 function _templateObject5() {
-  var data = _taggedTemplateLiteral(["\n      color: blue;\n    "]);
+  var data = _taggedTemplateLiteral(["flex: 1"]);
 
   _templateObject5 = function _templateObject5() {
     return data;
@@ -105,7 +115,7 @@ function _templateObject5() {
 }
 
 function _templateObject4() {
-  var data = _taggedTemplateLiteral(["flex: 1;"]);
+  var data = _taggedTemplateLiteral(["\n      color: blue;\n    "]);
 
   _templateObject4 = function _templateObject4() {
     return data;
@@ -115,7 +125,7 @@ function _templateObject4() {
 }
 
 function _templateObject3() {
-  var data = _taggedTemplateLiteral(["", ""]);
+  var data = _taggedTemplateLiteral(["flex: 1;"]);
 
   _templateObject3 = function _templateObject3() {
     return data;
@@ -160,9 +170,7 @@ var StaticTemplate = function StaticTemplate(p) {
 };
 
 var ObjectProp = function ObjectProp(p) {
-  return <_StyledP3 _css={{
-    color: 'blue'
-  }}>A</_StyledP3>;
+  return <_StyledP3>A</_StyledP3>;
 };
 
 var NoChildren = function NoChildren(p) {
@@ -184,11 +192,11 @@ var CustomComp = function CustomComp(p) {
 };
 
 var DynamicProp = function DynamicProp(p) {
-  return <_StyledP6 _css2={props.cssText}>H</_StyledP6>;
+  return <_StyledP6 _css={props.cssText}>H</_StyledP6>;
 };
 
 var LocalInterpolation = function LocalInterpolation(p) {
-  return <_StyledP7 _css3={props.bg}>
+  return <_StyledP7 _css2={props.bg}>
     H
   </_StyledP7>;
 };
@@ -208,7 +216,7 @@ var GlobalInterpolation = function GlobalInterpolation(p) {
 };
 
 var LocalCssHelperProp = function LocalCssHelperProp(p) {
-  return <_StyledP10 _css4={p.color}>
+  return <_StyledP10 _css3={p.color}>
     A
   </_StyledP10>;
 };
@@ -227,42 +235,54 @@ var NestedCompWithDot = function NestedCompWithDot(p) {
   return <_StyledButtonGhostNew>H</_StyledButtonGhostNew>;
 };
 
+var CustomCompWithDotLowerCase = function CustomCompWithDotLowerCase(p) {
+  return <_StyledButtonGhost2>H</_StyledButtonGhost2>;
+};
+
+var CustomElement = function CustomElement(p) {
+  return <_StyledButtonGhost3>H</_StyledButtonGhost3>;
+};
+
 var _StyledP = _styledComponents["default"].p(_templateObject());
 
 var _StyledP2 = _styledComponents["default"].p(_templateObject2());
 
-var _StyledP3 = _styledComponents["default"].p(_templateObject3(), function (p) {
-  return p._css;
+var _StyledP3 = _styledComponents["default"].p({
+  color: 'blue'
 });
 
-var _StyledP4 = _styledComponents["default"].p(_templateObject4());
+var _StyledP4 = _styledComponents["default"].p(_templateObject3());
 
-var _StyledP5 = _styledComponents["default"].p(_templateObject5());
+var _StyledP5 = _styledComponents["default"].p(_templateObject4());
 
-var _StyledParagraph = (0, _styledComponents["default"])(Paragraph)(_templateObject6());
+var _StyledParagraph = (0, _styledComponents["default"])(Paragraph)(_templateObject5());
 
-var _StyledP6 = _styledComponents["default"].p(_templateObject7(), function (p) {
-  return p._css2;
+var _StyledP6 = _styledComponents["default"].p(_templateObject6(), function (p) {
+  return p._css;
 });
 
-var _StyledP7 = _styledComponents["default"].p(_templateObject8(), function (p) {
-  return p._css3;
+var _StyledP7 = _styledComponents["default"].p(_templateObject7(), function (p) {
+  return p._css2;
 });
 
-var _StyledP8 = _styledComponents["default"].p(_templateObject9(), function (props) {
+var _StyledP8 = _styledComponents["default"].p(_templateObject8(), function (props) {
   return props.theme.a;
 });
 
-var _StyledP9 = _styledComponents["default"].p(_templateObject10(), radius);
+var _StyledP9 = _styledComponents["default"].p(_templateObject9(), radius);
 
-var _StyledP10 = _styledComponents["default"].p(_templateObject11(), function (p) {
-  return p._css4;
+var _StyledP10 = _styledComponents["default"].p(_templateObject10(), function (p) {
+  return p._css3;
 });
 
-var _StyledP11 = _styledComponents["default"].p(_templateObject12(), function (props) {
+var _StyledP11 = _styledComponents["default"].p(_templateObject11(), function (props) {
   return props.theme.color;
 });
 
-var _StyledButtonGhost = (0, _styledComponents["default"])(Button.Ghost)(_templateObject13());
+var _StyledButtonGhost = (0, _styledComponents["default"])(Button.Ghost)(_templateObject12());
+
+var _StyledButtonGhostNew = (0, _styledComponents["default"])(Button.Ghost.New)(_templateObject13());
+
+var _StyledButtonGhost2 = (0, _styledComponents["default"])(button.ghost)(_templateObject14());
 
-var _StyledButtonGhostNew = (0, _styledComponents["default"])(Button.Ghost.New)(_templateObject14());
+var _StyledButtonGhost3 = _styledComponents["default"]["button-ghost"](_templateObject15());