Repository: tkh44/emotion
Pull request #138 (2017-07-12 17:22:01)

Initial working version of using glamor core
<!--
Thanks for your interest in the project. I appreciate bugs filed and PRs submitted!

Please make sure that you are familiar with and follow the Code of Conduct for
this project (found in the CODE_OF_CONDUCT.md file).

Also, please make sure you're familiar with and follow the instructions in the
contributing guidelines (found in the CONTRIBUTING.md file).

If you're new to contributing to open source projects, you might find this free
video course helpful: http://kcd.im/pull-request

Please fill out the information below to expedite the review and (hopefully)
merge of your pull request!
-->

<!-- What changes are being made? (What feature/bug is being fixed here?) -->
**What**:
Use glamor's core algorithm instead of cxs'

<!-- Why are these changes necessary? -->
**Why**:
CSS object support is much cleaner. This will lead to fixes related to #128, #104 eventually as well.

<!-- How were these changes implemented? -->
**How**:
I ported glamor's core to work in our setup

<!-- Have you done all of these things?  -->
**Checklist**:
<!-- add "N/A" to the end of each line that's irrelevant to your changes -->
<!-- to check an item, place an "x" in the box like so: "- [x] Documentation" -->
- [ ] Documentation
- [ ] Tests
- [ ] Code complete

<!-- feel free to add additional comments -->
I did this on @threepointone's advice.


Patch:
diff --git a/package.json b/package.json
index 894b7d68..5d338023 100644
--- a/package.json
+++ b/package.json
@@ -33,6 +33,7 @@
     "@arr/foreach": "^1.0.0",
     "@arr/map": "^1.0.0",
     "babel-plugin-syntax-jsx": "^6.18.0",
+    "fbjs": "^0.8.12",
     "styled-components": "2.0.0",
     "theming": "^1.0.1",
     "touch": "^1.0.0"
@@ -112,7 +113,7 @@
   "bundlesize": [
     {
       "path": "./dist/DO-NOT-USE.min.js",
-      "threshold": "3 Kb"
+      "threshold": "6 Kb"
     }
   ]
 }
diff --git a/src/glamor/CSSPropertyOperations/CSSProperty.js b/src/glamor/CSSPropertyOperations/CSSProperty.js
new file mode 100644
index 00000000..642a839d
--- /dev/null
+++ b/src/glamor/CSSPropertyOperations/CSSProperty.js
@@ -0,0 +1,146 @@
+/**
+ * Copyright 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule CSSProperty
+ */
+
+/**
+ * CSS properties which accept numbers but are not in units of "px".
+ */
+
+let isUnitlessNumber = {
+  animationIterationCount: true,
+  borderImageOutset: true,
+  borderImageSlice: true,
+  borderImageWidth: true,
+  boxFlex: true,
+  boxFlexGroup: true,
+  boxOrdinalGroup: true,
+  columnCount: true,
+  flex: true,
+  flexGrow: true,
+  flexPositive: true,
+  flexShrink: true,
+  flexNegative: true,
+  flexOrder: true,
+  gridRow: true,
+  gridColumn: true,
+  fontWeight: true,
+  lineClamp: true,
+  lineHeight: true,
+  opacity: true,
+  order: true,
+  orphans: true,
+  tabSize: true,
+  widows: true,
+  zIndex: true,
+  zoom: true,
+
+  // SVG-related properties
+  fillOpacity: true,
+  floodOpacity: true,
+  stopOpacity: true,
+  strokeDasharray: true,
+  strokeDashoffset: true,
+  strokeMiterlimit: true,
+  strokeOpacity: true,
+  strokeWidth: true
+}
+
+/**
+ * @param {string} prefix vendor-specific prefix, eg: Webkit
+ * @param {string} key style name, eg: transitionDuration
+ * @return {string} style name prefixed with `prefix`, properly camelCased, eg:
+ * WebkitTransitionDuration
+ */
+function prefixKey (prefix, key) {
+  return prefix + key.charAt(0).toUpperCase() + key.substring(1)
+}
+
+/**
+ * Support style names that may come passed in prefixed by adding permutations
+ * of vendor prefixes.
+ */
+let prefixes = ['Webkit', 'ms', 'Moz', 'O']
+
+// Using Object.keys here, or else the vanilla for-in loop makes IE8 go into an
+// infinite loop, because it iterates over the newly added props too.
+Object.keys(isUnitlessNumber).forEach(function (prop) {
+  prefixes.forEach(function (prefix) {
+    isUnitlessNumber[prefixKey(prefix, prop)] = isUnitlessNumber[prop]
+  })
+})
+
+/**
+ * Most style properties can be unset by doing .style[prop] = '' but IE8
+ * doesn't like doing that with shorthand properties so for the properties that
+ * IE8 breaks on, which are listed here, we instead unset each of the
+ * individual properties. See http://bugs.jquery.com/ticket/12385.
+ * The 4-value 'clock' properties like margin, padding, border-width seem to
+ * behave without any problems. Curiously, list-style works too without any
+ * special prodding.
+ */
+let shorthandPropertyExpansions = {
+  background: {
+    backgroundAttachment: true,
+    backgroundColor: true,
+    backgroundImage: true,
+    backgroundPositionX: true,
+    backgroundPositionY: true,
+    backgroundRepeat: true
+  },
+  backgroundPosition: {
+    backgroundPositionX: true,
+    backgroundPositionY: true
+  },
+  border: {
+    borderWidth: true,
+    borderStyle: true,
+    borderColor: true
+  },
+  borderBottom: {
+    borderBottomWidth: true,
+    borderBottomStyle: true,
+    borderBottomColor: true
+  },
+  borderLeft: {
+    borderLeftWidth: true,
+    borderLeftStyle: true,
+    borderLeftColor: true
+  },
+  borderRight: {
+    borderRightWidth: true,
+    borderRightStyle: true,
+    borderRightColor: true
+  },
+  borderTop: {
+    borderTopWidth: true,
+    borderTopStyle: true,
+    borderTopColor: true
+  },
+  font: {
+    fontStyle: true,
+    fontVariant: true,
+    fontWeight: true,
+    fontSize: true,
+    lineHeight: true,
+    fontFamily: true
+  },
+  outline: {
+    outlineWidth: true,
+    outlineStyle: true,
+    outlineColor: true
+  }
+}
+
+const CSSProperty = {
+  isUnitlessNumber: isUnitlessNumber,
+  shorthandPropertyExpansions: shorthandPropertyExpansions
+}
+
+export default CSSProperty
diff --git a/src/glamor/CSSPropertyOperations/dangerousStyleValue.js b/src/glamor/CSSPropertyOperations/dangerousStyleValue.js
new file mode 100644
index 00000000..69dd058b
--- /dev/null
+++ b/src/glamor/CSSPropertyOperations/dangerousStyleValue.js
@@ -0,0 +1,92 @@
+/**
+ * Copyright 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule dangerousStyleValue
+ */
+
+import CSSProperty from './CSSProperty'
+import warning from 'fbjs/lib/warning'
+
+let isUnitlessNumber = CSSProperty.isUnitlessNumber
+let styleWarnings = {}
+
+/**
+ * Convert a value into the proper css writable value. The style name `name`
+ * should be logical (no hyphens), as specified
+ * in `CSSProperty.isUnitlessNumber`.
+ *
+ * @param {string} name CSS property name such as `topMargin`.
+ * @param {*} value CSS property value such as `10px`.
+ * @param {ReactDOMComponent} component
+ * @return {string} Normalized style value with dimensions applied.
+ */
+function dangerousStyleValue (name, value, component) {
+  // Note that we've removed escapeTextForBrowser() calls here since the
+  // whole string will be escaped when the attribute is injected into
+  // the markup. If you provide unsafe user data here they can inject
+  // arbitrary CSS which may be problematic (I couldn't repro this):
+  // https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet
+  // http://www.thespanner.co.uk/2007/11/26/ultimate-xss-css-injection/
+  // This is not an XSS hole but instead a potential CSS injection issue
+  // which has lead to a greater discussion about how we're going to
+  // trust URLs moving forward. See #2115901
+
+  let isEmpty = value == null || typeof value === 'boolean' || value === ''
+  if (isEmpty) {
+    return ''
+  }
+
+  let isNonNumeric = isNaN(value)
+  if (
+    isNonNumeric ||
+    value === 0 ||
+    (isUnitlessNumber.hasOwnProperty(name) && isUnitlessNumber[name])
+  ) {
+    return '' + value // cast to string
+  }
+
+  if (typeof value === 'string') {
+    if (process.env.NODE_ENV !== 'production') {
+      // Allow '0' to pass through without warning. 0 is already special and
+      // doesn't require units, so we don't need to warn about it.
+      if (component && value !== '0') {
+        let owner = component._currentElement._owner
+        let ownerName = owner ? owner.getName() : null
+        if (ownerName && !styleWarnings[ownerName]) {
+          styleWarnings[ownerName] = {}
+        }
+        let warned = false
+        if (ownerName) {
+          let warnings = styleWarnings[ownerName]
+          warned = warnings[name]
+          if (!warned) {
+            warnings[name] = true
+          }
+        }
+        if (!warned) {
+          process.env.NODE_ENV !== 'production'
+            ? warning(
+                false,
+                'a `%s` tag (owner: `%s`) was passed a numeric string value ' +
+                  'for CSS property `%s` (value: `%s`) which will be treated ' +
+                  'as a unitless number in a future version of React.',
+                component._currentElement.type,
+                ownerName || 'unknown',
+                name,
+                value
+              )
+            : void 0
+        }
+      }
+    }
+    value = value.trim()
+  }
+  return value + 'px'
+}
+
+export default dangerousStyleValue
diff --git a/src/glamor/CSSPropertyOperations/index.js b/src/glamor/CSSPropertyOperations/index.js
new file mode 100644
index 00000000..76010282
--- /dev/null
+++ b/src/glamor/CSSPropertyOperations/index.js
@@ -0,0 +1,170 @@
+/**
+ * Copyright 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule CSSPropertyOperations
+ */
+
+import camelizeStyleName from 'fbjs/lib/camelizeStyleName'
+import dangerousStyleValue from './dangerousStyleValue'
+import hyphenateStyleName from 'fbjs/lib/hyphenateStyleName'
+import memoizeStringOnly from 'fbjs/lib/memoizeStringOnly'
+import warning from 'fbjs/lib/warning'
+
+export const processStyleName = memoizeStringOnly(hyphenateStyleName)
+
+if (process.env.NODE_ENV !== 'production') {
+  // 'msTransform' is correct, but the other prefixes should be capitalized
+  let badVendoredStyleNamePattern = /^(?:webkit|moz|o)[A-Z]/
+
+  // style values shouldn't contain a semicolon
+  let badStyleValueWithSemicolonPattern = /;\s*$/
+
+  let warnedStyleNames = {}
+  let warnedStyleValues = {}
+  let warnedForNaNValue = false
+
+  let warnHyphenatedStyleName = function (name, owner) {
+    if (warnedStyleNames.hasOwnProperty(name) && warnedStyleNames[name]) {
+      return
+    }
+
+    warnedStyleNames[name] = true
+    process.env.NODE_ENV !== 'production'
+      ? warning(
+          false,
+          'Unsupported style property %s. Did you mean %s?%s',
+          name,
+          camelizeStyleName(name),
+          checkRenderMessage(owner)
+        )
+      : void 0
+  }
+
+  let warnBadVendoredStyleName = function (name, owner) {
+    if (warnedStyleNames.hasOwnProperty(name) && warnedStyleNames[name]) {
+      return
+    }
+
+    warnedStyleNames[name] = true
+    process.env.NODE_ENV !== 'production'
+      ? warning(
+          false,
+          'Unsupported vendor-prefixed style property %s. Did you mean %s?%s',
+          name,
+          name.charAt(0).toUpperCase() + name.slice(1),
+          checkRenderMessage(owner)
+        )
+      : void 0
+  }
+
+  let warnStyleValueWithSemicolon = function (name, value, owner) {
+    if (warnedStyleValues.hasOwnProperty(value) && warnedStyleValues[value]) {
+      return
+    }
+
+    warnedStyleValues[value] = true
+    process.env.NODE_ENV !== 'production'
+      ? warning(
+          false,
+          "Style property values shouldn't contain a semicolon.%s " +
+            'Try "%s: %s" instead.',
+          checkRenderMessage(owner),
+          name,
+          value.replace(badStyleValueWithSemicolonPattern, '')
+        )
+      : void 0
+  }
+
+  let warnStyleValueIsNaN = function (name, value, owner) {
+    if (warnedForNaNValue) {
+      return
+    }
+
+    warnedForNaNValue = true
+    process.env.NODE_ENV !== 'production'
+      ? warning(
+          false,
+          '`NaN` is an invalid value for the `%s` css style property.%s',
+          name,
+          checkRenderMessage(owner)
+        )
+      : void 0
+  }
+
+  let checkRenderMessage = function (owner) {
+    if (owner) {
+      let name = owner.getName()
+      if (name) {
+        return ' Check the render method of `' + name + '`.'
+      }
+    }
+    return ''
+  }
+
+  /**
+   * @param {string} name
+   * @param {*} value
+   * @param {ReactDOMComponent} component
+   */
+  var warnValidStyle = function (name, value, component) {
+    // eslint-disable-line no-var
+    let owner
+    if (component) {
+      owner = component._currentElement._owner
+    }
+    if (name.indexOf('-') > -1) {
+      warnHyphenatedStyleName(name, owner)
+    } else if (badVendoredStyleNamePattern.test(name)) {
+      warnBadVendoredStyleName(name, owner)
+    } else if (badStyleValueWithSemicolonPattern.test(value)) {
+      warnStyleValueWithSemicolon(name, value, owner)
+    }
+
+    if (typeof value === 'number' && isNaN(value)) {
+      warnStyleValueIsNaN(name, value, owner)
+    }
+  }
+}
+
+/**
+   * Serializes a mapping of style properties for use as inline styles:
+   *
+   *   > createMarkupForStyles({width: '200px', height: 0})
+   *   "width:200px;height:0;"
+   *
+   * Undefined values are ignored so that declarative programming is easier.
+   * The result should be HTML-escaped before insertion into the DOM.
+   *
+   * @param {object} styles
+   * @param {ReactDOMComponent} component
+   * @return {?string}
+   */
+
+export function createMarkupForStyles (styles, component) {
+  let serialized = ''
+  for (let styleName in styles) {
+    const isCustomProp = styleName.indexOf('--') === 0
+    if (!styles.hasOwnProperty(styleName)) {
+      continue
+    }
+    let styleValue = styles[styleName]
+    if (process.env.NODE_ENV !== 'production' && !isCustomProp) {
+      warnValidStyle(styleName, styleValue, component)
+    }
+    if (styleValue != null) {
+      if (isCustomProp) {
+        serialized += `${styleName}:${styleValue};`
+      } else {
+        serialized += processStyleName(styleName) + ':'
+        serialized +=
+          dangerousStyleValue(styleName, styleValue, component) + ';'
+      }
+    }
+  }
+  return serialized || null
+}
diff --git a/src/glamor/clean.js b/src/glamor/clean.js
new file mode 100644
index 00000000..d20052de
--- /dev/null
+++ b/src/glamor/clean.js
@@ -0,0 +1,50 @@
+// Returns true for null, false, undefined and {}
+function isFalsy (value) {
+  return (
+    value === null ||
+    value === undefined ||
+    value === false ||
+    (typeof value === 'object' && Object.keys(value).length === 0)
+  )
+}
+
+function cleanObject (object) {
+  if (isFalsy(object)) return null
+  if (typeof object !== 'object') return object
+
+  let acc = {},
+    keys = Object.keys(object),
+    hasFalsy = false
+  for (let i = 0; i < keys.length; i++) {
+    let value = object[keys[i]]
+    const filteredValue = clean(value)
+    if (filteredValue === null || filteredValue !== value) {
+      hasFalsy = true
+    }
+    if (filteredValue !== null) {
+      acc[keys[i]] = filteredValue
+    }
+  }
+  return Object.keys(acc).length === 0 ? null : hasFalsy ? acc : object
+}
+
+function cleanArray (rules) {
+  let hasFalsy = false
+  const filtered = []
+  rules.forEach(rule => {
+    const filteredRule = clean(rule)
+    if (filteredRule === null || filteredRule !== rule) {
+      hasFalsy = true
+    }
+    if (filteredRule !== null) {
+      filtered.push(filteredRule)
+    }
+  })
+  return filtered.length == 0 ? null : hasFalsy ? filtered : rules
+}
+
+// Takes style array or object provided by user and clears all the falsy data
+// If there is no styles left after filtration returns null
+export default function clean (input) {
+  return Array.isArray(input) ? cleanArray(input) : cleanObject(input)
+}
diff --git a/src/index.js b/src/index.js
index 8369f767..42802fd1 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,9 +1,12 @@
 // @flow
 import forEach from '@arr/foreach'
 import { StyleSheet } from './sheet'
-import { hashArray, hashObject } from './hash'
+import { hashString as hash, hashArray, hashObject } from './hash'
+import { createMarkupForStyles } from './glamor/CSSPropertyOperations'
+import clean from './glamor/clean.js'
 
 export const sheet = new StyleSheet()
+// 🚀
 sheet.inject()
 
 export let inserted: { [string]: boolean | void } = {}
@@ -35,17 +38,66 @@ export function flush () {
   sheet.inject()
 }
 
+// a simple cache to store generated rules
+let registered = (sheet.registered = {})
+
+function register (spec) {
+  if (!registered[spec.id]) {
+    registered[spec.id] = spec
+  }
+}
+
+function _getRegistered (rule) {
+  if (isLikeRule(rule)) {
+    let ret = registered[idFor(rule)]
+    if (ret == null) {
+      throw new Error(
+        '[emotion] an unexpected rule cache miss occurred. This is probably a sign of multiple glamor instances in your app. See https://github.com/threepointone/glamor/issues/79'
+      )
+    }
+    return ret
+  }
+  return rule
+}
+
+// The idea on how to merge object class names come from glamorous
+// 💄
+// https://github.com/paypal/glamorous/blob/master/src/get-glamor-classname.js
+function getGlamorStylesFromClassName (className) {
+  const id = className.trim().slice('css-obj-'.length)
+  if (sheet.registered[id]) {
+    return sheet.registered[id].style
+  } else {
+    return null
+  }
+}
+
 export function css (classes: string[], vars: vars, content: () => string[]) {
   if (!Array.isArray(classes)) {
     classes = [classes]
   }
 
   let computedClassName = ''
-  forEach(classes, (cls): string => {
+  let objectStyles = []
+  forEach(classes, (cls): void => {
     computedClassName && (computedClassName += ' ')
-    computedClassName += typeof cls === 'string' ? cls : objStyle(cls)
+
+    if (typeof cls === 'string') {
+      if (cls.trim().indexOf('css-obj-') === 0) {
+        const glamorStylesFromClassName = getGlamorStylesFromClassName(cls)
+        objectStyles.push(glamorStylesFromClassName)
+      } else {
+        computedClassName += cls
+      }
+    } else {
+      objectStyles.push(cls)
+    }
   })
 
+  if (objectStyles.length) {
+    computedClassName += ' ' + objStyle(...objectStyles).toString()
+  }
+
   if (content) {
     // inline mode
     let src = content(...vars) // returns an array
@@ -55,7 +107,8 @@ export function css (classes: string[], vars: vars, content: () => string[]) {
       inserted[hash] = true
       const rgx = new RegExp(classes[0], 'gm')
       forEach(src, r => {
-        sheet.insert(r.replace(rgx, `${classes[0]}-${hash}`))
+        const finalRule = r.replace(rgx, `${classes[0]}-${hash}`)
+        sheet.insert(finalRule)
       })
     }
     return `${classes[0]}-${hash} ${computedClassName}`
@@ -93,94 +146,365 @@ export function hydrate (ids: string[]) {
 
 // 🍩
 // https://github.com/jxnblk/cxs/blob/master/src/monolithic/index.js
-export function objStyle (style: { [string]: any }) {
-  const hash = hashObject(style)
-  const className = `css-${hash}`
-  const selector = '.' + className
+type GlamorRule = { [string]: any }
 
-  if (inserted[hash]) return className
+type CSSRuleList = Array<GlamorRule>
 
-  const rules = deconstruct(selector, style)
-  forEach(rules, rule => sheet.insert(rule))
+type GlamorClassName = {
+  [string]: any
+}
 
-  inserted[hash] = true
+let cachedCss: (rules: CSSRuleList) => GlamorClassName = typeof WeakMap !==
+  'undefined'
+  ? multiIndexCache(_css)
+  : _css
 
-  return className
+export function objStyle (...rules: CSSRuleList): GlamorClassName {
+  rules = clean(rules)
+  if (!rules) {
+    return nullrule
+  }
+
+  return cachedCss(rules)
+}
+
+function _css (rules) {
+  let style = {}
+  build(style, { src: rules }) // mutative! but worth it.
+
+  let spec = {
+    id: hashObject(style),
+    style,
+    type: 'css'
+  }
+  return toRule(spec)
 }
 
-function deconstruct (selector, styles, media) {
-  const decs = []
-  const rules = []
+// define some constants
 
-  for (let key in styles) {
-    const value = styles[key]
-    const type = typeof value
+const isDev = process.env.NODE_ENV === 'development' || !process.env.NODE_ENV
+const isTest = process.env.NODE_ENV === 'test'
 
-    if (type === 'number' || type === 'string') {
-      decs.push(createDec(key, value))
-    } else if (Array.isArray(value)) {
-      forEach(value, val => decs.push(createDec(key, val)))
-    } else if (key.charCodeAt(0) === 58) {
-      forEach(deconstruct(selector + key, value, media), r => rules.push(r))
-    } else if (key.indexOf('@media') !== -1) {
-      forEach(deconstruct(selector, value, key), r => rules.push(r))
+// takes a string, converts to lowercase, strips out nonalphanumeric.
+function simple (str) {
+  return str.toLowerCase().replace(/[^a-z0-9]/g, '')
+}
+
+// of shape { 'data-css-<id>': '' }
+export function isLikeRule (rule: GlamorRule) {
+  let keys = Object.keys(rule).filter(x => x !== 'toString')
+  if (keys.length !== 1) {
+    return false
+  }
+  return !!/css\-obj\-([a-zA-Z0-9]+)/.exec(keys[0])
+}
+
+// extracts id from a { 'css-<id>': ''} like object
+export function idFor (rule: GlamorRule) {
+  let keys = Object.keys(rule).filter(x => x !== 'toString')
+  if (keys.length !== 1) throw new Error('not a rule')
+  let regex = /css\-obj\-([a-zA-Z0-9]+)/
+  let match = regex.exec(keys[0])
+  if (!match) throw new Error('not a rule')
+  return match[1]
+}
+
+function selector (id: string, path: string = '') {
+  if (!id) {
+    return path.replace(/\&/g, '')
+  }
+  if (!path) return `.css-obj-${id}`
+
+  let x = path
+    .split(',')
+    .map(
+      x =>
+        x.indexOf('&') >= 0
+          ? x.replace(/\&/gm, `.css-obj-${id}`)
+          : `.css-obj-${id}${x}`
+    )
+    .join(',')
+
+  return x
+}
+
+function deconstruct (style) {
+  // we can be sure it's not infinitely nested here
+  let plain, selects, medias, supports
+  Object.keys(style).forEach(key => {
+    if (key.indexOf('&') >= 0) {
+      selects = selects || {}
+      selects[key] = style[key]
+    } else if (key.indexOf('@media') === 0) {
+      medias = medias || {}
+      medias[key] = deconstruct(style[key])
+    } else if (key.indexOf('@supports') === 0) {
+      supports = supports || {}
+      supports[key] = deconstruct(style[key])
     } else {
-      forEach(deconstruct(selector + ' ' + key, value, media), r => rules.push(r))
+      plain = plain || {}
+      plain[key] = style[key]
+    }
+  })
+  return { plain, selects, medias, supports }
+}
+
+function deconstructedStyleToCSS (id, style) {
+  let css = []
+
+  // plugins here
+  let { plain, selects, medias, supports } = style
+  if (plain) {
+    css.push(`${selector(id)}{${createMarkupForStyles(plain)}}`)
+  }
+  if (selects) {
+    Object.keys(selects).forEach((key: string) =>
+      css.push(`${selector(id, key)}{${createMarkupForStyles(selects[key])}}`)
+    )
+  }
+  if (medias) {
+    Object.keys(medias).forEach(key =>
+      css.push(`${key}{${deconstructedStyleToCSS(id, medias[key]).join('')}}`)
+    )
+  }
+  if (supports) {
+    Object.keys(supports).forEach(key =>
+      css.push(`${key}{${deconstructedStyleToCSS(id, supports[key]).join('')}}`)
+    )
+  }
+  return css
+}
+
+// and helpers to insert rules into said sheet
+function insert (spec) {
+  if (!inserted[spec.id]) {
+    inserted[spec.id] = true
+    let deconstructed = deconstruct(spec.style)
+    deconstructedStyleToCSS(spec.id, deconstructed).map(cssRule =>
+      sheet.insert(cssRule)
+    )
+  }
+}
+
+// todo - perf
+let ruleCache = {}
+
+function toRule (spec) {
+  register(spec)
+  insert(spec)
+
+  if (ruleCache[spec.id]) {
+    return ruleCache[spec.id]
+  }
+
+  let ret = { [`css-${spec.id}`]: '' }
+  Object.defineProperty(ret, 'toString', {
+    enumerable: false,
+    value () {
+      return 'css-obj-' + spec.id
+    }
+  })
+  ruleCache[spec.id] = ret
+  return ret
+}
+
+function log () {
+  // eslint-disable-line no-unused-vars
+  console.log(this) // eslint-disable-line no-console
+  return this
+}
+
+function isSelector (key) {
+  let possibles = [':', '.', '[', '>', ' '],
+    found = false,
+    ch = key.charAt(0)
+  for (let i = 0; i < possibles.length; i++) {
+    if (ch === possibles[i]) {
+      found = true
+      break
     }
   }
+  return found || key.indexOf('&') >= 0
+}
+
+function joinSelectors (a, b) {
+  let as = a.split(',').map(a => (!(a.indexOf('&') >= 0) ? '&' + a : a))
+  let bs = b.split(',').map(b => (!(b.indexOf('&') >= 0) ? '&' + b : b))
+
+  return bs
+    .reduce((arr, b) => arr.concat(as.map(a => b.replace(/\&/g, a))), [])
+    .join(',')
+}
+
+function joinMediaQueries (a, b) {
+  return a ? `@media ${a.substring(6)} and ${b.substring(6)}` : b
+}
+
+function isMediaQuery (key) {
+  return key.indexOf('@media') === 0
+}
 
-  rules.unshift(createRule(selector, decs, media))
-
-  return rules
-}
-
-function createDec (key, value) {
-  const prop = hyphenate(key)
-  const val = addPx(key, value)
-  return prop + ':' + val
-}
-
-function createRule (selector, decs, media) {
-  const rule = `${selector}{${decs.join(';')}}`
-  return media ? `${media}{${rule}}` : rule
-}
-
-function hyphenate (str) {
-  return ('' + str).replace(/[A-Z]|^ms/g, '-$&').toLowerCase()
-}
-
-function addPx (prop, value) {
-  if (typeof value !== 'number' || unitlessProps[prop] !== undefined) return value
-  return value + 'px'
-}
-
-const unitlessProps = {
-  animationIterationCount: 1,
-  boxFlex: 1,
-  boxFlexGroup: 1,
-  boxOrdinalGroup: 1,
-  columnCount: 1,
-  flex: 1,
-  flexGrow: 1,
-  flexPositive: 1,
-  flexShrink: 1,
-  flexNegative: 1,
-  flexOrder: 1,
-  gridRow: 1,
-  gridColumn: 1,
-  fontWeight: 1,
-  lineClamp: 1,
-  lineHeight: 1,
-  opacity: 1,
-  order: 1,
-  orphans: 1,
-  tabSize: 1,
-  widows: 1,
-  zIndex: 1,
-  zoom: 1,
-  fillOpacity: 1,
-  stopOpacity: 1,
-  strokeDashoffset: 1,
-  strokeOpacity: 1,
-  strokeWidth: 1
+function isSupports (key) {
+  return key.indexOf('@supports') === 0
+}
+
+function joinSupports (a, b) {
+  return a ? `@supports ${a.substring(9)} and ${b.substring(9)}` : b
+}
+
+// flatten a nested array
+function flatten (inArr) {
+  let arr = []
+  for (let i = 0; i < inArr.length; i++) {
+    if (Array.isArray(inArr[i])) arr = arr.concat(flatten(inArr[i]))
+    else arr = arr.concat(inArr[i])
+  }
+  return arr
+}
+
+// mutable! modifies dest.
+function build (dest, { selector = '', mq = '', supp = '', src = {} }) {
+  if (!Array.isArray(src)) {
+    src = [src]
+  }
+  src = flatten(src)
+
+  src.forEach(_src => {
+    if (isLikeRule(_src)) {
+      let reg = _getRegistered(_src)
+      if (reg.type !== 'css') {
+        throw new Error('cannot merge this rule')
+      }
+      _src = reg.style
+    }
+    _src = clean(_src)
+    if (_src && _src.composes) {
+      build(dest, { selector, mq, supp, src: _src.composes })
+    }
+    Object.keys(_src || {}).forEach(key => {
+      if (isSelector(key)) {
+        if (key === '::placeholder') {
+          build(dest, {
+            selector: joinSelectors(selector, '::-webkit-input-placeholder'),
+            mq,
+            supp,
+            src: _src[key]
+          })
+          build(dest, {
+            selector: joinSelectors(selector, '::-moz-placeholder'),
+            mq,
+            supp,
+            src: _src[key]
+          })
+          build(dest, {
+            selector: joinSelectors(selector, '::-ms-input-placeholder'),
+            mq,
+            supp,
+            src: _src[key]
+          })
+        }
+
+        build(dest, {
+          selector: joinSelectors(selector, key),
+          mq,
+          supp,
+          src: _src[key]
+        })
+      } else if (isMediaQuery(key)) {
+        build(dest, {
+          selector,
+          mq: joinMediaQueries(mq, key),
+          supp,
+          src: _src[key]
+        })
+      } else if (isSupports(key)) {
+        build(dest, {
+          selector,
+          mq,
+          supp: joinSupports(supp, key),
+          src: _src[key]
+        })
+      } else if (key === 'composes') {
+        // ignore, we already dealth with it
+      } else {
+        let _dest = dest
+        if (supp) {
+          _dest[supp] = _dest[supp] || {}
+          _dest = _dest[supp]
+        }
+        if (mq) {
+          _dest[mq] = _dest[mq] || {}
+          _dest = _dest[mq]
+        }
+        if (selector) {
+          _dest[selector] = _dest[selector] || {}
+          _dest = _dest[selector]
+        }
+
+        _dest[key] = _src[key]
+      }
+    })
+  })
+}
+
+let nullrule: GlamorClassName = {
+  // 'data-css-nil': ''
+}
+
+Object.defineProperty(nullrule, 'toString', {
+  enumerable: false,
+  value () {
+    return 'css-nil'
+  }
+})
+
+let inputCaches = typeof WeakMap !== 'undefined'
+  ? [nullrule, new WeakMap(), new WeakMap(), new WeakMap()]
+  : [nullrule]
+
+let warnedWeakMapError = false
+
+function multiIndexCache (fn) {
+  return function (args) {
+    if (inputCaches[args.length]) {
+      let coi = inputCaches[args.length]
+      let ctr = 0
+      while (ctr < args.length - 1) {
+        if (!coi.has(args[ctr])) {
+          coi.set(args[ctr], new WeakMap())
+        }
+        coi = coi.get(args[ctr])
+        ctr++
+      }
+      if (coi.has(args[args.length - 1])) {
+        let ret = coi.get(args[ctr])
+
+        if (registered[ret.toString().substring(4)]) {
+          // make sure it hasn't been flushed
+          return ret
+        }
+      }
+    }
+    let value = fn(args)
+    if (inputCaches[args.length]) {
+      let ctr = 0,
+        coi = inputCaches[args.length]
+      while (ctr < args.length - 1) {
+        coi = coi.get(args[ctr])
+        ctr++
+      }
+      try {
+        coi.set(args[ctr], value)
+      } catch (err) {
+        if (isDev && !warnedWeakMapError) {
+          warnedWeakMapError = true
+          console.warn('failed setting the WeakMap cache for args:', ...args) // eslint-disable-line no-console
+          console.warn(
+            'this should NOT happen, please file a bug on the github repo.'
+          ) // eslint-disable-line no-console
+        }
+      }
+    }
+    return value
+  }
 }


Test patch:
diff --git a/test/__snapshots__/css.test.js.snap b/test/__snapshots__/css.test.js.snap
index c4a23a78..449df6ea 100644
--- a/test/__snapshots__/css.test.js.snap
+++ b/test/__snapshots__/css.test.js.snap
@@ -15,16 +15,16 @@ exports[`css composes 3`] = `
       z-index: 100;
       font-size: 18px;
       text-align: center;
-      border: solid 1px red; }.css-15famh2{}.css-cls2-1qb2ovg-1em6aur {
+      border: solid 1px red; }.css-cls2-1qb2ovg-1em6aur {
       -webkit-justify-content: center;
       -ms-flex-pack: center;
       -webkit-box-pack: center;
       justify-content: center; }.css-cls1-vyoujf-va5xsk { display: -webkit-box; display: -moz-box; display: -ms-flexbox; display: -webkit-flex; display: flex; }"
 `;
 
-exports[`css composes with objects 1`] = `"css-q8izmm"`;
+exports[`css composes with objects 1`] = `" css-obj-1a5gpco"`;
 
-exports[`css composes with objects 2`] = `"css-cls2-1qb2ovg-1em6aur css-cls2-1qb2ovg css-q8izmm"`;
+exports[`css composes with objects 2`] = `"css-cls2-1qb2ovg-1em6aur css-cls2-1qb2ovg  css-obj-1a5gpco"`;
 
 exports[`css composes with objects 3`] = `
 ".css-cls1-1gi569l-b877w5 { background: white;
@@ -37,14 +37,14 @@ exports[`css composes with objects 3`] = `
       z-index: 100;
       font-size: 18px;
       text-align: center;
-      border: solid 1px red; }.css-15famh2{}.css-cls2-1qb2ovg-1em6aur {
+      border: solid 1px red; }.css-cls2-1qb2ovg-1em6aur {
       -webkit-justify-content: center;
       -ms-flex-pack: center;
       -webkit-box-pack: center;
-      justify-content: center; }.css-cls1-vyoujf-va5xsk { display: -webkit-box; display: -moz-box; display: -ms-flexbox; display: -webkit-flex; display: flex; }.css-1fe3owl{display:flex}.css-q8izmm{display:flex;display:block;width:30px;height:calc(40vw - 50px)}.css-q8izmm:hover{color:blue}.css-q8izmm:after{content:\\" \\";color:red}@media(min-width: 420px){.css-q8izmm{color:green}}"
+      justify-content: center; }.css-cls1-vyoujf-va5xsk { display: -webkit-box; display: -moz-box; display: -ms-flexbox; display: -webkit-flex; display: flex; }.css-obj-1fe3owl{display:flex;}.css-obj-1a5gpco{display:flex,block;width:30px;height:calc(40vw - 50px);}.css-obj-1a5gpco:hover{color:blue;}.css-obj-1a5gpco:after{content:\\" \\";color:red;}@media(min-width: 420px){.css-obj-1a5gpco{color:green;}}"
 `;
 
-exports[`css composes with undefined values 1`] = `"css-cls2-1qb2ovg-1em6aur css-cls2-1qb2ovg css-15famh2"`;
+exports[`css composes with undefined values 1`] = `"css-cls2-1qb2ovg-1em6aur css-cls2-1qb2ovg  css-nil"`;
 
 exports[`css composes with undefined values 2`] = `
 ".css-cls1-1gi569l-b877w5 { background: white;
@@ -57,7 +57,7 @@ exports[`css composes with undefined values 2`] = `
       z-index: 100;
       font-size: 18px;
       text-align: center;
-      border: solid 1px red; }.css-15famh2{}.css-cls2-1qb2ovg-1em6aur {
+      border: solid 1px red; }.css-cls2-1qb2ovg-1em6aur {
       -webkit-justify-content: center;
       -ms-flex-pack: center;
       -webkit-box-pack: center;
@@ -80,4 +80,4 @@ exports[`css handles more than 10 dynamic properties 2`] = `
       border: solid 1px red; }"
 `;
 
-exports[`css handles objects 1`] = `"css-1fe3owl"`;
+exports[`css handles objects 1`] = `" css-obj-1fe3owl"`;
diff --git a/test/__snapshots__/react.test.js.snap b/test/__snapshots__/react.test.js.snap
index c1319c41..62eb28e6 100644
--- a/test/__snapshots__/react.test.js.snap
+++ b/test/__snapshots__/react.test.js.snap
@@ -26,12 +26,12 @@ exports[`styled basic render 1`] = `
 `;
 
 exports[`styled basic render with object as style 1`] = `
-.css-kp2uw9 {
+.css-obj-kp2uw9 {
   font-size: 20px;
 }
 
 <h1
-  className="css-kp2uw9"
+  className=" css-obj-kp2uw9"
 >
   hello world
 </h1>
@@ -138,7 +138,7 @@ exports[`styled composes with objects 1`] = `
   font-size: 32px;
 }
 
-.css-1olphq9 {
+.css-obj-ioza2b {
   color: #333;
   font-size: 1.333em;
 }
@@ -152,13 +152,13 @@ exports[`styled composes with objects 1`] = `
     only screen and (-o-min-device-pixel-ratio: 1.5/1),
     only screen and (min-resolution: 144dpi),
     only screen and (min-resolution: 1.5dppx) {
-  .css-1olphq9 {
+  .css-obj-ioza2b {
     font-size: 1.4323121856191332em;
   }
 }
 
 <h1
-  className="css-H1-zejj71-1ym7xv css-H1-zejj71 css-cssB-ob3sm8-fwh0v css-cssB-ob3sm8 css-1olphq9 css-H2-idm3bz-361gww css-H2-idm3bz legacy__class"
+  className="css-H1-zejj71-1ym7xv css-H1-zejj71 css-cssB-ob3sm8-fwh0v css-cssB-ob3sm8  css-obj-ioza2b css-H2-idm3bz-361gww css-H2-idm3bz legacy__class"
   scale={2}
 >
   hello world
@@ -257,17 +257,33 @@ exports[`styled no dynamic 1`] = `
 </h1>
 `;
 
-exports[`styled objects 1`] = `
-.css-1x9ygd3 {
-  padding: 10px;
+exports[`styled object composition 1`] = `
+.css-obj-1vqgc1f {
+  border-radius: 50%;
+  transition: transform 400ms ease-in-out;
+  border: 3px solid currentColor;
+  width: 96px;
+  height: 96px;
+  color: blue;
+}
+
+.css-obj-1vqgc1f:hover {
+  transform: scale(1.2);
 }
 
-.css-1fe3owl {
+<img
+  className="css-Avatar-1ax1vhp    css-obj-1vqgc1f"
+/>
+`;
+
+exports[`styled objects 1`] = `
+.css-obj-1aibhc {
+  padding: 10px;
   display: flex;
 }
 
 <h1
-  className="some-class css-1x9ygd3 css-1fe3owl"
+  className="some-class   css-obj-1aibhc"
   display="flex"
 >
   hello world
diff --git a/test/react.test.js b/test/react.test.js
index 2ea17f6f..bfee2c68 100644
--- a/test/react.test.js
+++ b/test/react.test.js
@@ -97,6 +97,53 @@ describe('styled', () => {
     expect(tree).toMatchSnapshotWithEmotion()
   })
 
+  test('object composition', () => {
+    const imageStyles = css({
+      width: 96,
+      height: 96
+    })
+
+    const fakeBlue = css([
+      {
+        color: "blue"
+      }
+    ]);
+
+    const red = css([
+      {
+        color: "red"
+      }
+    ]);
+
+    const blue = css([
+      red,
+      {
+        color: "blue"
+      }
+    ]);
+
+    const prettyStyles = css([
+      {
+        borderRadius: '50%',
+        transition: 'transform 400ms ease-in-out',
+        ':hover': {
+          transform: 'scale(1.2)'
+        }
+      },
+      {border: '3px solid currentColor'}
+    ])
+
+    const Avatar = styled('img')`
+      composes: ${prettyStyles} ${imageStyles} ${blue}
+    `
+
+    const tree = renderer
+      .create(<Avatar/>)
+      .toJSON()
+
+    expect(tree).toMatchSnapshotWithEmotion()
+  })
+
   test('component as selector', () => {
     const fontSize = '20px'
     const H1 = styled.h1`
@@ -169,9 +216,9 @@ describe('styled', () => {
   test('composes with objects', () => {
     const cssA = {
       color: lighten(0.2, '#000'),
-      'font-size': modularScale(1),
-      [hiDPI(1.5)]: {
-        'font-size': modularScale(1.25)
+      'fontSize': modularScale(1),
+      [hiDPI(1.5).trim()]: {
+        fontSize: modularScale(1.25)
       }
     }