Repository: microsoft/griffel
Pull request #137 (2022-06-16 07:08:57)

feat: Create Isomorphic stylesheet for style renderer
The isomorphic stylesheet is intended mainly for SSR to simplify
element rendering and make current and future (i.e. media queries) insertion more straightforward.

The SSR `renderToStyleElement` function no longer needs to know about insertion cache or specific buckets

github-actions[bot]: ## 📊 Bundle size report

| Package & Exports                                                                                                            |   Baseline (minified/GZIP) |                          PR |                                                                                                                                                                                                   Change |
| :--------------------------------------------------------------------------------------------------------------------------- | -------------------------: | --------------------------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
| <samp>react</samp> <br /> <abbr title='bundle-size/makeStaticStyles-runtime.fixture.js'>makeStaticStyles (runtime)</abbr>    | `8.247 kB`<br />`3.595 kB` |   `8.68 kB`<br />`3.784 kB` | `433 B` <img aria-hidden="true" src="https://microsoft.github.io/monosize/images/increase.png" /><br />`189 B` <img aria-hidden="true" src="https://microsoft.github.io/monosize/images/increase.png" /> |
| <samp>react</samp> <br /> <abbr title='bundle-size/makeStyles-runtime.fixture.js'>makeStyles + mergeClasses (runtime)</abbr> | `21.294 kB`<br />`7.98 kB` | `21.725 kB`<br />`8.166 kB` | `431 B` <img aria-hidden="true" src="https://microsoft.github.io/monosize/images/increase.png" /><br />`186 B` <img aria-hidden="true" src="https://microsoft.github.io/monosize/images/increase.png" /> |
| <samp>react</samp> <br /> <abbr title='bundle-size/makeStyles.fixture.js'>makeStyles + mergeClasses (build time)</abbr>      | `2.811 kB`<br />`1.275 kB` |  `3.235 kB`<br />`1.469 kB` | `424 B` <img aria-hidden="true" src="https://microsoft.github.io/monosize/images/increase.png" /><br />`194 B` <img aria-hidden="true" src="https://microsoft.github.io/monosize/images/increase.png" /> |

<details>
<summary>Unchanged fixtures</summary>

| Package & Exports                                                                                                          |        Size (minified/GZIP) |
| -------------------------------------------------------------------------------------------------------------------------- | --------------------------: |
| <samp>core</samp> <br /> <abbr title='bundle-size/makeStyles.fixture.js'>makeStyles + mergeClasses (build time)</abbr>     |       `1.8 kB`<br />`861 B` |
| <samp>core</samp> <br /> <abbr title='bundle-size/makeStylesRuntime.fixture.js'>makeStyles + mergeClasses (runtime)</abbr> | `20.269 kB`<br />`7.549 kB` |

</details>
<sub>🤖 This report was generated against <a href='https://github.com/microsoft/griffel/commit/0de4bd154e9670e2884f3ad67ab9a6a645262382'>0de4bd154e9670e2884f3ad67ab9a6a645262382</a></sub>

<!-- Sticky Pull Request Comment -->

Patch:
diff --git a/apps/benchmark/src/App.tsx b/apps/benchmark/src/App.tsx
index 6eb05d6..a1d192d 100644
--- a/apps/benchmark/src/App.tsx
+++ b/apps/benchmark/src/App.tsx
@@ -22,7 +22,7 @@ interface AppState {
 
 const appInitialState: AppState = {
   benchmark: 'Mount deep tree',
-  library: Object.keys(implementations)[0],
+  library: 'griffel',
   results: [],
   inProgress: false,
 };
diff --git a/apps/website/src/components/Playground/code/app.js b/apps/website/src/components/Playground/code/app.js
index fcd523d..12edae0 100644
--- a/apps/website/src/components/Playground/code/app.js
+++ b/apps/website/src/components/Playground/code/app.js
@@ -23,7 +23,7 @@ export default function App() {
         setRules(prettified);
       },
       insertionCache: {},
-      styleElements: {},
+      stylesheets: {},
     };
 
     styles({
diff --git a/change/@griffel-core-37ff1b21-9fd5-4dc1-8bb9-673f0af5c5f0.json b/change/@griffel-core-37ff1b21-9fd5-4dc1-8bb9-673f0af5c5f0.json
new file mode 100644
index 0000000..78f6128
--- /dev/null
+++ b/change/@griffel-core-37ff1b21-9fd5-4dc1-8bb9-673f0af5c5f0.json
@@ -0,0 +1,7 @@
+{
+  "type": "minor",
+  "comment": "feat: Create isomorphic stylesheet for DOM renderer",
+  "packageName": "@griffel/core",
+  "email": "lingfangao@hotmail.com",
+  "dependentChangeType": "patch"
+}
diff --git a/change/@griffel-react-4dd6ac38-fca5-4652-be5b-6b3ff5709fdb.json b/change/@griffel-react-4dd6ac38-fca5-4652-be5b-6b3ff5709fdb.json
new file mode 100644
index 0000000..021a7a3
--- /dev/null
+++ b/change/@griffel-react-4dd6ac38-fca5-4652-be5b-6b3ff5709fdb.json
@@ -0,0 +1,7 @@
+{
+  "type": "minor",
+  "comment": "feat: Update renderToStyleElement to use isomorphic stylesheets from renderer",
+  "packageName": "@griffel/react",
+  "email": "lingfangao@hotmail.com",
+  "dependentChangeType": "patch"
+}
diff --git a/packages/core/src/common/snapshotSerializers.ts b/packages/core/src/common/snapshotSerializers.ts
index c2ec844..1c9e92e 100644
--- a/packages/core/src/common/snapshotSerializers.ts
+++ b/packages/core/src/common/snapshotSerializers.ts
@@ -10,7 +10,7 @@ const isObject = (value: unknown) => value != null && !Array.isArray(value) && t
 
 export const griffelRendererSerializer: jest.SnapshotSerializerPlugin = {
   test(value) {
-    return isObject(value) && isObject(value.styleElements);
+    return isObject(value) && isObject(value.stylesheets);
   },
   print(value) {
     /**
@@ -18,13 +18,13 @@ export const griffelRendererSerializer: jest.SnapshotSerializerPlugin = {
      */
     const _value = value as GriffelRenderer;
 
-    const styleElementsKeys = Object.keys(_value.styleElements) as (keyof typeof _value['styleElements'])[];
+    const stylesheetKeys = Object.keys(_value.stylesheets) as (keyof typeof _value['stylesheets'])[];
 
-    const rules = styleElementsKeys.reduce((acc, styleEl) => {
-      const styleElement = _value.styleElements[styleEl];
+    const rules = stylesheetKeys.reduce((acc, styleEl) => {
+      const stylesheet = _value.stylesheets[styleEl]?.element;
 
-      if (styleElement) {
-        const cssRules = styleElement.sheet ? Array.from(styleElement.sheet.cssRules) : [];
+      if (stylesheet) {
+        const cssRules = stylesheet.sheet ? Array.from(stylesheet.sheet.cssRules) : [];
 
         return [...acc, ...cssRules.map(rule => rule.cssText)];
       }
diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts
index 2bfd62a..4c49715 100644
--- a/packages/core/src/constants.ts
+++ b/packages/core/src/constants.ts
@@ -1,6 +1,9 @@
 import * as CSS from 'csstype';
 import { LookupItem, SequenceHash } from './types';
 
+/** @internal */
+export const DATA_BUCKET_ATTR = 'data-make-styles-bucket';
+
 /** @internal */
 export const HASH_PREFIX = 'f';
 
diff --git a/packages/core/src/renderer/createDOMRenderer.ts b/packages/core/src/renderer/createDOMRenderer.ts
index 0f36b4d..bbf8b59 100644
--- a/packages/core/src/renderer/createDOMRenderer.ts
+++ b/packages/core/src/renderer/createDOMRenderer.ts
@@ -35,7 +35,7 @@ export function createDOMRenderer(
   const { unstable_filterCSSRule } = options;
   const renderer: GriffelRenderer = {
     insertionCache: {},
-    styleElements: {},
+    stylesheets: {},
 
     id: `d${lastIndex++}`,
 
@@ -43,9 +43,12 @@ export function createDOMRenderer(
       // eslint-disable-next-line guard-for-in
       for (const styleBucketName in cssRules) {
         const cssRulesForBucket = cssRules[styleBucketName as StyleBucketName]!;
-        const sheet =
-          target &&
-          getStyleSheetForBucket(styleBucketName as StyleBucketName, target, renderer, options.styleElementAttributes);
+        const sheet = getStyleSheetForBucket(
+          styleBucketName as StyleBucketName,
+          target,
+          renderer,
+          options.styleElementAttributes,
+        );
 
         // This is a hot path in rendering styles: ".length" is cached in "l" var to avoid accesses the property
         for (let i = 0, l = cssRulesForBucket.length; i < l; i++) {
@@ -60,21 +63,19 @@ export function createDOMRenderer(
             debugData.addCSSRule(ruleCSS);
           }
 
-          if (sheet) {
-            try {
-              if (unstable_filterCSSRule) {
-                if (unstable_filterCSSRule(ruleCSS)) {
-                  sheet.insertRule(ruleCSS, sheet.cssRules.length);
-                }
-              } else {
-                sheet.insertRule(ruleCSS, sheet.cssRules.length);
-              }
-            } catch (e) {
-              // We've disabled these warnings due to false-positive errors with browser prefixes
-              if (process.env.NODE_ENV !== 'production' && !ignoreSuffixesRegex.test(ruleCSS)) {
-                // eslint-disable-next-line no-console
-                console.error(`There was a problem inserting the following rule: "${ruleCSS}"`, e);
+          try {
+            if (unstable_filterCSSRule) {
+              if (unstable_filterCSSRule(ruleCSS)) {
+                sheet.insertRule(ruleCSS);
               }
+            } else {
+              sheet.insertRule(ruleCSS);
+            }
+          } catch (e) {
+            // We've disabled these warnings due to false-positive errors with browser prefixes
+            if (process.env.NODE_ENV !== 'production' && !ignoreSuffixesRegex.test(ruleCSS)) {
+              // eslint-disable-next-line no-console
+              console.error(`There was a problem inserting the following rule: "${ruleCSS}"`, e);
             }
           }
         }
diff --git a/packages/core/src/renderer/createIsomorphicStyleSheet.ts b/packages/core/src/renderer/createIsomorphicStyleSheet.ts
new file mode 100644
index 0000000..56860cf
--- /dev/null
+++ b/packages/core/src/renderer/createIsomorphicStyleSheet.ts
@@ -0,0 +1,53 @@
+import { DATA_BUCKET_ATTR } from '../constants';
+import { IsomorphicStyleSheet, StyleBucketName } from '../types';
+
+export function createIsomorphicStyleSheet(
+  styleElement: HTMLStyleElement | undefined,
+  bucketName: StyleBucketName,
+  elementAttributes: Record<string, string>,
+): IsomorphicStyleSheet {
+  // no CSSStyleSheet in SSR, just append rules here for server render
+  const __cssRulesForSSR: string[] = [];
+
+  elementAttributes[DATA_BUCKET_ATTR] = bucketName;
+  if (styleElement) {
+    for (const attrName in elementAttributes) {
+      styleElement.setAttribute(attrName, elementAttributes[attrName]);
+    }
+  }
+
+  function insertRule(rule: string) {
+    if (styleElement?.sheet) {
+      return styleElement.sheet.insertRule(rule, styleElement.sheet.cssRules.length);
+    }
+
+    return __cssRulesForSSR.push(rule);
+  }
+
+  return {
+    elementAttributes,
+    insertRule,
+    element: styleElement,
+    bucketName,
+    cssRules() {
+      if (styleElement?.sheet) {
+        return Array.from(styleElement.sheet.cssRules).map(cssRule => cssRule.cssText);
+      }
+
+      return __cssRulesForSSR;
+    },
+  };
+}
+
+export function createIsomorphicStyleSheetFromElement(element: HTMLStyleElement) {
+  const elementAttributes = Array.from(element.attributes).reduce((acc, attr) => {
+    acc[attr.name] = attr.value;
+    return acc;
+  }, {} as Record<string, string>);
+  const stylesheet = createIsomorphicStyleSheet(
+    element,
+    elementAttributes[DATA_BUCKET_ATTR] as StyleBucketName,
+    elementAttributes,
+  );
+  return stylesheet;
+}
diff --git a/packages/core/src/renderer/getStyleSheetForBucket.ts b/packages/core/src/renderer/getStyleSheetForBucket.ts
index 9e59742..7d78e98 100644
--- a/packages/core/src/renderer/getStyleSheetForBucket.ts
+++ b/packages/core/src/renderer/getStyleSheetForBucket.ts
@@ -1,4 +1,5 @@
-import { GriffelRenderer, StyleBucketName } from '../types';
+import { GriffelRenderer, IsomorphicStyleSheet, StyleBucketName } from '../types';
+import { createIsomorphicStyleSheet } from './createIsomorphicStyleSheet';
 
 /**
  * Ordered style buckets using their short pseudo name.
@@ -33,34 +34,31 @@ export const styleBucketOrdering: StyleBucketName[] = [
  */
 export function getStyleSheetForBucket(
   bucketName: StyleBucketName,
-  target: Document,
+  target: Document | undefined,
   renderer: GriffelRenderer,
   elementAttributes: Record<string, string> = {},
-): CSSStyleSheet {
-  if (!renderer.styleElements[bucketName]) {
-    let currentBucketIndex = styleBucketOrdering.indexOf(bucketName) + 1;
-    let nextBucketFromCache = null;
+): IsomorphicStyleSheet {
+  if (!renderer.stylesheets[bucketName]) {
+    const tag: HTMLStyleElement | undefined = target && target.createElement('style');
+    const stylesheet = createIsomorphicStyleSheet(tag, bucketName, elementAttributes);
+    renderer.stylesheets[bucketName] = stylesheet;
 
-    // Find the next bucket which we will add our new style bucket before.
-    for (; currentBucketIndex < styleBucketOrdering.length; currentBucketIndex++) {
-      const nextBucket = renderer.styleElements[styleBucketOrdering[currentBucketIndex]];
-      if (nextBucket) {
-        nextBucketFromCache = nextBucket;
-        break;
-      }
-    }
-
-    const tag = target.createElement('style');
+    if (target && tag) {
+      let currentBucketIndex = styleBucketOrdering.indexOf(bucketName) + 1;
+      let nextBucketFromCache = null;
 
-    tag.dataset['makeStylesBucket'] = bucketName;
+      // Find the next bucket which we will add our new style bucket before.
+      for (; currentBucketIndex < styleBucketOrdering.length; currentBucketIndex++) {
+        const nextBucket = renderer.stylesheets[styleBucketOrdering[currentBucketIndex]];
+        if (nextBucket) {
+          nextBucketFromCache = nextBucket;
+          break;
+        }
+      }
 
-    for (const attribute in elementAttributes) {
-      tag.setAttribute(attribute, elementAttributes[attribute]);
+      target.head.insertBefore(tag, nextBucketFromCache?.element || null);
     }
-
-    renderer.styleElements[bucketName] = tag;
-    target.head.insertBefore(tag, nextBucketFromCache);
   }
 
-  return renderer.styleElements[bucketName]!.sheet as CSSStyleSheet;
+  return renderer.stylesheets[bucketName]!;
 }
diff --git a/packages/core/src/renderer/rehydrateRendererCache.ts b/packages/core/src/renderer/rehydrateRendererCache.ts
index 5ba2986..00a36d5 100644
--- a/packages/core/src/renderer/rehydrateRendererCache.ts
+++ b/packages/core/src/renderer/rehydrateRendererCache.ts
@@ -1,4 +1,5 @@
 import { GriffelRenderer, StyleBucketName } from '../types';
+import { createIsomorphicStyleSheetFromElement } from './createIsomorphicStyleSheet';
 
 // Regexps to extract names of classes and animations
 // https://github.com/styletron/styletron/blob/e0fcae826744eb00ce679ac613a1b10d44256660/packages/styletron-engine-atomic/src/client/client.js#L8
@@ -29,8 +30,8 @@ export function rehydrateRendererCache(
       const regex = regexps[bucketName] || STYLES_HYDRATOR;
 
       // 👇 If some elements are not created yet, we will register them in renderer
-      if (!renderer.styleElements[bucketName]) {
-        renderer.styleElements[bucketName] = styleElement;
+      if (!renderer.stylesheets[bucketName]) {
+        renderer.stylesheets[bucketName] = createIsomorphicStyleSheetFromElement(styleElement);
       }
 
       let match;
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index ec71957..7d370c1 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -85,6 +85,23 @@ export interface MakeStaticStylesOptions {
   renderer: GriffelRenderer;
 }
 
+export interface IsomorphicStyleSheet {
+  /**
+   * Attributes applied to the underlying HTMLStyleElement
+   */
+  elementAttributes: Record<string, string>;
+  /**
+   * Underlying HTMLStyleElement
+   */
+  element: HTMLStyleElement | undefined;
+  bucketName: StyleBucketName;
+  /**
+   * Returns all CSS rules on the stylesheet
+   */
+  cssRules(): string[];
+  insertRule(rule: string): number | undefined;
+}
+
 export interface GriffelRenderer {
   id: string;
 
@@ -96,7 +113,7 @@ export interface GriffelRenderer {
   /**
    * @private
    */
-  styleElements: Partial<Record<StyleBucketName, HTMLStyleElement>>;
+  stylesheets: Partial<Record<StyleBucketName, IsomorphicStyleSheet>>;
 
   /**
    * @private
diff --git a/packages/react/src/renderToStyleElements.ts b/packages/react/src/renderToStyleElements.ts
index 946aab4..bd82a73 100644
--- a/packages/react/src/renderToStyleElements.ts
+++ b/packages/react/src/renderToStyleElements.ts
@@ -1,8 +1,6 @@
 import { styleBucketOrdering } from '@griffel/core';
 import * as React from 'react';
-import type { GriffelRenderer, StyleBucketName } from '@griffel/core';
-
-type CSSRulesGroupedByStyleBucket = Record<StyleBucketName, string[]>;
+import type { GriffelRenderer } from '@griffel/core';
 
 /**
  * This method returns a list of <style> React elements with the rendered CSS. This is useful for Server-Side rendering.
@@ -10,37 +8,28 @@ type CSSRulesGroupedByStyleBucket = Record<StyleBucketName, string[]>;
  * @public
  */
 export function renderToStyleElements(renderer: GriffelRenderer): React.ReactElement[] {
-  const styles = styleBucketOrdering.reduce<CSSRulesGroupedByStyleBucket>((acc, bucketName) => {
-    return { ...acc, [bucketName]: [] };
-  }, {} as CSSRulesGroupedByStyleBucket);
-
-  // eslint-disable-next-line guard-for-in
-  for (const cssRule in renderer.insertionCache) {
-    const bucketName: StyleBucketName = renderer.insertionCache[cssRule];
-
-    styles[bucketName].push(cssRule);
-  }
-
-  return (Object.keys(styles) as StyleBucketName[])
-    .map(bucketName => {
-      const cssRules = styles[bucketName].join('');
-
-      // We don't want to create empty style elements
-      if (cssRules.length === 0) {
+  const stylesheets = Object.values(renderer.stylesheets).sort((a, b) => {
+    return styleBucketOrdering.indexOf(a.bucketName) - styleBucketOrdering.indexOf(b.bucketName);
+  });
+
+  return stylesheets
+    .map(styleElement => {
+      const cssRules = styleElement.cssRules();
+      // don't want to create any empty style elements
+      if (!cssRules.length) {
         return null;
       }
 
       return React.createElement('style', {
-        key: bucketName,
+        key: styleElement.bucketName,
 
         // TODO: support "nonce"
         // ...renderer.styleNodeAttributes,
-
-        'data-make-styles-bucket': bucketName || 'default',
-        'data-make-styles-rehydration': true,
+        ...styleElement.elementAttributes,
+        'data-make-styles-rehydration': 'true',
 
         dangerouslySetInnerHTML: {
-          __html: cssRules,
+          __html: cssRules.join(''),
         },
       });
     })
diff --git a/packages/webpack-extraction-plugin/__fixtures__/babel/basic/code.ts b/packages/webpack-extraction-plugin/__fixtures__/babel/basic/code.ts
new file mode 100644
index 0000000..19dad50
--- /dev/null
+++ b/packages/webpack-extraction-plugin/__fixtures__/babel/basic/code.ts
@@ -0,0 +1,22 @@
+import { __styles } from '@griffel/react';
+
+export const styles = __styles(
+  {
+    root: {
+      sj55zd: 'fe3e8s9',
+      uwmqm3: ['fycuoez', 'f8wuabp'],
+    },
+    icon: {
+      De3pzq: 'fcnqdeg',
+      Bi91k9c: 'faf35ka',
+    },
+  },
+  {
+    d: [
+      '.fe3e8s9{color:red;}',
+      '.fycuoez{padding-left:4px;}',
+      '.f8wuabp{padding-right:4px;}',
+      '.fcnqdeg{background-color:green;}',
+    ],
+  },
+);
diff --git a/packages/webpack-extraction-plugin/__fixtures__/babel/basic/output.ts b/packages/webpack-extraction-plugin/__fixtures__/babel/basic/output.ts
new file mode 100644
index 0000000..2a6fcfd
--- /dev/null
+++ b/packages/webpack-extraction-plugin/__fixtures__/babel/basic/output.ts
@@ -0,0 +1,11 @@
+import { __styles, __css } from '@griffel/react';
+export const styles = __css({
+  root: {
+    sj55zd: 'fe3e8s9',
+    uwmqm3: ['fycuoez', 'f8wuabp'],
+  },
+  icon: {
+    De3pzq: 'fcnqdeg',
+    Bi91k9c: 'faf35ka',
+  },
+});
diff --git a/packages/webpack-extraction-plugin/__fixtures__/babel/multiple/code.ts b/packages/webpack-extraction-plugin/__fixtures__/babel/multiple/code.ts
new file mode 100644
index 0000000..0b4f067
--- /dev/null
+++ b/packages/webpack-extraction-plugin/__fixtures__/babel/multiple/code.ts
@@ -0,0 +1,23 @@
+import { __styles } from '@griffel/react';
+
+export const stylesA = __styles(
+  {
+    root: {
+      sj55zd: 'fe3e8s9',
+    },
+  },
+  {
+    d: ['.fe3e8s9{color:red;}'],
+  },
+);
+
+export const stylesB = __styles(
+  {
+    root: {
+      De3pzq: 'fcnqdeg',
+    },
+  },
+  {
+    d: ['.fcnqdeg{background-color:green;}'],
+  },
+);
diff --git a/packages/webpack-extraction-plugin/__fixtures__/babel/multiple/output.ts b/packages/webpack-extraction-plugin/__fixtures__/babel/multiple/output.ts
new file mode 100644
index 0000000..9669953
--- /dev/null
+++ b/packages/webpack-extraction-plugin/__fixtures__/babel/multiple/output.ts
@@ -0,0 +1,11 @@
+import { __styles, __css } from '@griffel/react';
+export const stylesA = __css({
+  root: {
+    sj55zd: 'fe3e8s9',
+  },
+});
+export const stylesB = __css({
+  root: {
+    De3pzq: 'fcnqdeg',
+  },
+});
diff --git a/packages/webpack-extraction-plugin/package.json b/packages/webpack-extraction-plugin/package.json
index ca3c0ae..918e103 100644
--- a/packages/webpack-extraction-plugin/package.json
+++ b/packages/webpack-extraction-plugin/package.json
@@ -7,5 +7,13 @@
   "repository": {
     "type": "git",
     "url": "https://github.com/microsoft/griffel"
+  },
+  "dependencies": {
+    "@babel/core": "^7.12.13",
+    "@babel/helper-plugin-utils": "^7.12.13",
+    "tslib": "^2.1.0"
+  },
+  "peerDependencies": {
+    "@griffel/core": "^1.3.1"
   }
 }
diff --git a/packages/webpack-extraction-plugin/src/babelPluginStripGriffelRuntime.ts b/packages/webpack-extraction-plugin/src/babelPluginStripGriffelRuntime.ts
new file mode 100644
index 0000000..b50d184
--- /dev/null
+++ b/packages/webpack-extraction-plugin/src/babelPluginStripGriffelRuntime.ts
@@ -0,0 +1,107 @@
+import { NodePath, PluginObj, PluginPass, types as t } from '@babel/core';
+import { declare } from '@babel/helper-plugin-utils';
+import type { CSSRulesByBucket } from '@griffel/core';
+
+type StripRuntimeBabelPluginOptions = never;
+
+type StripRuntimeBabelPluginState = PluginPass & {
+  cssRules?: string[];
+};
+
+export type StripRuntimeBabelPluginMetadata = {
+  cssRules: string[];
+};
+
+export const babelPluginStripGriffelRuntime = declare<
+  Partial<StripRuntimeBabelPluginOptions>,
+  PluginObj<StripRuntimeBabelPluginState>
+>(api => {
+  api.assertVersion(7);
+
+  return {
+    name: '@griffel/webpack-extraction-plugin/babel',
+    pre() {
+      this.cssRules = [];
+    },
+    post() {
+      (this.file.metadata as unknown as StripRuntimeBabelPluginMetadata).cssRules = this.cssRules!;
+    },
+
+    visitor: {
+      Program: {
+        exit(path, state) {
+          path.traverse({
+            ImportSpecifier(path) {
+              const importedPath = path.get('imported');
+
+              if (!importedPath.isIdentifier({ name: '__styles' })) {
+                return;
+              }
+
+              const declarationPath = path.findParent(p =>
+                p.isImportDeclaration(),
+              ) as NodePath<t.ImportDeclaration> | null;
+
+              if (declarationPath === null) {
+                throw path.buildCodeFrameError(
+                  [
+                    'Failed to find "ImportDeclaration" path for an "ImportSpecifier".',
+                    'Please report a bug (https://github.com/microsoft/griffel/issues) if this error happens',
+                  ].join(' '),
+                );
+              }
+
+              declarationPath.pushContainer('specifiers', t.identifier('__css'));
+            },
+
+            /**
+             * Visits all call expressions (__styles function calls):
+             * - replaces "__styles" calls "__css"
+             * - removes CSS rules from "__styles" calls
+             */
+            CallExpression(path) {
+              const calleePath = path.get('callee');
+
+              if (!calleePath.isIdentifier({ name: '__styles' })) {
+                return;
+              }
+
+              calleePath.replaceWith(t.identifier('__css'));
+
+              const argumentPaths = path.get('arguments');
+
+              if (argumentPaths.length !== 2) {
+                throw calleePath.buildCodeFrameError(
+                  [
+                    '"__styles" function call should have exactly two arguments.',
+                    'Please report a bug (https://github.com/microsoft/griffel/issues) if this error happens',
+                  ].join(' '),
+                );
+              }
+
+              // Returns the styles as a JavaScript object
+              const evaluationResult = argumentPaths[1].evaluate();
+
+              if (!evaluationResult.confident) {
+                throw argumentPaths[1].buildCodeFrameError(
+                  [
+                    'Failed to evaluate CSS rules from "__styles" call.',
+                    'Please report a bug (https://github.com/microsoft/griffel/issues) if this error happens',
+                  ].join(' '),
+                );
+              }
+
+              const cssRulesByBucket = evaluationResult.value as CSSRulesByBucket;
+
+              Object.values(cssRulesByBucket).forEach(cssRules => {
+                state.cssRules!.push(...cssRules);
+              });
+
+              argumentPaths[1].remove();
+            },
+          });
+        },
+      },
+    },
+  };
+});
diff --git a/packages/webpack-extraction-plugin/tsconfig.lib.json b/packages/webpack-extraction-plugin/tsconfig.lib.json
index 475cca8..278cb18 100644
--- a/packages/webpack-extraction-plugin/tsconfig.lib.json
+++ b/packages/webpack-extraction-plugin/tsconfig.lib.json
@@ -6,6 +6,6 @@
     "declaration": true,
     "types": ["node", "environment"]
   },
-  "exclude": ["__fixtures__/*/*.ts", "**/*.spec.ts", "**/*.test.ts"],
+  "exclude": ["__fixtures__/**/*/*.ts", "**/*.spec.ts", "**/*.test.ts"],
   "include": ["**/*.ts"]
 }
diff --git a/packages/webpack-extraction-plugin/tsconfig.spec.json b/packages/webpack-extraction-plugin/tsconfig.spec.json
index 76b0900..c8d8748 100644
--- a/packages/webpack-extraction-plugin/tsconfig.spec.json
+++ b/packages/webpack-extraction-plugin/tsconfig.spec.json
@@ -7,7 +7,7 @@
     "types": ["jest", "node", "environment"]
   },
   "include": [
-    "__fixtures__/*/code.ts",
+    "__fixtures__/**/*/code.ts",
     "**/*.test.ts",
     "**/*.spec.ts",
     "**/*.test.tsx",


Test patch:
diff --git a/packages/core/src/makeStyles.test.ts b/packages/core/src/makeStyles.test.ts
index 71eed6a..4064348 100644
--- a/packages/core/src/makeStyles.test.ts
+++ b/packages/core/src/makeStyles.test.ts
@@ -188,7 +188,7 @@ describe('makeStyles', () => {
     // Classes emitted by different renderers can be the same
     expect(classesA).toBe(classesB);
     // Style elements should be different for different renderers
-    expect(rendererA.styleElements.d).not.toBe(rendererB.styleElements.d);
+    expect(rendererA.stylesheets.d).not.toBe(rendererB.stylesheets.d);
 
     expect(rendererA).toMatchInlineSnapshot(`
       .f22iagw {
diff --git a/packages/core/src/renderer/createIsomorphicStyleElement-node.test.ts b/packages/core/src/renderer/createIsomorphicStyleElement-node.test.ts
new file mode 100644
index 0000000..d491e5f
--- /dev/null
+++ b/packages/core/src/renderer/createIsomorphicStyleElement-node.test.ts
@@ -0,0 +1,29 @@
+/*
+ * @jest-environment node
+ */
+
+// 👆 this is intentionally to test in SSR like environment
+
+import { createIsomorphicStyleSheet } from './createIsomorphicStyleSheet';
+
+describe('createIsomorphicStyleElement - node', () => {
+  it('should insert css rule', () => {
+    const stylesheet = createIsomorphicStyleSheet(undefined, 'd', {});
+    stylesheet.insertRule(".foo { color: 'red' }");
+    expect(stylesheet.cssRules()).toMatchInlineSnapshot(`
+      Array [
+        ".foo { color: 'red' }",
+      ]
+    `);
+  });
+
+  it('should set element attributes', () => {
+    const stylesheet = createIsomorphicStyleSheet(undefined, 'd', { 'data-foo': 'foo' });
+    expect(stylesheet.elementAttributes).toMatchInlineSnapshot(`
+      Object {
+        "data-foo": "foo",
+        "data-make-styles-bucket": "d",
+      }
+    `);
+  });
+});
diff --git a/packages/core/src/renderer/createIsomorphicStyleSheet.test.ts b/packages/core/src/renderer/createIsomorphicStyleSheet.test.ts
new file mode 100644
index 0000000..818764c
--- /dev/null
+++ b/packages/core/src/renderer/createIsomorphicStyleSheet.test.ts
@@ -0,0 +1,47 @@
+import { createIsomorphicStyleSheet } from './createIsomorphicStyleSheet';
+
+describe('createIsomorphicStyleElement', () => {
+  it('should insert css rule', () => {
+    const stylesheet = createIsomorphicStyleSheet(document.createElement('style'), 'd', {});
+    stylesheet.insertRule(".foo { color: 'red' }");
+    expect(stylesheet.cssRules()).toMatchInlineSnapshot(`
+      Array [
+        ".foo { color: 'red' }",
+      ]
+    `);
+  });
+
+  it('should set element attributes', () => {
+    const stylesheet = createIsomorphicStyleSheet(document.createElement('style'), 'd', { 'data-foo': 'foo' });
+    expect(stylesheet.elementAttributes).toMatchInlineSnapshot(`
+      Object {
+        "data-foo": "foo",
+        "data-make-styles-bucket": "d",
+      }
+    `);
+
+    expect(stylesheet.element).toMatchInlineSnapshot(`
+      <style
+        data-foo="foo"
+        data-make-styles-bucket="d"
+      />
+    `);
+
+    const actualElementAttributes = Array.from(stylesheet.element!.attributes).reduce((acc, attr) => {
+      acc[attr.name] = attr.value;
+      return acc;
+    }, {} as Record<string, string>);
+
+    expect(actualElementAttributes).toEqual(stylesheet.elementAttributes);
+  });
+
+  it('should create HTML style element', () => {
+    const stylesheet = createIsomorphicStyleSheet(document.createElement('style'), 'd', {});
+    expect(stylesheet.element).not.toBeUndefined();
+    expect(stylesheet.element).toMatchInlineSnapshot(`
+      <style
+        data-make-styles-bucket="d"
+      />
+    `);
+  });
+});
diff --git a/packages/core/src/renderer/rehydrateRendererCache.test.ts b/packages/core/src/renderer/rehydrateRendererCache.test.ts
new file mode 100644
index 0000000..810e9d3
--- /dev/null
+++ b/packages/core/src/renderer/rehydrateRendererCache.test.ts
@@ -0,0 +1,37 @@
+import { DATA_BUCKET_ATTR } from '../constants';
+import { createDOMRenderer } from './createDOMRenderer';
+import { rehydrateRendererCache } from './rehydrateRendererCache';
+
+describe('rehydrateRendererCache', () => {
+  it('should rehydrate renderer cache', () => {
+    const renderer = createDOMRenderer(document);
+    const styleElement = document.createElement('style');
+    styleElement.setAttribute(DATA_BUCKET_ATTR, 'd');
+    document.head.appendChild(styleElement);
+    styleElement.textContent = '.foo { color: red; }';
+
+    rehydrateRendererCache(renderer, document);
+
+    expect(renderer.insertionCache).toMatchInlineSnapshot(`
+      Object {
+        ".foo { color: red; }": "d",
+      }
+    `);
+  });
+
+  it('should create isomorphic stylesheet', () => {
+    const renderer = createDOMRenderer(document);
+    const styleElement = document.createElement('style');
+    document.head.appendChild(styleElement);
+    styleElement.setAttribute(DATA_BUCKET_ATTR, 'd');
+    rehydrateRendererCache(renderer, document);
+
+    expect(renderer.stylesheets.d).not.toBeUndefined();
+    expect(renderer.stylesheets.d?.bucketName).toMatchInlineSnapshot(`"d"`);
+    expect(renderer.stylesheets.d?.elementAttributes).toMatchInlineSnapshot(`
+      Object {
+        "data-make-styles-bucket": "d",
+      }
+    `);
+  });
+});
diff --git a/packages/react/src/renderToStyleElements.test.tsx b/packages/react/src/renderToStyleElements.test.tsx
new file mode 100644
index 0000000..cf85399
--- /dev/null
+++ b/packages/react/src/renderToStyleElements.test.tsx
@@ -0,0 +1,55 @@
+import { createDOMRenderer } from '@griffel/core';
+import * as prettier from 'prettier';
+import * as React from 'react';
+import * as ReactDOM from 'react-dom/server';
+
+import { makeStyles } from './makeStyles';
+import { RendererProvider } from './RendererContext';
+import { renderToStyleElements } from './renderToStyleElements';
+
+expect.addSnapshotSerializer({
+  test(value) {
+    return typeof value === 'string';
+  },
+  print(value) {
+    /**
+     * test function makes sure that value is the guarded type
+     */
+    const _value = value as string;
+
+    return prettier.format(_value, { parser: 'html' }).trim();
+  },
+});
+
+describe('renderToStyleElements', () => {
+  it('renders elements in DOM env', () => {
+    const useExampleStyles = makeStyles({
+      root: { color: 'red', ':hover': { color: 'green' } },
+    });
+    const ExampleComponent: React.FC = () => {
+      const classes = useExampleStyles();
+
+      return <div className={classes.root} />;
+    };
+
+    const renderer = createDOMRenderer();
+
+    ReactDOM.renderToStaticMarkup(
+      <RendererProvider renderer={renderer}>
+        <ExampleComponent />
+      </RendererProvider>,
+    );
+
+    expect(ReactDOM.renderToStaticMarkup(<>{renderToStyleElements(renderer)}</>)).toMatchInlineSnapshot(`
+      <style data-make-styles-bucket="d" data-make-styles-rehydration="true">
+        .fe3e8s9 {
+          color: red;
+        }</style
+      ><style data-make-styles-bucket="h" data-make-styles-rehydration="true">
+        .f1ej289o:hover {
+          color: green;
+        }
+      </style>
+    `);
+  });
+});
diff --git a/packages/webpack-extraction-plugin/src/babelPluginStripGriffelRuntime.test.ts b/packages/webpack-extraction-plugin/src/babelPluginStripGriffelRuntime.test.ts
new file mode 100644
index 0000000..c29bb93
--- /dev/null
+++ b/packages/webpack-extraction-plugin/src/babelPluginStripGriffelRuntime.test.ts
@@ -0,0 +1,36 @@
+import pluginTester, { prettierFormatter } from 'babel-plugin-tester';
+import * as fs from 'fs';
+import * as path from 'path';
+
+import { babelPluginStripGriffelRuntime } from './babelPluginStripGriffelRuntime';
+
+const prettierConfig = JSON.parse(
+  fs.readFileSync(path.resolve(__dirname, '../../../.prettierrc'), { encoding: 'utf-8' }),
+);
+const fixturesDir = path.join(__dirname, '..', '__fixtures__', 'babel');
+
+pluginTester({
+  babelOptions: {
+    parserOpts: {
+      plugins: ['typescript'],
+    },
+  },
+  pluginOptions: {
+    babelOptions: {
+      presets: ['@babel/typescript'],
+    },
+  },
+  formatResult: code =>
+    prettierFormatter(code, {
+      config: {
+        ...prettierConfig,
+        parser: 'typescript',
+      },
+    }),
+
+  fixtures: fixturesDir,
+  tests: [],
+
+  plugin: babelPluginStripGriffelRuntime,
+  pluginName: '@griffel/webpack-extraction-plugin/babel',
+});