Repository: tailwindlabs/tailwindcss (ts)

Issue:
Custom animations with more than 1 animation separeted by commas dont work
### What version of Tailwind CSS are you using?

v2.2.4

### What build tool (or framework if it abstracts the build tool) are you using?

Next.js 11.0.1

### What version of Node.js are you using?

v16.4.2

### What browser are you using?

Chrome

### What operating system are you using?

Windows

### Reproduction repository

https://play.tailwindcss.com/si3enE3TvA

### Describe your issue

On `tailwind.config.js`
```js
theme: {
  extend: {
    animation: {
      issue: 'animation1, animation2',
    }
  }
}
```
adding multiple animations makes the keyframes disappear from the styled component which makes the animation not work

Pull request:
[JIT] Support animation lists
Fixes #5015

This PR adds support for animation lists in JIT mode, e.g.

```js
{
  theme: {
    extend: {
      animation: {
        multiple: 'bounce 2s linear, pulse 3s ease-in',
      },
    },
  },
}
```

Patch:
diff --git a/plugins/nesting/plugin.js b/plugins/nesting/plugin.js
index 6ba55759..72bb24c2 100644
--- a/plugins/nesting/plugin.js
+++ b/plugins/nesting/plugin.js
@@ -9,7 +9,7 @@ module.exports = function nesting(opts = postcssNested) {
     })
 
     root.walkAtRules('apply', (rule) => {
-      rule.before(postcss.decl({ prop: '__apply', value: rule.params }))
+      rule.before(postcss.decl({ prop: '__apply', value: rule.params, source: rule.source }))
       rule.remove()
     })
 
@@ -32,7 +32,7 @@ module.exports = function nesting(opts = postcssNested) {
     postcss([plugin]).process(root, result.opts).sync()
 
     root.walkDecls('__apply', (decl) => {
-      decl.before(postcss.atRule({ name: 'apply', params: decl.value }))
+      decl.before(postcss.atRule({ name: 'apply', params: decl.value, source: decl.source }))
       decl.remove()
     })
 
diff --git a/src/plugins/animation.js b/src/plugins/animation.js
index e028fb1d..9d51c80b 100644
--- a/src/plugins/animation.js
+++ b/src/plugins/animation.js
@@ -19,18 +19,23 @@ export default function () {
     matchUtilities(
       {
         animate: (value, { includeRules }) => {
-          let { name: animationName } = parseAnimationValue(value)
+          let animations = parseAnimationValue(value)
 
-          if (keyframes[animationName] !== undefined) {
-            includeRules(keyframes[animationName], { respectImportant: false })
-          }
-
-          if (animationName === undefined || keyframes[animationName] === undefined) {
-            return { animation: value }
+          for (let { name } of animations) {
+            if (keyframes[name] !== undefined) {
+              includeRules(keyframes[name], { respectImportant: false })
+            }
           }
 
           return {
-            animation: value.replace(animationName, prefixName(animationName)),
+            animation: animations
+              .map(({ name, value }) => {
+                if (name === undefined || keyframes[name] === undefined) {
+                  return value
+                }
+                return value.replace(name, prefixName(name))
+              })
+              .join(', '),
           }
         },
       },
diff --git a/src/util/parseAnimationValue.js b/src/util/parseAnimationValue.js
index bd34f589..94c77ff3 100644
--- a/src/util/parseAnimationValue.js
+++ b/src/util/parseAnimationValue.js
@@ -20,9 +20,10 @@ const DIGIT = /^(\d+)$/
 
 export default function parseAnimationValue(input) {
   let animations = input.split(COMMA)
-  let result = animations.map((animation) => {
-    let result = {}
-    let parts = animation.trim().split(SPACE)
+  return animations.map((animation) => {
+    let value = animation.trim()
+    let result = { value }
+    let parts = value.split(SPACE)
     let seen = new Set()
 
     for (let part of parts) {
@@ -58,6 +59,4 @@ export default function parseAnimationValue(input) {
 
     return result
   })
-
-  return animations.length > 1 ? result : result[0]
 }


Test patch:
diff --git a/tests/jit/animations.test.css b/tests/jit/animations.test.css
deleted file mode 100644
index 78244c7d..00000000
--- a/tests/jit/animations.test.css
+++ /dev/null
@@ -1,32 +0,0 @@
-@keyframes spin {
-  to {
-    transform: rotate(360deg);
-  }
-}
-.animate-spin {
-  animation: spin 1s linear infinite;
-}
-@keyframes ping {
-  75%,
-  100% {
-    transform: scale(2);
-    opacity: 0;
-  }
-}
-.hover\:animate-ping:hover {
-  animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
-}
-@keyframes bounce {
-  0%,
-  100% {
-    transform: translateY(-25%);
-    animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
-  }
-  50% {
-    transform: none;
-    animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
-  }
-}
-.group:hover .group-hover\:animate-bounce {
-  animation: bounce 1s infinite;
-}
diff --git a/tests/jit/animations.test.html b/tests/jit/animations.test.html
deleted file mode 100644
index 3d5a919e..00000000
--- a/tests/jit/animations.test.html
+++ /dev/null
@@ -1,3 +0,0 @@
-<div class="animate-spin"></div>
-<div class="hover:animate-ping"></div>
-<div class="group-hover:animate-bounce"></div>
diff --git a/tests/jit/animations.test.js b/tests/jit/animations.test.js
index 7ddedcf4..1b88fdcd 100644
--- a/tests/jit/animations.test.js
+++ b/tests/jit/animations.test.js
@@ -1,5 +1,4 @@
 import postcss from 'postcss'
-import fs from 'fs'
 import path from 'path'
 import tailwind from '../../src/jit/index.js'
 
@@ -9,22 +8,199 @@ function run(input, config = {}) {
   })
 }
 
-test('animations', () => {
+test('basic', () => {
   let config = {
-    darkMode: 'class',
     mode: 'jit',
-    purge: [path.resolve(__dirname, './animations.test.html')],
-    corePlugins: {},
-    theme: {},
-    plugins: [],
+    purge: [
+      {
+        raw: `
+          <div class="animate-spin"></div>
+          <div class="hover:animate-ping"></div>
+          <div class="group-hover:animate-bounce"></div>
+        `,
+      },
+    ],
   }
 
   let css = `@tailwind utilities`
 
   return run(css, config).then((result) => {
-    let expectedPath = path.resolve(__dirname, './animations.test.css')
-    let expected = fs.readFileSync(expectedPath, 'utf8')
+    expect(result.css).toMatchFormattedCss(`
+      @keyframes spin {
+        to {
+          transform: rotate(360deg);
+        }
+      }
+      .animate-spin {
+        animation: spin 1s linear infinite;
+      }
+      @keyframes ping {
+        75%,
+        100% {
+          transform: scale(2);
+          opacity: 0;
+        }
+      }
+      .hover\\:animate-ping:hover {
+        animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
+      }
+      @keyframes bounce {
+        0%,
+        100% {
+          transform: translateY(-25%);
+          animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
+        }
+        50% {
+          transform: none;
+          animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
+        }
+      }
+      .group:hover .group-hover\\:animate-bounce {
+        animation: bounce 1s infinite;
+      }
+    `)
+  })
+})
+
+test('custom', () => {
+  let config = {
+    mode: 'jit',
+    purge: [{ raw: `<div class="animate-one"></div>` }],
+    theme: {
+      extend: {
+        keyframes: {
+          one: { to: { transform: 'rotate(360deg)' } },
+        },
+        animation: {
+          one: 'one 2s',
+        },
+      },
+    },
+  }
+
+  let css = `@tailwind utilities`
+
+  return run(css, config).then((result) => {
+    expect(result.css).toMatchFormattedCss(`
+      @keyframes one {
+        to {
+          transform: rotate(360deg);
+        }
+      }
+      .animate-one {
+        animation: one 2s;
+      }
+    `)
+  })
+})
+
+test('custom prefixed', () => {
+  let config = {
+    mode: 'jit',
+    prefix: 'tw-',
+    purge: [{ raw: `<div class="tw-animate-one"></div>` }],
+    theme: {
+      extend: {
+        keyframes: {
+          one: { to: { transform: 'rotate(360deg)' } },
+        },
+        animation: {
+          one: 'one 2s',
+        },
+      },
+    },
+  }
+
+  let css = `@tailwind utilities`
 
-    expect(result.css).toMatchFormattedCss(expected)
+  return run(css, config).then((result) => {
+    expect(result.css).toMatchFormattedCss(`
+      @keyframes tw-one {
+        to {
+          transform: rotate(360deg);
+        }
+      }
+      .tw-animate-one {
+        animation: tw-one 2s;
+      }
+    `)
+  })
+})
+
+test('multiple', () => {
+  let config = {
+    mode: 'jit',
+    purge: [{ raw: `<div class="animate-multiple"></div>` }],
+    theme: {
+      extend: {
+        animation: {
+          multiple: 'bounce 2s linear, pulse 3s ease-in',
+        },
+      },
+    },
+  }
+
+  let css = `@tailwind utilities`
+
+  return run(css, config).then((result) => {
+    expect(result.css).toMatchFormattedCss(`
+      @keyframes bounce {
+        0%,
+        100% {
+          transform: translateY(-25%);
+          animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
+        }
+        50% {
+          transform: none;
+          animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
+        }
+      }
+      @keyframes pulse {
+        50% {
+          opacity: 0.5;
+        }
+      }
+      .animate-multiple {
+        animation: bounce 2s linear, pulse 3s ease-in;
+      }
+    `)
+  })
+})
+
+test('multiple custom', () => {
+  let config = {
+    mode: 'jit',
+    purge: [{ raw: `<div class="animate-multiple"></div>` }],
+    theme: {
+      extend: {
+        keyframes: {
+          one: { to: { transform: 'rotate(360deg)' } },
+          two: { to: { transform: 'scale(1.23)' } },
+        },
+        animation: {
+          multiple: 'one 2s, two 3s',
+        },
+      },
+    },
+  }
+
+  let css = `@tailwind utilities`
+
+  return run(css, config).then((result) => {
+    expect(result.css).toMatchFormattedCss(`
+      @keyframes one {
+        to {
+          transform: rotate(360deg);
+        }
+      }
+      @keyframes two {
+        to {
+          transform: scale(1.23);
+        }
+      }
+      .animate-multiple {
+        animation: one 2s, two 3s;
+      }
+    `)
   })
 })
diff --git a/tests/parseAnimationValue.test.js b/tests/parseAnimationValue.test.js
index 38e360e7..8fd211d2 100644
--- a/tests/parseAnimationValue.test.js
+++ b/tests/parseAnimationValue.test.js
@@ -5,6 +5,7 @@ describe('Tailwind Defaults', () => {
     [
       'spin 1s linear infinite',
       {
+        value: 'spin 1s linear infinite',
         name: 'spin',
         duration: '1s',
         timingFunction: 'linear',
@@ -14,15 +15,26 @@ describe('Tailwind Defaults', () => {
     [
       'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
       {
+        value: 'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
         name: 'ping',
         duration: '1s',
         timingFunction: 'cubic-bezier(0, 0, 0.2, 1)',
         iterationCount: 'infinite',
       },
     ],
-    ['bounce 1s infinite', { name: 'bounce', duration: '1s', iterationCount: 'infinite' }],
+    [
+      'bounce 1s infinite',
+      {
+        value: 'bounce 1s infinite',
+        name: 'bounce',
+        duration: '1s',
+        iterationCount: 'infinite',
+      },
+    ],
   ])('should be possible to parse: "%s"', (input, expected) => {
-    expect(parseAnimationValue(input)).toEqual(expected)
+    const parsed = parseAnimationValue(input)
+    expect(parsed).toHaveLength(1)
+    expect(parsed[0]).toEqual(expected)
   })
 })
 
@@ -31,6 +43,7 @@ describe('MDN Examples', () => {
     [
       '3s ease-in 1s 2 reverse both paused slidein',
       {
+        value: '3s ease-in 1s 2 reverse both paused slidein',
         delay: '1s',
         direction: 'reverse',
         duration: '3s',
@@ -44,15 +57,18 @@ describe('MDN Examples', () => {
     [
       'slidein 3s linear 1s',
       {
+        value: 'slidein 3s linear 1s',
         delay: '1s',
         duration: '3s',
         name: 'slidein',
         timingFunction: 'linear',
       },
     ],
-    ['slidein 3s', { duration: '3s', name: 'slidein' }],
+    ['slidein 3s', { value: 'slidein 3s', duration: '3s', name: 'slidein' }],
   ])('should be possible to parse: "%s"', (input, expected) => {
-    expect(parseAnimationValue(input)).toEqual(expected)
+    const parsed = parseAnimationValue(input)
+    expect(parsed).toHaveLength(1)
+    expect(parsed[0]).toEqual(expected)
   })
 })
 
@@ -83,8 +99,9 @@ describe('duration & delay', () => {
     ['spin -200.321ms -100.321ms linear', { duration: '-200.321ms', delay: '-100.321ms' }],
   ])('should be possible to parse "%s" into %o', (input, { duration, delay }) => {
     const parsed = parseAnimationValue(input)
-    expect(parsed.duration).toEqual(duration)
-    expect(parsed.delay).toEqual(delay)
+    expect(parsed).toHaveLength(1)
+    expect(parsed[0].duration).toEqual(duration)
+    expect(parsed[0].delay).toEqual(delay)
   })
 })
 
@@ -106,7 +123,9 @@ describe('iteration count', () => {
   ])(
     'should be possible to parse "%s" with an iteraction count of "%s"',
     (input, iterationCount) => {
-      expect(parseAnimationValue(input).iterationCount).toEqual(iterationCount)
+      const parsed = parseAnimationValue(input)
+      expect(parsed).toHaveLength(1)
+      expect(parsed[0].iterationCount).toEqual(iterationCount)
     }
   )
 })
@@ -123,18 +142,21 @@ describe('multiple animations', () => {
     expect(parsed).toHaveLength(3)
     expect(parsed).toEqual([
       {
+        value: 'spin 1s linear infinite',
         name: 'spin',
         duration: '1s',
         timingFunction: 'linear',
         iterationCount: 'infinite',
       },
       {
+        value: 'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
         name: 'ping',
         duration: '1s',
         timingFunction: 'cubic-bezier(0, 0, 0.2, 1)',
         iterationCount: 'infinite',
       },
       {
+        value: 'pulse 2s cubic-bezier(0.4, 0, 0.6) infinite',
         name: 'pulse',
         duration: '2s',
         timingFunction: 'cubic-bezier(0.4, 0, 0.6)',
@@ -145,20 +167,21 @@ describe('multiple animations', () => {
 })
 
 it.each`
-  input                                                           | direction    | playState    | fillMode       | iterationCount | timingFunction | duration | delay   | name
-  ${'1s spin 1s infinite'}                                        | ${undefined} | ${undefined} | ${undefined}   | ${'infinite'}  | ${undefined}   | ${'1s'}  | ${'1s'} | ${'spin'}
-  ${'infinite infinite 1s 1s'}                                    | ${undefined} | ${undefined} | ${undefined}   | ${'infinite'}  | ${undefined}   | ${'1s'}  | ${'1s'} | ${'infinite'}
-  ${'ease 1s ease 1s'}                                            | ${undefined} | ${undefined} | ${undefined}   | ${undefined}   | ${'ease'}      | ${'1s'}  | ${'1s'} | ${'ease'}
-  ${'normal paused backwards infinite ease-in 1s 2s name'}        | ${'normal'}  | ${'paused'}  | ${'backwards'} | ${'infinite'}  | ${'ease-in'}   | ${'1s'}  | ${'2s'} | ${'name'}
-  ${'paused backwards infinite ease-in 1s 2s name normal'}        | ${'normal'}  | ${'paused'}  | ${'backwards'} | ${'infinite'}  | ${'ease-in'}   | ${'1s'}  | ${'2s'} | ${'name'}
-  ${'backwards infinite ease-in 1s 2s name normal paused'}        | ${'normal'}  | ${'paused'}  | ${'backwards'} | ${'infinite'}  | ${'ease-in'}   | ${'1s'}  | ${'2s'} | ${'name'}
-  ${'infinite ease-in 1s 2s name normal paused backwards'}        | ${'normal'}  | ${'paused'}  | ${'backwards'} | ${'infinite'}  | ${'ease-in'}   | ${'1s'}  | ${'2s'} | ${'name'}
-  ${'ease-in 1s 2s name normal paused backwards infinite'}        | ${'normal'}  | ${'paused'}  | ${'backwards'} | ${'infinite'}  | ${'ease-in'}   | ${'1s'}  | ${'2s'} | ${'name'}
-  ${'1s 2s name normal paused backwards infinite ease-in'}        | ${'normal'}  | ${'paused'}  | ${'backwards'} | ${'infinite'}  | ${'ease-in'}   | ${'1s'}  | ${'2s'} | ${'name'}
-  ${'2s name normal paused backwards infinite ease-in 1s'}        | ${'normal'}  | ${'paused'}  | ${'backwards'} | ${'infinite'}  | ${'ease-in'}   | ${'2s'}  | ${'1s'} | ${'name'}
-  ${'name normal paused backwards infinite ease-in 1s 2s'}        | ${'normal'}  | ${'paused'}  | ${'backwards'} | ${'infinite'}  | ${'ease-in'}   | ${'1s'}  | ${'2s'} | ${'name'}
-  ${'  name   normal  paused backwards infinite ease-in 1s 2s  '} | ${'normal'}  | ${'paused'}  | ${'backwards'} | ${'infinite'}  | ${'ease-in'}   | ${'1s'}  | ${'2s'} | ${'name'}
+  input                                                           | value                                                       | direction    | playState    | fillMode       | iterationCount | timingFunction | duration | delay   | name
+  ${'1s spin 1s infinite'}                                        | ${'1s spin 1s infinite'}                                    | ${undefined} | ${undefined} | ${undefined}   | ${'infinite'}  | ${undefined}   | ${'1s'}  | ${'1s'} | ${'spin'}
+  ${'infinite infinite 1s 1s'}                                    | ${'infinite infinite 1s 1s'}                                | ${undefined} | ${undefined} | ${undefined}   | ${'infinite'}  | ${undefined}   | ${'1s'}  | ${'1s'} | ${'infinite'}
+  ${'ease 1s ease 1s'}                                            | ${'ease 1s ease 1s'}                                        | ${undefined} | ${undefined} | ${undefined}   | ${undefined}   | ${'ease'}      | ${'1s'}  | ${'1s'} | ${'ease'}
+  ${'normal paused backwards infinite ease-in 1s 2s name'}        | ${'normal paused backwards infinite ease-in 1s 2s name'}    | ${'normal'}  | ${'paused'}  | ${'backwards'} | ${'infinite'}  | ${'ease-in'}   | ${'1s'}  | ${'2s'} | ${'name'}
+  ${'paused backwards infinite ease-in 1s 2s name normal'}        | ${'paused backwards infinite ease-in 1s 2s name normal'}    | ${'normal'}  | ${'paused'}  | ${'backwards'} | ${'infinite'}  | ${'ease-in'}   | ${'1s'}  | ${'2s'} | ${'name'}
+  ${'backwards infinite ease-in 1s 2s name normal paused'}        | ${'backwards infinite ease-in 1s 2s name normal paused'}    | ${'normal'}  | ${'paused'}  | ${'backwards'} | ${'infinite'}  | ${'ease-in'}   | ${'1s'}  | ${'2s'} | ${'name'}
+  ${'infinite ease-in 1s 2s name normal paused backwards'}        | ${'infinite ease-in 1s 2s name normal paused backwards'}    | ${'normal'}  | ${'paused'}  | ${'backwards'} | ${'infinite'}  | ${'ease-in'}   | ${'1s'}  | ${'2s'} | ${'name'}
+  ${'ease-in 1s 2s name normal paused backwards infinite'}        | ${'ease-in 1s 2s name normal paused backwards infinite'}    | ${'normal'}  | ${'paused'}  | ${'backwards'} | ${'infinite'}  | ${'ease-in'}   | ${'1s'}  | ${'2s'} | ${'name'}
+  ${'1s 2s name normal paused backwards infinite ease-in'}        | ${'1s 2s name normal paused backwards infinite ease-in'}    | ${'normal'}  | ${'paused'}  | ${'backwards'} | ${'infinite'}  | ${'ease-in'}   | ${'1s'}  | ${'2s'} | ${'name'}
+  ${'2s name normal paused backwards infinite ease-in 1s'}        | ${'2s name normal paused backwards infinite ease-in 1s'}    | ${'normal'}  | ${'paused'}  | ${'backwards'} | ${'infinite'}  | ${'ease-in'}   | ${'2s'}  | ${'1s'} | ${'name'}
+  ${'name normal paused backwards infinite ease-in 1s 2s'}        | ${'name normal paused backwards infinite ease-in 1s 2s'}    | ${'normal'}  | ${'paused'}  | ${'backwards'} | ${'infinite'}  | ${'ease-in'}   | ${'1s'}  | ${'2s'} | ${'name'}
+  ${'  name   normal  paused backwards infinite ease-in 1s 2s  '} | ${'name   normal  paused backwards infinite ease-in 1s 2s'} | ${'normal'}  | ${'paused'}  | ${'backwards'} | ${'infinite'}  | ${'ease-in'}   | ${'1s'}  | ${'2s'} | ${'name'}
 `('should parse "$input" correctly', ({ input, ...expected }) => {
   let parsed = parseAnimationValue(input)
-  expect(parsed).toEqual(expected)
+  expect(parsed).toHaveLength(1)
+  expect(parsed[0]).toEqual(expected)
 })