Repository: tkh44/emotion
Pull request #22 (2017-06-24 20:37:54)

keyframe work started


Patch:
diff --git a/.babelrc b/.babelrc
index f9481593..4333b8e7 100644
--- a/.babelrc
+++ b/.babelrc
@@ -7,7 +7,8 @@
       }
     ],
     "stage-0",
-    "react"
+    "react",
+    "flow"
   ],
   "env": {
     "test": {
@@ -19,7 +20,8 @@
           }
         ],
         "stage-2",
-        "react"
+        "react",
+        "flow"
       ]
     }
   }
diff --git a/.flowconfig b/.flowconfig
new file mode 100644
index 00000000..ce334480
--- /dev/null
+++ b/.flowconfig
@@ -0,0 +1,24 @@
+[ignore]
+<PROJECT_ROOT>/node_modules/[^s].*
+<PROJECT_ROOT>/node_modules/s[^t].*
+<PROJECT_ROOT>/node_modules/st[^y].*
+<PROJECT_ROOT>/node_modules/sty[^l].*
+<PROJECT_ROOT>/node_modules/styl[^e].*
+<PROJECT_ROOT>/node_modules/style[^d].*
+<PROJECT_ROOT>/node_modules/styled[^\-].*
+<PROJECT_ROOT>/node_modules/styled-[^c].*
+<PROJECT_ROOT>/node_modules/styled-c[^o].*
+<PROJECT_ROOT>/node_modules/styled-co[^m].*
+<PROJECT_ROOT>/node_modules/styled-com[^p].*
+<PROJECT_ROOT>/node_modules/styled-comp[^o].*
+<PROJECT_ROOT>/node_modules/styled-compo[^n].*
+<PROJECT_ROOT>/node_modules/styled-compon[^e].*
+<PROJECT_ROOT>/node_modules/styled-compone[^n].*
+<PROJECT_ROOT>/node_modules/styled-componen[^t].*
+<PROJECT_ROOT>/node_modules/styled-component[^s].*
+<PROJECT_ROOT>/node_modules/styled-components.+
+[include]
+
+[libs]
+
+[options]
diff --git a/example/src/blocks/basic.example b/example/src/blocks/basic.example
index daebf046..c57bed69 100644
--- a/example/src/blocks/basic.example
+++ b/example/src/blocks/basic.example
@@ -18,6 +18,33 @@ const CodeBlock = styled('pre')`
 
 `
 
+// from https://daneden.github.io/animate.css/
+const bounce = keyframes`
+  from, 20%, 53%, 80%, to {
+    animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
+    transform: translate3d(0,0,0);
+  }
+
+  40%, 43% {
+    animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
+    transform: translate3d(0, -30px, 0);
+  }
+
+  70% {
+    animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
+    transform: translate3d(0, -15px, 0);
+  }
+
+  90% {
+    transform: translate3d(0,-4px,0);
+  }
+`
+
+const BouncyHeading = styled(Heading)`
+  animation: ${bounce} 1s linear infinite;
+  transform-origin: center bottom;
+`
+
 render(
   <ReactMarkdown
     source={docMarkdown}
diff --git a/example/src/main.js b/example/src/main.js
index 98624dd0..37c85951 100755
--- a/example/src/main.js
+++ b/example/src/main.js
@@ -4,7 +4,7 @@ import './dracula.css'
 import React from 'react'
 import { render } from 'react-dom'
 import styled from 'emotion/styled'
-import { css, fragment } from 'emotion'
+import { css, fragment, keyframes } from 'emotion'
 import ReactMarkdown from 'react-markdown'
 import colors from 'open-color/open-color.json'
 import Playground from './playground'
@@ -30,6 +30,7 @@ class App extends React.Component {
           scope={{
             React,
             css,
+            keyframes,
             fragment,
             styled,
             colors,
diff --git a/example/src/playground/index.js b/example/src/playground/index.js
index 92f9ab50..48f878c4 100644
--- a/example/src/playground/index.js
+++ b/example/src/playground/index.js
@@ -86,7 +86,7 @@ class ReactPlayground extends Component {
     } = this.props
 
     return (
-      <PlaygroundContent style={{ display: 'flex' }}>
+      <Content style={{ display: 'flex' }}>
         <Editor
           codeText={code}
           external={external}
@@ -101,7 +101,7 @@ class ReactPlayground extends Component {
           noRender={noRender}
           previewComponent={previewComponent}
         />
-      </PlaygroundContent>
+      </Content>
     )
   }
 }
diff --git a/src/babel.js b/src/babel.js
index 86e823da..7868ddaf 100644
--- a/src/babel.js
+++ b/src/babel.js
@@ -1,4 +1,5 @@
-import { inline } from './inline'
+import { parseKeyframes } from './parser'
+import { inline, keyframes } from './inline'
 import findAndReplaceAttrs from './attrs'
 
 function parseDynamicValues (rules, t) {
@@ -186,6 +187,33 @@ export default function (babel) {
               )
             ])
           )
+        } else if (
+          t.isIdentifier(path.node.tag) &&
+          path.node.tag.name === 'keyframes'
+        ) {
+          const { hash, name, rules } = keyframes(
+            path.node.quasi,
+            identifierName,
+            'animation'
+          )
+
+          path.replaceWith(
+            t.callExpression(t.identifier('keyframes'), [
+              t.stringLiteral(`${name}-${hash}`),
+              t.arrayExpression(path.node.quasi.expressions),
+              t.functionExpression(
+                t.identifier('createEmotionKeyframe'),
+                path.node.quasi.expressions.map((x, i) =>
+                  t.identifier(`x${i}`)
+                ),
+                t.blockStatement([
+                  t.returnStatement(
+                    t.arrayExpression(rules.map(r => t.stringLiteral(r)))
+                  )
+                ])
+              )
+            ])
+          )
         }
       }
     }
diff --git a/src/index.js b/src/index.js
index 9ce3f356..ddb87763 100644
--- a/src/index.js
+++ b/src/index.js
@@ -12,10 +12,14 @@ export function flush () {
   sheet.inject()
 }
 
-export function css (cls, vars, content) {
+export function css (
+  cls: string,
+  vars: Array<string | number | (() => string | number)>,
+  content: () => mixed[]
+) {
   // inline mode
   vars = vars.map(v => (/^frag-/.exec(v) ? fragments[v] : v))
-  let src = content(...vars) // returns an array
+  let src = content(...vars)
   let hash = hashArray(src)
 
   if (!inserted[hash]) {
@@ -29,9 +33,13 @@ export function css (cls, vars, content) {
 
 const fragments = {}
 
-export function fragment (frag, vars, content) {
+export function fragment (
+  frag: string,
+  vars: Array<string | number | (() => string | number)>,
+  content: () => mixed[]
+) {
   vars = vars.map(v => (/^frag-/.exec(v) ? fragments[v] : v))
-  let src = content(...vars) // return array?
+  let src = content(...vars)
   if (src.length > 1) {
     throw new Error('what up!')
   }
@@ -45,6 +53,22 @@ export function fragment (frag, vars, content) {
   return `${frag}-${hash}`
 }
 
+export function keyframes (
+  kfm: string,
+  vars: Array<string | number | (() => string | number)>,
+  content: () => mixed[]
+) {
+  let src = content(...vars)
+  let hash = hashArray(src)
+  if (!inserted[hash]) {
+    inserted[hash] = true
+    src.forEach(r => {
+      return sheet.insert(`@keyframes ${kfm}-${hash} {${r}}`)
+    })
+  }
+  return `${kfm}-${hash}`
+}
+
 export function hydrate (ids) {
   ids.forEach(id => (inserted[id] = true))
 }
diff --git a/src/inline.js b/src/inline.js
index 4af0737f..797c93d5 100644
--- a/src/inline.js
+++ b/src/inline.js
@@ -1,4 +1,4 @@
-import parseCSS from './parser'
+import { parseCSS } from './parser'
 import hashArray from './hash'
 
 function extractNameFromProperty (str) {
@@ -60,3 +60,19 @@ export function inline (quasi, identifierName, prefix) {
 
   return { hash, name, rules }
 }
+
+export function keyframes (quasi, identifierName, prefix) {
+  let strs = quasi.quasis.map(x => x.value.cooked)
+  let hash = hashArray([...strs])
+  let name = getName(
+    extractNameFromProperty(strs.join('xxx')),
+    identifierName,
+    prefix
+  )
+
+  return {
+    hash,
+    name,
+    rules: [parseCSS(`{ ${strs.join('').trim()} }`).join('').trim()]
+  }
+}
diff --git a/src/parser.js b/src/parser.js
index 4ce13c6e..bd3d2b86 100644
--- a/src/parser.js
+++ b/src/parser.js
@@ -1,9 +1,10 @@
+// @flow
 import parse from 'styled-components/lib/vendor/postcss-safe-parser/parse'
 import postcssNested from 'styled-components/lib/vendor/postcss-nested'
 import stringify from 'styled-components/lib/vendor/postcss/stringify'
 import autoprefix from 'styled-components/lib/utils/autoprefix'
 
-export default function parser (css, options = {}) {
+export function parseCSS (css: string, options: { nested: boolean } = { nested: true }): string[] {
   // todo - handle errors
   const root = parse(css)
   if (options.nested !== false) postcssNested(root)
diff --git a/src/sheet.js b/src/sheet.js
index f695162b..34508bb6 100644
--- a/src/sheet.js
+++ b/src/sheet.js
@@ -39,9 +39,10 @@ function sheetForTag (tag) {
   }
 }
 
-const isBrowser = typeof window !== 'undefined'
-const isDev = process.env.NODE_ENV === 'development' || !process.env.NODE_ENV // (x => (x === 'development') || !x)(process.env.NODE_ENV)
-const isTest = process.env.NODE_ENV === 'test'
+const isBrowser: boolean = typeof window !== 'undefined'
+const isDev: boolean =
+  process.env.NODE_ENV === 'development' || !process.env.NODE_ENV // (x => (x === 'development') || !x)(process.env.NODE_ENV)
+const isTest: boolean = process.env.NODE_ENV === 'test'
 
 const oldIE = (() => {
   if (isBrowser) {
@@ -64,7 +65,7 @@ export function StyleSheet (
   {
     speedy = !isDev && !isTest,
     maxLength = isBrowser && oldIE ? 4000 : 65000
-  } = {}
+  }: { speedy: boolean, maxLength: number } = {}
 ) {
   this.isSpeedy = speedy // the big drawback here is that the css won't be editable in devtools
   this.sheet = undefined
diff --git a/src/styled.js b/src/styled.js
index 9f61e25d..26f874cf 100644
--- a/src/styled.js
+++ b/src/styled.js
@@ -2,6 +2,12 @@ import React from 'react'
 import { css } from './index'
 
 export default function (tag, cls, vars = [], content) {
+  if (!tag) {
+    throw new Error(
+      'You are trying to create a styled element with an undefined component.\nYou may have forgotten to import it.'
+    )
+  }
+
   function Styled (props) {
     const className = css(
       cls,
@@ -13,7 +19,7 @@ export default function (tag, cls, vars = [], content) {
       tag,
       Object.assign({}, props, {
         className: props.className
-          ? props.className + ' ' + className
+          ? className + ' ' + props.className
           : className
       })
     )


Test patch:
diff --git a/test/.babelrc b/test/.babelrc
index a2f709d2..e0132fae 100644
--- a/test/.babelrc
+++ b/test/.babelrc
@@ -1,5 +1,6 @@
 {
   "presets": [
+    "flow",
     "env",
     "stage-0",
     "react",
diff --git a/test/__snapshots__/babel.test.js.snap b/test/__snapshots__/babel.test.js.snap
index b1ec7f4e..a7a22cf9 100644
--- a/test/__snapshots__/babel.test.js.snap
+++ b/test/__snapshots__/babel.test.js.snap
@@ -1,5 +1,12 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`emotion/babel babel keyframes keyframe basic 1`] = `
+"
+const rotate360 = keyframes(\\"animation-rotate360-f35ahc\\", [], function createEmotionKeyframe() {
+  return [\\"from {\\\\n    -webkit-transform: rotate(0deg);\\\\n    -ms-transform: rotate(0deg);\\\\n    transform: rotate(0deg);\\\\n} to {\\\\n    -webkit-transform: rotate(360deg);\\\\n    -ms-transform: rotate(360deg);\\\\n    transform: rotate(360deg);\\\\n}\\"];
+});"
+`;
+
 exports[`emotion/babel babel styled component attr 1`] = `
 "styled('input', 'css-14vghnb', [function getMargin(props) {
        return props.margin;
diff --git a/test/__snapshots__/keyframes.test.js.snap b/test/__snapshots__/keyframes.test.js.snap
new file mode 100644
index 00000000..bdd25434
--- /dev/null
+++ b/test/__snapshots__/keyframes.test.js.snap
@@ -0,0 +1,15 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`keyframes renders 1`] = `
+.css-H1-o0kcx2-5hrnwy {
+  font-size: 20px;
+  -webkit-animation: animation-bounce-10a3qiv-c1ph90 2s linear infinite;
+  animation: animation-bounce-10a3qiv-c1ph90 2s linear infinite;
+}
+
+<h1
+  className="css-H1-o0kcx2-5hrnwy"
+>
+  hello world
+</h1>
+`;
diff --git a/test/__snapshots__/styled.test.js.snap b/test/__snapshots__/styled.test.js.snap
index 2e73adfe..c4ccdc93 100644
--- a/test/__snapshots__/styled.test.js.snap
+++ b/test/__snapshots__/styled.test.js.snap
@@ -30,7 +30,7 @@ exports[`styled call expression 1`] = `
 }
 
 <h1
-  className="legacy__class css-H1-13wdnau-xy96vj"
+  className="css-H1-13wdnau-xy96vj legacy__class"
 >
   hello world
 </h1>
@@ -46,7 +46,7 @@ exports[`styled composition 1`] = `
 }
 
 <h1
-  className="legacy__class css-H2-vxb7tq-1gn1gp5 css-H1-13wdnau-xy96vj"
+  className="css-H1-13wdnau-xy96vj css-H2-vxb7tq-1gn1gp5 legacy__class"
 >
   hello world
 </h1>
@@ -64,7 +64,7 @@ exports[`styled fragments 1`] = `
 }
 
 <h1
-  className="legacy__class css-H2-idm3bz-361gww css-H1-1qd8mfb-1mxqzi0"
+  className="css-H1-1qd8mfb-1mxqzi0 css-H2-idm3bz-361gww legacy__class"
   scale={2}
 >
   hello world
@@ -81,7 +81,7 @@ exports[`styled function in expression 1`] = `
 }
 
 <h1
-  className="legacy__class css-H2-vxb7tq-1wx98rf css-H1-13wdnau-xy96vj"
+  className="css-H1-13wdnau-xy96vj css-H2-vxb7tq-1wx98rf legacy__class"
   scale={2}
 >
   hello world
@@ -106,7 +106,7 @@ exports[`styled higher order component 1`] = `
 }
 
 <div
-  className="css-onyx-wn422p-gyln3i css-Content-13wdnau-1z6hk6"
+  className="css-Content-13wdnau-1z6hk6 css-onyx-wn422p-gyln3i"
 />
 `;
 
diff --git a/test/babel.test.js b/test/babel.test.js
index 8f05177b..65cf8043 100644
--- a/test/babel.test.js
+++ b/test/babel.test.js
@@ -178,4 +178,22 @@ describe('emotion/babel', () => {
       expect(code).toMatchSnapshot()
     })
   })
+  describe('babel keyframes', () => {
+    test('keyframe basic', () => {
+      const basic = `
+        const rotate360 = keyframes\`
+          from {
+            transform: rotate(0deg);
+          }
+        
+          to {
+            transform: rotate(360deg);
+          }
+      \`;`
+      const { code } = babel.transform(basic, {
+        plugins: [[plugin]]
+      })
+      expect(code).toMatchSnapshot()
+    })
+  })
 })
diff --git a/test/keyframes.test.js b/test/keyframes.test.js
new file mode 100644
index 00000000..bbfa41bb
--- /dev/null
+++ b/test/keyframes.test.js
@@ -0,0 +1,53 @@
+/* eslint-disable jsx-quotes,no-useless-escape,no-template-curly-in-string */
+/* eslint-env jest */
+import React from 'react'
+import renderer from 'react-test-renderer'
+import { matcher, serializer } from '../jest-utils'
+
+// eslint-disable-next-line no-unused-vars
+import css, { fragment, keyframes } from '../src/index'
+import styled from '../src/styled'
+
+expect.addSnapshotSerializer(serializer)
+expect.extend(matcher)
+
+describe('keyframes', () => {
+  test('renders', () => {
+    const fontSize = 20
+    const bounce = keyframes`
+      from, 20%, 53%, 80%, to {
+        animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
+        transform: translate3d(0,0,0);
+      }
+    
+      40%, 43% {
+        animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
+        transform: translate3d(0, -30px, 0);
+      }
+    
+      70% {
+        animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
+        transform: translate3d(0, -15px, 0);
+      }
+    
+      90% {
+        transform: translate3d(0,-4px,0);
+      }
+    `
+
+    const H1 = styled.h1`
+      font-size: ${fontSize}px;
+      animation: ${bounce} 2s linear infinite;
+    `
+
+    const tree = renderer
+      .create(
+        <H1>
+          hello world
+        </H1>
+      )
+      .toJSON()
+
+    expect(tree).toMatchSnapshotWithEmotion()
+  })
+})