Repository: microsoft/griffel (ts)

Issue:
animationName with array value not supported by babel-plugin
In an array with multiple animations is given as the value of the `animationName` attribute within a `makeStyles` call and the babel plugin optimizations are enabled, there is an error thrown at during build at the `babel:postprocess` stage indicating that `ruleValue.indexOf is not a function`:

![image](https://user-images.githubusercontent.com/7798177/203412603-9d5f11ff-9a8b-4b37-ab17-94a3b576bb93.png)

<img width="689" alt="image" src="https://user-images.githubusercontent.com/7798177/203412917-2bcfdc20-01b5-4287-b1d2-48d4a8fe54d6.png">


Pull request:
fix: Babel plugin no longer errors on array values being passed to animationName
## PR Description

This PR fixes an issue with the babel plugin that errored when an array of animations was being passed to `animationName`. This happened because the plugin tried to parse each animation as an individual rule instead of as an object with rules inside of it.

## Related issue(s)

- Fixes #275

Patch:
diff --git a/change/@griffel-babel-preset-3f5d5c23-15e4-450a-b8d7-eafa30001b48.json b/change/@griffel-babel-preset-3f5d5c23-15e4-450a-b8d7-eafa30001b48.json
new file mode 100644
index 0000000..de7fdb9
--- /dev/null
+++ b/change/@griffel-babel-preset-3f5d5c23-15e4-450a-b8d7-eafa30001b48.json
@@ -0,0 +1,7 @@
+{
+  "type": "patch",
+  "comment": "fix: Handle arrays in `animationName` definitions.",
+  "packageName": "@griffel/babel-preset",
+  "email": "humberto_makoto@hotmail.com",
+  "dependentChangeType": "patch"
+}
diff --git a/change/@griffel-core-da60ef94-dd69-4128-9f8f-53fceb3ec3c3.json b/change/@griffel-core-da60ef94-dd69-4128-9f8f-53fceb3ec3c3.json
new file mode 100644
index 0000000..eb2ccb9
--- /dev/null
+++ b/change/@griffel-core-da60ef94-dd69-4128-9f8f-53fceb3ec3c3.json
@@ -0,0 +1,7 @@
+{
+  "type": "patch",
+  "comment": "chore: export \"CSSBucketEntry\" type for internal usage",
+  "packageName": "@griffel/core",
+  "email": "olfedias@microsoft.com",
+  "dependentChangeType": "patch"
+}
diff --git a/change/@griffel-webpack-extraction-plugin-b995688a-9700-4878-8832-49047eed3bb3.json b/change/@griffel-webpack-extraction-plugin-b995688a-9700-4878-8832-49047eed3bb3.json
new file mode 100644
index 0000000..a6a1e7e
--- /dev/null
+++ b/change/@griffel-webpack-extraction-plugin-b995688a-9700-4878-8832-49047eed3bb3.json
@@ -0,0 +1,7 @@
+{
+  "type": "patch",
+  "comment": "fix: use style buckets in CSS extraction",
+  "packageName": "@griffel/webpack-extraction-plugin",
+  "email": "olfedias@microsoft.com",
+  "dependentChangeType": "patch"
+}
diff --git a/packages/babel-preset/__fixtures__/keyframes/code.ts b/packages/babel-preset/__fixtures__/keyframes/code.ts
new file mode 100644
index 0000000..bffd271
--- /dev/null
+++ b/packages/babel-preset/__fixtures__/keyframes/code.ts
@@ -0,0 +1,22 @@
+import { makeStyles } from '@griffel/react';
+
+export const useStyles = makeStyles({
+  single: {
+    animationName: {
+      from: { transform: 'rotate(0deg)' },
+      to: { transform: 'rotate(360deg)' },
+    },
+  },
+  multiple: {
+    animationName: [
+      {
+        from: { transform: 'rotate(0deg)' },
+        to: { transform: 'rotate(360deg)' },
+      },
+      {
+        from: { height: '100px' },
+        to: { height: '200px' },
+      },
+    ],
+  },
+});
diff --git a/packages/babel-preset/__fixtures__/keyframes/output.ts b/packages/babel-preset/__fixtures__/keyframes/output.ts
new file mode 100644
index 0000000..d05166e
--- /dev/null
+++ b/packages/babel-preset/__fixtures__/keyframes/output.ts
@@ -0,0 +1,27 @@
+import { __styles } from '@griffel/react';
+export const useStyles = __styles(
+  {
+    single: {
+      Bv12yb3: ['f1g6ul6r', 'f1fp4ujf'],
+    },
+    multiple: {
+      Bv12yb3: ['f1e467oh', 'f1w9bd63'],
+    },
+  },
+  {
+    k: [
+      '@-webkit-keyframes f1q8eu9e{from{-webkit-transform:rotate(0deg);-moz-transform:rotate(0deg);-ms-transform:rotate(0deg);transform:rotate(0deg);}to{-webkit-transform:rotate(360deg);-moz-transform:rotate(360deg);-ms-transform:rotate(360deg);transform:rotate(360deg);}}',
+      '@-webkit-keyframes f55c0se{from{-webkit-transform:rotate(0deg);-moz-transform:rotate(0deg);-ms-transform:rotate(0deg);transform:rotate(0deg);}to{-webkit-transform:rotate(-360deg);-moz-transform:rotate(-360deg);-ms-transform:rotate(-360deg);transform:rotate(-360deg);}}',
+      '@keyframes f1q8eu9e{from{-webkit-transform:rotate(0deg);-moz-transform:rotate(0deg);-ms-transform:rotate(0deg);transform:rotate(0deg);}to{-webkit-transform:rotate(360deg);-moz-transform:rotate(360deg);-ms-transform:rotate(360deg);transform:rotate(360deg);}}',
+      '@keyframes f55c0se{from{-webkit-transform:rotate(0deg);-moz-transform:rotate(0deg);-ms-transform:rotate(0deg);transform:rotate(0deg);}to{-webkit-transform:rotate(-360deg);-moz-transform:rotate(-360deg);-ms-transform:rotate(-360deg);transform:rotate(-360deg);}}',
+      '@-webkit-keyframes f19fnzg9{from{height:100px;}to{height:200px;}}',
+      '@keyframes f19fnzg9{from{height:100px;}to{height:200px;}}',
+    ],
+    d: [
+      '.f1g6ul6r{-webkit-animation-name:f1q8eu9e;animation-name:f1q8eu9e;}',
+      '.f1fp4ujf{-webkit-animation-name:f55c0se;animation-name:f55c0se;}',
+      '.f1e467oh{-webkit-animation-name:f1q8eu9e,f19fnzg9;animation-name:f1q8eu9e,f19fnzg9;}',
+      '.f1w9bd63{-webkit-animation-name:f55c0se,f19fnzg9;animation-name:f55c0se,f19fnzg9;}',
+    ],
+  },
+);
diff --git a/packages/babel-preset/src/assets/normalizeStyleRules.ts b/packages/babel-preset/src/assets/normalizeStyleRules.ts
index 8247a34..1667a8f 100644
--- a/packages/babel-preset/src/assets/normalizeStyleRules.ts
+++ b/packages/babel-preset/src/assets/normalizeStyleRules.ts
@@ -1,4 +1,4 @@
-import { GriffelStyle } from '@griffel/core';
+import { GriffelAnimation, GriffelStyle } from '@griffel/core';
 import { tokenize } from 'stylis';
 
 import { isAssetUrl } from './isAssetUrl';
@@ -54,9 +54,18 @@ export function normalizeStyleRules(
         return [key, value];
       }
 
-      // Fallback value
+      // Fallback values or keyframes
       if (Array.isArray(value)) {
-        return [key, value.map(rule => normalizeStyleRule(path, projectRoot, filename, rule as string))];
+        return [
+          key,
+          value.map(rule => {
+            if (typeof rule === 'object') {
+              return normalizeStyleRules(path, projectRoot, filename, rule as GriffelAnimation);
+            }
+
+            return normalizeStyleRule(path, projectRoot, filename, rule);
+          }),
+        ];
       }
 
       // Nested objects
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index 4075283..10cdbc6 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -80,6 +80,7 @@ export type {
   // Internal types
   CSSClasses,
   CSSClassesMapBySlot,
+  CSSBucketEntry,
   CSSRulesByBucket,
   StyleBucketName,
   // Util
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index ff9260f..68b3bbd 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -316,6 +316,7 @@ export type CSSRulesByBucket = {
   m?: CSSBucketEntry[];
 };
 
+/** @internal */
 export type CSSBucketEntry = string | [string, Record<string, unknown>];
 
 export type StylesBySlots<Slots extends string | number> = Record<Slots, GriffelStyle>;
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/assets-multiple/output.ts b/packages/webpack-extraction-plugin/__fixtures__/webpack/assets-multiple/output.ts
index 004b02f..51d1b76 100644
--- a/packages/webpack-extraction-plugin/__fixtures__/webpack/assets-multiple/output.ts
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/assets-multiple/output.ts
@@ -5,4 +5,4 @@ export const useStyles = __css({
   },
 });
 
-import 'griffel.css!=!../../../virtual-loader/index.js!../../../virtual-loader/griffel.css?style=.fp00rh9%7Bbackground-image%3Aurl(..%2F__fixtures__%2Fwebpack%2Fassets-multiple%2Fblank.jpg)%2Curl(..%2F__fixtures__%2Fwebpack%2Fassets-multiple%2Fempty.jpg)%3B%7D';
+import 'griffel.css!=!../../../virtual-loader/index.js!../../../virtual-loader/griffel.css?style=%2F**%20%40griffel%3Acss-start%20%5Bd%5D%20**%2F%0A.fp00rh9%7Bbackground-image%3Aurl(..%2F__fixtures__%2Fwebpack%2Fassets-multiple%2Fblank.jpg)%2Curl(..%2F__fixtures__%2Fwebpack%2Fassets-multiple%2Fempty.jpg)%3B%7D%0A%2F**%20%40griffel%3Acss-end%20**%2F%0A';
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/assets/output.ts b/packages/webpack-extraction-plugin/__fixtures__/webpack/assets/output.ts
index 49c5175..df00930 100644
--- a/packages/webpack-extraction-plugin/__fixtures__/webpack/assets/output.ts
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/assets/output.ts
@@ -5,4 +5,4 @@ export const useStyles = __css({
   },
 });
 
-import 'griffel.css!=!../../../virtual-loader/index.js!../../../virtual-loader/griffel.css?style=.fnwsaxv%7Bbackground-image%3Aurl(..%2F__fixtures__%2Fwebpack%2Fassets%2Fblank.jpg)%3B%7D';
+import 'griffel.css!=!../../../virtual-loader/index.js!../../../virtual-loader/griffel.css?style=%2F**%20%40griffel%3Acss-start%20%5Bd%5D%20**%2F%0A.fnwsaxv%7Bbackground-image%3Aurl(..%2F__fixtures__%2Fwebpack%2Fassets%2Fblank.jpg)%3B%7D%0A%2F**%20%40griffel%3Acss-end%20**%2F%0A';
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/basic-rules/output.ts b/packages/webpack-extraction-plugin/__fixtures__/webpack/basic-rules/output.ts
index 3a5eb05..56ad088 100644
--- a/packages/webpack-extraction-plugin/__fixtures__/webpack/basic-rules/output.ts
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/basic-rules/output.ts
@@ -10,4 +10,4 @@ export const styles = __css({
   },
 });
 
-import 'griffel.css!=!../../../virtual-loader/index.js!../../../virtual-loader/griffel.css?style=.fe3e8s9%7Bcolor%3Ared%3B%7D%0A.fycuoez%7Bpadding-left%3A4px%3B%7D%0A.f8wuabp%7Bpadding-right%3A4px%3B%7D%0A.fcnqdeg%7Bbackground-color%3Agreen%3B%7D';
+import 'griffel.css!=!../../../virtual-loader/index.js!../../../virtual-loader/griffel.css?style=%2F**%20%40griffel%3Acss-start%20%5Bd%5D%20**%2F%0A.fe3e8s9%7Bcolor%3Ared%3B%7D.fycuoez%7Bpadding-left%3A4px%3B%7D.f8wuabp%7Bpadding-right%3A4px%3B%7D.fcnqdeg%7Bbackground-color%3Agreen%3B%7D%0A%2F**%20%40griffel%3Acss-end%20**%2F%0A';
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/config-name/fs.json b/packages/webpack-extraction-plugin/__fixtures__/webpack/config-name/fs.json
index 7a3911c..4086f0a 100644
--- a/packages/webpack-extraction-plugin/__fixtures__/webpack/config-name/fs.json
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/config-name/fs.json
@@ -1,1 +1,1 @@
-["bundle.js", "griffel.e44646c584b8724e9528.css"]
+["bundle.js", "griffel.3f25ad2d330c015ca6fa.css"]
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/config-name/output.css b/packages/webpack-extraction-plugin/__fixtures__/webpack/config-name/output.css
index 6948fb7..c4122ec 100644
--- a/packages/webpack-extraction-plugin/__fixtures__/webpack/config-name/output.css
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/config-name/output.css
@@ -1,4 +1,4 @@
-/** griffel.e44646c584b8724e9528.css **/
+/** griffel.3f25ad2d330c015ca6fa.css **/
 .fe3e8s9 {
   color: red;
 }
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/mixed/output.ts b/packages/webpack-extraction-plugin/__fixtures__/webpack/mixed/output.ts
index 79dc01f..b670831 100644
--- a/packages/webpack-extraction-plugin/__fixtures__/webpack/mixed/output.ts
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/mixed/output.ts
@@ -6,4 +6,4 @@ export const useClasses = __css({
 });
 export const useClassName = __resetCSS('rjefjbm', 'r7z97ji');
 
-import 'griffel.css!=!../../../virtual-loader/index.js!../../../virtual-loader/griffel.css?style=.fe3e8s9%7Bcolor%3Ared%3B%7D%0A.rjefjbm%7Bcolor%3Ared%3Bpadding-left%3A4px%3B%7D%0A.r7z97ji%7Bcolor%3Ared%3Bpadding-right%3A4px%3B%7D';
+import 'griffel.css!=!../../../virtual-loader/index.js!../../../virtual-loader/griffel.css?style=%2F**%20%40griffel%3Acss-start%20%5Bd%5D%20**%2F%0A.fe3e8s9%7Bcolor%3Ared%3B%7D%0A%2F**%20%40griffel%3Acss-end%20**%2F%0A%2F**%20%40griffel%3Acss-start%20%5Br%5D%20**%2F%0A.rjefjbm%7Bcolor%3Ared%3Bpadding-left%3A4px%3B%7D.r7z97ji%7Bcolor%3Ared%3Bpadding-right%3A4px%3B%7D%0A%2F**%20%40griffel%3Acss-end%20**%2F%0A';
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/multiple/output.css b/packages/webpack-extraction-plugin/__fixtures__/webpack/multiple/output.css
index 8325d4d..a4f3fe2 100644
--- a/packages/webpack-extraction-plugin/__fixtures__/webpack/multiple/output.css
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/multiple/output.css
@@ -1,7 +1,7 @@
 /** griffel.css **/
-.fe3e8s9 {
-  color: red;
-}
 .fcnqdeg {
   background-color: green;
 }
+.fe3e8s9 {
+  color: red;
+}
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/multiple/output.ts b/packages/webpack-extraction-plugin/__fixtures__/webpack/multiple/output.ts
index 42aeb0d..5450176 100644
--- a/packages/webpack-extraction-plugin/__fixtures__/webpack/multiple/output.ts
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/multiple/output.ts
@@ -10,4 +10,4 @@ export const stylesB = __css({
   },
 });
 
-import 'griffel.css!=!../../../virtual-loader/index.js!../../../virtual-loader/griffel.css?style=.fe3e8s9%7Bcolor%3Ared%3B%7D%0A.fcnqdeg%7Bbackground-color%3Agreen%3B%7D';
+import 'griffel.css!=!../../../virtual-loader/index.js!../../../virtual-loader/griffel.css?style=%2F**%20%40griffel%3Acss-start%20%5Bd%5D%20**%2F%0A.fcnqdeg%7Bbackground-color%3Agreen%3B%7D.fe3e8s9%7Bcolor%3Ared%3B%7D%0A%2F**%20%40griffel%3Acss-end%20**%2F%0A';
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/reset-assets/output.ts b/packages/webpack-extraction-plugin/__fixtures__/webpack/reset-assets/output.ts
index d8a01b1..7283d9e 100644
--- a/packages/webpack-extraction-plugin/__fixtures__/webpack/reset-assets/output.ts
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/reset-assets/output.ts
@@ -1,4 +1,4 @@
 import { __resetStyles, __resetCSS } from '@griffel/react';
 export const useClassName = __resetCSS('ra9m047', null);
 
-import 'griffel.css!=!../../../virtual-loader/index.js!../../../virtual-loader/griffel.css?style=.ra9m047%7Bbackground-image%3Aurl(..%2F__fixtures__%2Fwebpack%2Freset-assets%2Fblank.jpg)%3B%7D';
+import 'griffel.css!=!../../../virtual-loader/index.js!../../../virtual-loader/griffel.css?style=%2F**%20%40griffel%3Acss-start%20%5Br%5D%20**%2F%0A.ra9m047%7Bbackground-image%3Aurl(..%2F__fixtures__%2Fwebpack%2Freset-assets%2Fblank.jpg)%3B%7D%0A%2F**%20%40griffel%3Acss-end%20**%2F%0A';
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/reset/output.ts b/packages/webpack-extraction-plugin/__fixtures__/webpack/reset/output.ts
index dc0f79a..9380ff8 100644
--- a/packages/webpack-extraction-plugin/__fixtures__/webpack/reset/output.ts
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/reset/output.ts
@@ -1,4 +1,4 @@
 import { __resetStyles, __resetCSS } from '@griffel/react';
 export const useClassName = __resetCSS('rjefjbm', 'r7z97ji');
 
-import 'griffel.css!=!../../../virtual-loader/index.js!../../../virtual-loader/griffel.css?style=.rjefjbm%7Bcolor%3Ared%3Bpadding-left%3A4px%3B%7D%0A.r7z97ji%7Bcolor%3Ared%3Bpadding-right%3A4px%3B%7D';
+import 'griffel.css!=!../../../virtual-loader/index.js!../../../virtual-loader/griffel.css?style=%2F**%20%40griffel%3Acss-start%20%5Br%5D%20**%2F%0A.rjefjbm%7Bcolor%3Ared%3Bpadding-left%3A4px%3B%7D.r7z97ji%7Bcolor%3Ared%3Bpadding-right%3A4px%3B%7D%0A%2F**%20%40griffel%3Acss-end%20**%2F%0A';
diff --git a/packages/webpack-extraction-plugin/src/GriffelCSSExtractionPlugin.ts b/packages/webpack-extraction-plugin/src/GriffelCSSExtractionPlugin.ts
index a9de0f3..081d6f9 100644
--- a/packages/webpack-extraction-plugin/src/GriffelCSSExtractionPlugin.ts
+++ b/packages/webpack-extraction-plugin/src/GriffelCSSExtractionPlugin.ts
@@ -3,6 +3,7 @@ import { Compilation } from 'webpack';
 import type { Compiler, sources } from 'webpack';
 
 import { sortCSSRules } from './sortCSSRules';
+import { parseCSSRules } from './parseCSSRules';
 
 export type GriffelCSSExtractionPluginOptions = {
   compareMediaQueries?: GriffelRenderer['compareMediaQueries'];
@@ -62,10 +63,10 @@ export class GriffelCSSExtractionPlugin {
 
           const [assetName, assetSource] = griffelAsset;
 
-          const unsortedCSSRules = getAssetSourceContents(assetSource);
-          const sortedCSSRules = sortCSSRules(unsortedCSSRules, this.compareMediaQueries);
+          const { cssRulesByBucket } = parseCSSRules(getAssetSourceContents(assetSource));
+          const cssRules = sortCSSRules([cssRulesByBucket], this.compareMediaQueries);
 
-          compilation.updateAsset(assetName, new compiler.webpack.sources.RawSource(sortedCSSRules));
+          compilation.updateAsset(assetName, new compiler.webpack.sources.RawSource(cssRules));
         },
       );
     });
diff --git a/packages/webpack-extraction-plugin/src/babelPluginStripGriffelRuntime.ts b/packages/webpack-extraction-plugin/src/babelPluginStripGriffelRuntime.ts
index 5a9ab9c..a2a8240 100644
--- a/packages/webpack-extraction-plugin/src/babelPluginStripGriffelRuntime.ts
+++ b/packages/webpack-extraction-plugin/src/babelPluginStripGriffelRuntime.ts
@@ -1,6 +1,6 @@
 import { NodePath, PluginObj, PluginPass, types as t } from '@babel/core';
 import { declare } from '@babel/helper-plugin-utils';
-import { CSSRulesByBucket, normalizeCSSBucketEntry } from '@griffel/core';
+import { CSSRulesByBucket } from '@griffel/core';
 import * as path from 'path';
 
 type StripRuntimeBabelPluginOptions = {
@@ -11,11 +11,11 @@ type StripRuntimeBabelPluginOptions = {
 type FunctionKinds = '__styles' | '__resetStyles';
 
 type StripRuntimeBabelPluginState = PluginPass & {
-  cssRules?: string[];
+  cssRulesByBucket?: CSSRulesByBucket;
 };
 
 export type StripRuntimeBabelPluginMetadata = {
-  cssRules: string[];
+  cssRulesByBucket: CSSRulesByBucket;
 };
 
 export function transformUrl(filename: string, resourceDirectory: string, assetPath: string) {
@@ -39,10 +39,10 @@ export const babelPluginStripGriffelRuntime = declare<
   return {
     name: '@griffel/webpack-extraction-plugin/babel',
     pre() {
-      this.cssRules = [];
+      this.cssRulesByBucket = {};
     },
     post() {
-      (this.file.metadata as unknown as StripRuntimeBabelPluginMetadata).cssRules = this.cssRules!;
+      (this.file.metadata as unknown as StripRuntimeBabelPluginMetadata).cssRulesByBucket = this.cssRulesByBucket!;
     },
 
     visitor: {
@@ -215,19 +215,15 @@ export const babelPluginStripGriffelRuntime = declare<
               if (functionKind === '__styles') {
                 const cssRulesByBucket = evaluationResult.value as CSSRulesByBucket;
 
-                Object.values(cssRulesByBucket).forEach(cssBucketEntries => {
-                  const cssRules = cssBucketEntries.map(cssBucketEntry => {
-                    const [cssRule] = normalizeCSSBucketEntry(cssBucketEntry);
-
-                    return cssRule;
-                  });
-
-                  state.cssRules!.push(...cssRules);
+                Object.entries(cssRulesByBucket).forEach(([cssBucketName, cssBucketEntries]) => {
+                  state.cssRulesByBucket![cssBucketName as keyof CSSRulesByBucket] = cssBucketEntries.concat(
+                    state.cssRulesByBucket![cssBucketName as keyof CSSRulesByBucket] || [],
+                  );
                 });
               } else if (functionKind === '__resetStyles') {
-                const cssRules = evaluationResult.value as string[];
+                const cssRules = evaluationResult.value as NonNullable<CSSRulesByBucket['r']>;
 
-                state.cssRules!.push(...cssRules);
+                state.cssRulesByBucket!.r = cssRules.concat(state.cssRulesByBucket!.r || []);
               }
 
               argumentPath.remove();
diff --git a/packages/webpack-extraction-plugin/src/parseCSSRules.ts b/packages/webpack-extraction-plugin/src/parseCSSRules.ts
new file mode 100644
index 0000000..fa07e96
--- /dev/null
+++ b/packages/webpack-extraction-plugin/src/parseCSSRules.ts
@@ -0,0 +1,50 @@
+import { styleBucketOrdering } from '@griffel/core';
+import { COMMENT, compile, MEDIA, serialize, stringify } from 'stylis';
+
+import type { CSSBucketEntry, CSSRulesByBucket, StyleBucketName } from '@griffel/core';
+
+export function parseCSSRules(css: string) {
+  const cssRulesByBucket = styleBucketOrdering.reduce<CSSRulesByBucket>((acc, styleBucketName) => {
+    acc[styleBucketName] = [];
+
+    return acc;
+  }, {}) as Required<CSSRulesByBucket>;
+  const elements = compile(css);
+  const unrelatedElements: ReturnType<typeof compile> = [];
+
+  let cssBucketName: StyleBucketName | null = null;
+  let cssMeta: Record<string, unknown> | null = null;
+
+  for (const element of elements) {
+    if (element.type === COMMENT) {
+      if (element.value.indexOf('/** @griffel:css-start') === 0) {
+        cssBucketName = element.value.charAt(24) as StyleBucketName;
+
+        if (cssBucketName === 'm') {
+          cssMeta = JSON.parse(element.value.slice(28, -5));
+        }
+
+        continue;
+      }
+
+      if (element.value.indexOf('/** @griffel:css-end') === 0) {
+        cssBucketName = null;
+        cssMeta = null;
+
+        continue;
+      }
+    }
+
+    if (cssBucketName) {
+      const cssRule = serialize([element], stringify);
+      const bucketEntry: CSSBucketEntry = element.type === MEDIA ? [cssRule, cssMeta!] : cssRule;
+
+      cssRulesByBucket[cssBucketName].push(bucketEntry);
+      continue;
+    }
+
+    unrelatedElements.push(element);
+  }
+
+  return { cssRulesByBucket, remainingCSS: serialize(unrelatedElements, stringify) };
+}
diff --git a/packages/webpack-extraction-plugin/src/sortCSSRules.ts b/packages/webpack-extraction-plugin/src/sortCSSRules.ts
index cd11a6d..a990f0d 100644
--- a/packages/webpack-extraction-plugin/src/sortCSSRules.ts
+++ b/packages/webpack-extraction-plugin/src/sortCSSRules.ts
@@ -1,99 +1,58 @@
-import {
-  RESET_HASH_PREFIX,
-  getStyleBucketName,
-  GriffelRenderer,
-  StyleBucketName,
-  styleBucketOrdering,
-} from '@griffel/core';
-import { COMMENT, compile, Element, KEYFRAMES, MEDIA, RULESET, serialize, stringify, SUPPORTS, tokenize } from 'stylis';
+import { styleBucketOrdering } from '@griffel/core';
+import type { GriffelRenderer, CSSBucketEntry, CSSRulesByBucket } from '@griffel/core';
 
-function isResetClassName(className: string) {
-  return className[0] === '.' && className[1] === RESET_HASH_PREFIX;
+function getCSSRuleFromBucketEntry(entry: CSSBucketEntry): string {
+  return Array.isArray(entry) ? entry[0] : entry;
 }
 
-export function getSelectorFromElement(element: Element) {
-  return tokenize(element.value).slice(1).join('');
+function getCSSMetaFromBucketEntry(entry: CSSBucketEntry): Record<string, unknown> {
+  return Array.isArray(entry) ? entry[1] : {};
 }
 
-export function getElementMetadata(element: Element): string {
-  if (element.type === MEDIA) {
-    return element.value.replace(/^@media/, '').trim();
-  }
-
-  return '';
-}
-
-export function getElementReference(element: Element, suffix = ''): string {
-  if (element.type === RULESET || element.type === KEYFRAMES) {
-    return element.value + suffix;
-  }
-
-  if (Array.isArray(element.children)) {
-    return element.value + '[' + element.children.map(child => getElementReference(child, suffix)).join(',') + ']';
-  }
-
-  function removeRootProperty(element: Element): unknown {
-    return {
-      ...element,
-      children: Array.isArray(element.children)
-        ? element.children.map(child => removeRootProperty(child))
-        : element.children,
-      root: undefined,
-      parent: undefined,
-    };
-  }
-
-  throw new Error(
-    [
-      'getElementReference(): An unhandled case, please report if it happens and provide debug information about an element:',
-      JSON.stringify(removeRootProperty(element), null, 2),
-    ].join('\n'),
-  );
-}
-
-export function getStyleBucketNameFromElement(element: Element): StyleBucketName {
-  if (element.type === KEYFRAMES) {
-    return 'k';
-  }
-
-  if (isResetClassName(element.value)) {
-    return 'r';
-  }
-
-  return getStyleBucketName(
-    [getSelectorFromElement(element)],
-    element.type === '@layer' ? element.value : '',
-    element.type === MEDIA ? element.value : '',
-    element.type === SUPPORTS ? element.value : '',
-  );
-}
-
-export function sortCSSRules(css: string, compareMediaQueries: GriffelRenderer['compareMediaQueries']): string {
-  const childElements = compile(css)
-    // Remove top level comments as it is unclear how to sort them
-    .filter(element => element.type !== COMMENT)
-    .map(element => ({
-      ...element,
-      bucketName: getStyleBucketNameFromElement(element),
-      metadata: getElementMetadata(element),
-      reference: getElementReference(element),
-    }));
-  const uniqueElements = childElements.reduce<Record<string, typeof childElements[0]>>((acc, element) => {
-    acc[element.reference] = element;
-
-    return acc;
-  }, {});
-  const sortedElements = Object.values(uniqueElements).sort((elementA, elementB) => {
-    if (elementA.bucketName === 'm' && elementB.bucketName === 'm') {
-      return compareMediaQueries(elementA.metadata, elementB.metadata);
-    }
-
-    if (elementA.bucketName === elementB.bucketName) {
-      return 0;
-    }
-
-    return styleBucketOrdering.indexOf(elementA.bucketName) - styleBucketOrdering.indexOf(elementB.bucketName);
-  });
-
-  return serialize(sortedElements, stringify);
+export function sortCSSRules(
+  setOfCSSRules: CSSRulesByBucket[],
+  compareMediaQueries: GriffelRenderer['compareMediaQueries'],
+) {
+  return styleBucketOrdering
+    .map(styleBucketName => {
+      return {
+        styleBucketName,
+        cssBucketEntries:
+          // We deduplicate CSS rules there by using them as keys in an object:
+          // - create an array with pairs [key, value]
+          // - use Object.fromEntries() to create an object that contains unique values
+          Object.values(
+            Object.fromEntries(
+              setOfCSSRules.flatMap(cssRulesByBucket => {
+                if (Array.isArray(cssRulesByBucket[styleBucketName])) {
+                  return cssRulesByBucket[styleBucketName]!.map(bucketEntry => [
+                    getCSSRuleFromBucketEntry(bucketEntry),
+                    bucketEntry,
+                  ]);
+                }
+
+                return [];
+              }),
+            ),
+          ),
+      };
+    })
+    .reduce((acc, { styleBucketName, cssBucketEntries }) => {
+      if (styleBucketName === 'm') {
+        return (
+          acc +
+          cssBucketEntries
+            .sort((entryA, entryB) => {
+              return compareMediaQueries(
+                getCSSMetaFromBucketEntry(entryA)['m'] as string,
+                getCSSMetaFromBucketEntry(entryB)['m'] as string,
+              );
+            })
+            .map(entry => entry[0])
+            .join('')
+        );
+      }
+
+      return acc + cssBucketEntries.join('');
+    }, '');
 }
diff --git a/packages/webpack-extraction-plugin/src/transformSync.ts b/packages/webpack-extraction-plugin/src/transformSync.ts
index f2183e3..7daf4f7 100644
--- a/packages/webpack-extraction-plugin/src/transformSync.ts
+++ b/packages/webpack-extraction-plugin/src/transformSync.ts
@@ -1,4 +1,6 @@
 import * as Babel from '@babel/core';
+import type { CSSRulesByBucket } from '@griffel/core';
+
 import { babelPluginStripGriffelRuntime, StripRuntimeBabelPluginMetadata } from './babelPluginStripGriffelRuntime';
 
 export type TransformOptions = {
@@ -11,7 +13,7 @@ export type TransformOptions = {
 
 export type TransformResult = {
   code: string;
-  cssRules: string[] | undefined;
+  cssRulesByBucket: CSSRulesByBucket | undefined;
   sourceMap: NonNullable<Babel.BabelFileResult['map']> | undefined;
 };
 
@@ -52,7 +54,7 @@ export function transformSync(sourceCode: string, options: TransformOptions): Tr
 
   return {
     code: babelFileResult.code as string,
-    cssRules: (babelFileResult.metadata as unknown as StripRuntimeBabelPluginMetadata).cssRules,
+    cssRulesByBucket: (babelFileResult.metadata as unknown as StripRuntimeBabelPluginMetadata).cssRulesByBucket,
     sourceMap: babelFileResult.map === null ? undefined : babelFileResult.map,
   };
 }
diff --git a/packages/webpack-extraction-plugin/src/webpackLoader.ts b/packages/webpack-extraction-plugin/src/webpackLoader.ts
index 7ee4918..73600ef 100644
--- a/packages/webpack-extraction-plugin/src/webpackLoader.ts
+++ b/packages/webpack-extraction-plugin/src/webpackLoader.ts
@@ -1,3 +1,4 @@
+import { normalizeCSSBucketEntry } from '@griffel/core';
 import { getOptions } from 'loader-utils';
 import * as path from 'path';
 import { validate } from 'schema-utils';
@@ -19,10 +20,6 @@ function toURIComponent(rule: string): string {
   return encodeURIComponent(rule).replace(/!/g, '%21');
 }
 
-function shouldTransformSourceCode(sourceCode: string): boolean {
-  return sourceCode.indexOf('__styles') !== -1 || sourceCode.indexOf('__resetStyles') !== -1;
-}
-
 /**
  * Webpack can also pass sourcemaps as a string, Babel accepts only objects.
  * See https://github.com/babel/babel-loader/pull/889.
@@ -57,7 +54,7 @@ function webpackLoader(
   });
 
   // Early return to handle cases when __styles() calls are not present, allows skipping expensive invocation of Babel
-  if (!shouldTransformSourceCode(sourceCode)) {
+  if (sourceCode.indexOf('__styles') === -1 && sourceCode.indexOf('__resetStyles') === -1) {
     this.callback(null, sourceCode, inputSourceMap);
     return;
   }
@@ -78,11 +75,39 @@ function webpackLoader(
   }
 
   if (result) {
-    if (result.cssRules) {
+    if (result.cssRulesByBucket) {
+      const css = Object.entries(result.cssRulesByBucket).reduce((acc, [cssBucketName, cssBucketRules]) => {
+        if (cssBucketName === 'm') {
+          return (
+            acc +
+            cssBucketRules
+              .map(entry => {
+                return [
+                  `/** @griffel:css-start [${cssBucketName}] [${JSON.stringify(entry[1])}] **/`,
+                  normalizeCSSBucketEntry(entry)[0],
+                  `/** @griffel:css-end **/`,
+                  '',
+                ].join('\n');
+              })
+              .join('')
+          );
+        }
+
+        return (
+          acc +
+          [
+            `/** @griffel:css-start [${cssBucketName}] **/`,
+            cssBucketRules.flatMap(entry => normalizeCSSBucketEntry(entry)).join(''),
+            `/** @griffel:css-end **/`,
+            '',
+          ].join('\n')
+        );
+      }, '');
+
       const request = `import ${JSON.stringify(
         this.utils.contextify(
           this.context || this.rootContext,
-          `griffel.css!=!${virtualLoaderPath}!${resourcePath}?style=${toURIComponent(result.cssRules.join('\n'))}`,
+          `griffel.css!=!${virtualLoaderPath}!${resourcePath}?style=${toURIComponent(css)}`,
         ),
       )};`;
 


Test patch:
diff --git a/packages/babel-preset/src/assets/normalizeStyleRules.test.ts b/packages/babel-preset/src/assets/normalizeStyleRules.test.ts
index 11c215a..6076f61 100644
--- a/packages/babel-preset/src/assets/normalizeStyleRules.test.ts
+++ b/packages/babel-preset/src/assets/normalizeStyleRules.test.ts
@@ -128,4 +128,24 @@ describe('normalizeStyleRules', () => {
       },
     });
   });
+
+  it('handles keyframe arrays', () => {
+    expect(
+      normalizeStyleRules(
+        path.posix,
+        '/home/projects/foo',
+        '/home/projects/foo/src/styles/Component.styles.ts',
+
+        {
+          root: {
+            animationName: [{ from: { height: '20px' }, to: { height: '10px' } }],
+          },
+        },
+      ),
+    ).toEqual({
+      root: {
+        animationName: [{ from: { height: '20px' }, to: { height: '10px' } }],
+      },
+    });
+  });
 });
diff --git a/packages/babel-preset/src/transformPlugin.test.ts b/packages/babel-preset/src/transformPlugin.test.ts
index 5877d25..64c0c2b 100644
--- a/packages/babel-preset/src/transformPlugin.test.ts
+++ b/packages/babel-preset/src/transformPlugin.test.ts
@@ -53,6 +53,11 @@ pluginTester({
       fixture: path.resolve(fixturesDir, 'non-existing-module-call', 'code.ts'),
       outputFixture: path.resolve(fixturesDir, 'non-existing-module-call', 'output.ts'),
     },
+    {
+      title: 'syntax: animationName',
+      fixture: path.resolve(fixturesDir, 'keyframes', 'code.ts'),
+      outputFixture: path.resolve(fixturesDir, 'keyframes', 'output.ts'),
+    },
 
     // Assets
     //
diff --git a/packages/webpack-extraction-plugin/src/parseCSSRules.test.ts b/packages/webpack-extraction-plugin/src/parseCSSRules.test.ts
new file mode 100644
index 0000000..aa1ec1e
--- /dev/null
+++ b/packages/webpack-extraction-plugin/src/parseCSSRules.test.ts
@@ -0,0 +1,91 @@
+import { parseCSSRules } from './parseCSSRules';
+import { CSSRulesByBucket } from '@griffel/core';
+
+function removeEmptyBuckets(cssRulesByBucket: CSSRulesByBucket) {
+  return Object.fromEntries(Object.entries(cssRulesByBucket).filter(([, bucketEntries]) => bucketEntries.length > 0));
+}
+
+describe('parseCSSRules', () => {
+  it('handles regular rules', () => {
+    const css = `
+    /** @griffel:css-start [d] **/
+    .fe3e8s9 { color: red; }
+    /** @griffel:css-end **/
+    /** @griffel:css-start [h] **/
+    .faf35ka:hover { color: red; }
+    /** @griffel:css-end **/
+  `;
+    const { cssRulesByBucket, remainingCSS } = parseCSSRules(css);
+
+    expect(removeEmptyBuckets(cssRulesByBucket)).toMatchInlineSnapshot(`
+      Object {
+        "d": Array [
+          ".fe3e8s9{color:red;}",
+        ],
+        "h": Array [
+          ".faf35ka:hover{color:red;}",
+        ],
+      }
+    `);
+    expect(remainingCSS).toBe('');
+  });
+
+  it('handles rules with meta', () => {
+    const css = `
+    /** @griffel:css-start [d] **/
+    .fe3e8s9 { color: red; }
+    /** @griffel:css-end **/
+    /** @griffel:css-start [m] [{"m":"screen and (max-width: 100px)"}] **/
+    @media screen and (max-width: 100px) { .fr5o61b{ color:red; } }
+    /** @griffel:css-end **/
+    /** @griffel:css-start [m] [{"m":"screen and (max-width: 100px)"}] **/
+    @media screen and (max-width: 100px) { .f1j0ers2 { display: grid; } }
+    /** @griffel:css-end **/
+  `;
+    const { cssRulesByBucket, remainingCSS } = parseCSSRules(css);
+
+    expect(removeEmptyBuckets(cssRulesByBucket)).toMatchInlineSnapshot(`
+      Object {
+        "d": Array [
+          ".fe3e8s9{color:red;}",
+        ],
+        "m": Array [
+          Array [
+            "@media screen and (max-width: 100px){.fr5o61b{color:red;}}",
+            Object {
+              "m": "screen and (max-width: 100px)",
+            },
+          ],
+          Array [
+            "@media screen and (max-width: 100px){.f1j0ers2{display:grid;}}",
+            Object {
+              "m": "screen and (max-width: 100px)",
+            },
+          ],
+        ],
+      }
+    `);
+    expect(remainingCSS).toBe('');
+  });
+
+  it('keeps third parties CSS', () => {
+    const css = `
+    /** @griffel:css-start [d] **/
+    .fe3e8s9 { color: red; }
+    /** @griffel:css-end **/
+    .foo { color: red }
+    /* some comment */
+    .bar { color: green }
+  `;
+    const { cssRulesByBucket, remainingCSS } = parseCSSRules(css);
+
+    expect(removeEmptyBuckets(cssRulesByBucket)).toMatchInlineSnapshot(`
+      Object {
+        "d": Array [
+          ".fe3e8s9{color:red;}",
+        ],
+      }
+    `);
+    expect(remainingCSS).toBe('.foo{color:red;}.bar{color:green;}');
+  });
+});
diff --git a/packages/webpack-extraction-plugin/src/sortCSSRules.test.ts b/packages/webpack-extraction-plugin/src/sortCSSRules.test.ts
index 2760007..5c4c100 100644
--- a/packages/webpack-extraction-plugin/src/sortCSSRules.test.ts
+++ b/packages/webpack-extraction-plugin/src/sortCSSRules.test.ts
@@ -1,8 +1,7 @@
-import { GriffelRenderer } from '@griffel/core';
+import { CSSRulesByBucket, GriffelRenderer } from '@griffel/core';
 import * as prettier from 'prettier';
-import { compile } from 'stylis';
 
-import { getElementReference, getElementMetadata, sortCSSRules } from './sortCSSRules';
+import { sortCSSRules } from './sortCSSRules';
 
 export const cssSerializer: jest.SnapshotSerializerPlugin = {
   test(value) {
@@ -20,47 +19,18 @@ export const cssSerializer: jest.SnapshotSerializerPlugin = {
 
 expect.addSnapshotSerializer(cssSerializer);
 
-describe('getElementReference', () => {
-  it.each`
-    css                                                                                              | reference
-    ${'.foo { color: red; }'}                                                                        | ${'.foo'}
-    ${'.foo:hover { color: red; }'}                                                                  | ${'.foo:hover'}
-    ${'@media (max-width: 2px) { .foo { color: blue; } }'}                                           | ${'@media (max-width: 2px)[.foo]'}
-    ${'@keyframes foo { from { transform:rotate(0deg); } to { transform:rotate(360deg); } }'}        | ${'@keyframes foo'}
-    ${'@media screen and (max-width: 992px) { .a { text-align: left; } .b { text-align: right; } }'} | ${'@media screen and (max-width: 992px)[.a,.b]'}
-    ${'@media (max-width: 2px) { @supports (display: grid) { .a { color: blue; } } }'}               | ${'@media (max-width: 2px)[@supports (display: grid)[.a]]'}
-  `('returns "$reference" for "$css"', ({ css, reference }: { css: string; reference: string }) => {
-    const element = compile(css)[0];
-
-    expect(getElementReference(element)).toBe(reference);
-  });
-});
-
-describe('getElementMetadata', () => {
-  it.each`
-    css                                                    | metadata
-    ${'.foo { color: red; }'}                              | ${''}
-    ${'.foo:hover { color: red; }'}                        | ${''}
-    ${'@media (max-width: 2px) { .foo { color: blue; } }'} | ${'(max-width: 2px)'}
-  `('returns "$reference" for "$css"', ({ css, metadata }: { css: string; metadata: string }) => {
-    const element = compile(css)[0];
-
-    expect(getElementMetadata(element)).toBe(metadata);
-  });
-});
-
 describe('sortCSSRules', () => {
   it('removes duplicate rules', () => {
-    const css = `
-      .baz { color: orange; }
-      .foo { color: red; }
-      .baz { color: orange; }
-
-      @media (max-width: 2px) { .foo { color: blue; } }
-      @media (max-width: 2px) { .yellow { color: blue; } }
-    `;
-
-    expect(sortCSSRules(css, () => 0)).toMatchInlineSnapshot(`
+    const setA: CSSRulesByBucket = {
+      d: ['.baz { color: orange; }', '.foo { color: red; }'],
+      m: [['@media (max-width: 2px) { .foo { color: blue; } }', { m: '(max-width: 2px)' }]],
+    };
+    const setB: CSSRulesByBucket = {
+      d: ['.baz { color: orange; }'],
+      m: [['@media (max-width: 2px) { .yellow { color: blue; } }', { m: '(max-width: 2px)' }]],
+    };
+
+    expect(sortCSSRules([setA, setB], () => 0)).toMatchInlineSnapshot(`
       .baz {
         color: orange;
       }
@@ -81,14 +51,16 @@ describe('sortCSSRules', () => {
   });
 
   it('sorts rules by buckets order', () => {
-    const css = `
-      .foo:focus { color: pink; }
-      .baz { color: orange; }
-      .foo:hover { color: yellow; }
-      .foo { color: red; }
-    `;
-
-    expect(sortCSSRules(css, () => 0)).toMatchInlineSnapshot(`
+    const setA: CSSRulesByBucket = {
+      d: ['.baz { color: orange; }'],
+      f: ['.foo:focus { color: pink; }'],
+    };
+    const setB: CSSRulesByBucket = {
+      d: ['.foo { color: red; }'],
+      h: ['.foo:hover { color: yellow; }'],
+    };
+
+    expect(sortCSSRules([setA, setB], () => 0)).toMatchInlineSnapshot(`
       .baz {
         color: orange;
       }
@@ -105,18 +77,22 @@ describe('sortCSSRules', () => {
   });
 
   it('sorts media queries', () => {
-    const css = `
-      @media (max-width: 2px) { .foo { color: blue; } }
-      @media (max-width: 1px) { .foo { color: red; } }
-      @media (max-width: 3px) { .foo { color: red; } }
-      .foo { color: green; }
-    `;
+    const setA: CSSRulesByBucket = {
+      m: [
+        ['@media (max-width: 2px) { .foo { color: blue; } }', { m: '(max-width: 2px)' }],
+        ['@media (max-width: 3px) { .foo { color: red; } }', { m: '(max-width: 3px)' }],
+      ],
+    };
+    const setB: CSSRulesByBucket = {
+      d: ['.foo { color: green; }'],
+      m: [['@media (max-width: 1px) { .foo { color: red; } }', { m: '(max-width: 1px)' }]],
+    };
 
     const mediaQueryOrder = ['(max-width: 1px)', '(max-width: 2px)', '(max-width: 3px)', '(max-width: 4px)'];
     const compareMediaQueries: GriffelRenderer['compareMediaQueries'] = (a: string, b: string) =>
       mediaQueryOrder.indexOf(a) - mediaQueryOrder.indexOf(b);
 
-    expect(sortCSSRules(css, compareMediaQueries)).toMatchInlineSnapshot(`
+    expect(sortCSSRules([setA, setB], compareMediaQueries)).toMatchInlineSnapshot(`
       .foo {
         color: green;
       }
@@ -137,17 +113,4 @@ describe('sortCSSRules', () => {
       }
     `);
   });
-
-  it('removes comments from output', () => {
-    const css = `
-      /* This is a comment in CSS */
-      .baz { color: orange; }
-    `;
-
-    expect(sortCSSRules(css, () => 0)).toMatchInlineSnapshot(`
-      .baz {
-        color: orange;
-      }
-    `);
-  });
 });