Repository: microsoft/griffel
Pull request #426 (2023-08-22 16:51:14)

Stop inlining the CSS in the virtual CSS module request
This change should have no impact on production build output. However, one issue I've noticed hen trying to migrate Outlook to use this is that our bundle-diff tool gets unwieldy with the massive module requests that embed the entire extracted CSS into the request.

Instead, this change stores the extracted CSS in a map in the plugin, and uses an enhancement to the loader context to read and write the CSS in the map. As a result, we can eliminate the `?styles=<css>` part of the module request.

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

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

<!-- Sticky Pull Request Comment -->

Patch:
diff --git a/change/@griffel-babel-preset-84c90e38-9d5d-40cf-9ef9-ef07ef7886b5.json b/change/@griffel-babel-preset-84c90e38-9d5d-40cf-9ef9-ef07ef7886b5.json
new file mode 100644
index 0000000..f11a6a3
--- /dev/null
+++ b/change/@griffel-babel-preset-84c90e38-9d5d-40cf-9ef9-ef07ef7886b5.json
@@ -0,0 +1,7 @@
+{
+  "type": "minor",
+  "comment": "feat: Add metadata generation to babel preset",
+  "packageName": "@griffel/babel-preset",
+  "email": "lingfan.gao@microsoft.com",
+  "dependentChangeType": "patch"
+}
diff --git a/change/@griffel-webpack-extraction-plugin-2722342d-e7ad-42c7-b370-cc43d50217de.json b/change/@griffel-webpack-extraction-plugin-2722342d-e7ad-42c7-b370-cc43d50217de.json
new file mode 100644
index 0000000..e9f03fa
--- /dev/null
+++ b/change/@griffel-webpack-extraction-plugin-2722342d-e7ad-42c7-b370-cc43d50217de.json
@@ -0,0 +1,7 @@
+{
+  "type": "patch",
+  "comment": "chore: stop inlining the styles in the virtual CSS module request",
+  "packageName": "@griffel/webpack-extraction-plugin",
+  "email": "miclo@microsoft.com",
+  "dependentChangeType": "patch"
+}
diff --git a/packages/babel-preset/README.md b/packages/babel-preset/README.md
index 46c8423..aa56352 100644
--- a/packages/babel-preset/README.md
+++ b/packages/babel-preset/README.md
@@ -13,6 +13,7 @@ A Babel preset that performs build time transforms for [`@griffel/react`](../rea
   - [Configuring module evaluation](#configuring-module-evaluation)
 - [Transforms](#transforms)
 - [Example](#example)
+- [Access CSS output from code](#access-css-output-from-code)
 - [Troubleshooting](#troubleshooting)
   - [Module evaluation](#module-evaluation)
 
@@ -161,6 +162,55 @@ import { __styles } from '@griffel/react';
 const useStyles = __styles(/* resolved styles */);
 ```
 
+## Access CSS output from code
+
+It's possible to configure the preset to return all the evaluated styles of a file. This
+metadata looks something like below:
+
+```ts
+const output = {
+  // makeStyles
+  cssEntries: {
+    // by each hook
+    useStyles1: {
+      // by each slot
+      root: [".fxxxxx { color: 'red' }"],
+    },
+    useStyles2: {
+      root: [".fxxxxx { color: 'red' }"],
+    },
+  },
+  // makeResetStyles
+  cssResetEntries: {
+    // by each hook
+    useResetStyles1: [".fxxxxx { color: 'red' }"],
+    useResetStyles2: [".fxxxxx { color: 'red' }"],
+  },
+};
+```
+
+This is intended for programmatic transforms in code so that they can be reused by other tooling without extra steps to determine
+the output styles
+
+```ts
+import { griffelPreset, BabelPluginMetadata } from '@griffel/babel-preset';
+
+const babelFileResult = Babel.transformFromAstSync(babelAST, sourceCode, {
+  babelrc: false,
+  configFile: false,
+  presets: [[griffelPreset, { generateMetadata: true }]],
+
+  filename: options.filename,
+
+  sourceMaps: options.enableSourceMaps,
+  sourceFileName: options.filename,
+  inputSourceMap: options.inputSourceMap,
+});
+
+// metadata
+console.log(babelFileResult.metadata as unknown as BabelPluginMetadata);
+```
+
 ## Troubleshooting
 
 This section focuses mainly on troubleshooting this babel preset in the [microsoft/fluentui](https://github.com/microsoft/fluentui) repo.
diff --git a/packages/babel-preset/src/index.ts b/packages/babel-preset/src/index.ts
index d6f2dd9..e104771 100644
--- a/packages/babel-preset/src/index.ts
+++ b/packages/babel-preset/src/index.ts
@@ -1,14 +1,14 @@
 import { transformPlugin } from './transformPlugin';
 
 import type { ConfigAPI } from '@babel/core';
-import type { BabelPluginOptions } from './types';
+import type { BabelPluginOptions, BabelPluginMetadata } from './types';
 
 export { default as shakerEvaluator } from '@linaria/shaker';
 export { EvalCache, Module } from '@linaria/babel-preset';
 export { configSchema } from './schema';
 
 export type { Evaluator, EvalRule } from '@linaria/babel-preset';
-export type { BabelPluginOptions };
+export type { BabelPluginOptions, BabelPluginMetadata };
 
 export default function griffelPreset(babel: ConfigAPI, options: BabelPluginOptions) {
   return {
diff --git a/packages/babel-preset/src/schema.ts b/packages/babel-preset/src/schema.ts
index 4f31641..71d1ae8 100644
--- a/packages/babel-preset/src/schema.ts
+++ b/packages/babel-preset/src/schema.ts
@@ -6,6 +6,9 @@ export const configSchema: JSONSchema7 = {
 
   type: 'object',
   properties: {
+    generateMetadata: {
+      type: 'boolean',
+    },
     modules: {
       type: 'array',
       items: {
diff --git a/packages/babel-preset/src/transformPlugin.ts b/packages/babel-preset/src/transformPlugin.ts
index a1061e1..dd4cbe9 100644
--- a/packages/babel-preset/src/transformPlugin.ts
+++ b/packages/babel-preset/src/transformPlugin.ts
@@ -2,14 +2,21 @@ import { NodePath, PluginObj, PluginPass, types as t } from '@babel/core';
 import { declare } from '@babel/helper-plugin-utils';
 import { Module } from '@linaria/babel-preset';
 import shakerEvaluator from '@linaria/shaker';
-import { resolveStyleRulesForSlots, GriffelStyle, resolveResetStyleRules } from '@griffel/core';
+import {
+  resolveStyleRulesForSlots,
+  GriffelStyle,
+  resolveResetStyleRules,
+  CSSRulesByBucket,
+  CSSClassesMapBySlot,
+  normalizeCSSBucketEntry,
+} from '@griffel/core';
 import * as path from 'path';
 
 import { normalizeStyleRules } from './assets/normalizeStyleRules';
 import { replaceAssetsWithImports } from './assets/replaceAssetsWithImports';
 import { dedupeCSSRules } from './utils/dedupeCSSRules';
 import { evaluatePaths } from './utils/evaluatePaths';
-import { BabelPluginOptions } from './types';
+import { BabelPluginOptions, BabelPluginMetadata } from './types';
 import { validateOptions } from './validateOptions';
 
 type FunctionKinds = 'makeStyles' | 'makeResetStyles';
@@ -18,8 +25,17 @@ type BabelPluginState = PluginPass & {
   importDeclarationPaths?: NodePath<t.ImportDeclaration>[];
   requireDeclarationPath?: NodePath<t.VariableDeclarator>;
 
-  definitionPaths?: { functionKind: FunctionKinds; path: NodePath<t.Expression | t.SpreadElement> }[];
+  definitionPaths?: {
+    /** The name of the resulting hook from a Griffel call */
+    declaratorId: string;
+    /** The type of Griffel call */
+    functionKind: FunctionKinds;
+    /** The code path of the Griffel call  */
+    path: NodePath<t.Expression | t.SpreadElement>;
+  }[];
   calleePaths?: NodePath<t.Identifier>[];
+  cssEntries?: BabelPluginMetadata['cssEntries'];
+  cssResetEntries?: BabelPluginMetadata['cssResetEntries'];
 };
 
 function getDefinitionPathFromCallExpression(
@@ -62,6 +78,83 @@ function getCalleeFunctionKind(
   return null;
 }
 
+/**
+ * Gets the id of the parent variable declarator
+ */
+function getParentDeclaratorId(path: NodePath<t.CallExpression> | NodePath<t.MemberExpression>): string {
+  const declarator = path.findParent(p => p.isVariableDeclarator());
+  if (declarator?.isVariableDeclarator()) {
+    const id = declarator.get('id');
+    if (id.isIdentifier()) {
+      return id.node.name;
+    }
+  }
+
+  return 'unknownHook';
+}
+
+/**
+ * Extracts CSS rules from evaluated reset styles to metadata
+ */
+function buildCSSResetEntriesMetadata(
+  state: Required<BabelPluginState>,
+  cssRules: string[] | CSSRulesByBucket,
+  declaratorId: string,
+) {
+  const cssRulesByBucket: CSSRulesByBucket = Array.isArray(cssRules) ? { d: cssRules } : cssRules;
+  state.cssResetEntries[declaratorId] ??= [];
+  state.cssResetEntries[declaratorId] = Object.values(cssRulesByBucket).flatMap(bucketEntries => {
+    return bucketEntries.map(bucketEntry => {
+      if (Array.isArray(bucketEntry)) {
+        throw new Error(
+          `CSS rules in buckets for "makeResetStyles()" should not contain arrays, got: ${JSON.stringify(
+            bucketEntry,
+          )})}`,
+        );
+      }
+
+      return bucketEntry;
+    });
+  });
+}
+
+/**
+ * Extracts CSS rules from evaluated styles to metadata
+ */
+function buildCSSEntriesMetadata(
+  state: Required<BabelPluginState>,
+  classnamesMapping: CSSClassesMapBySlot<string>,
+  cssRulesByBucket: CSSRulesByBucket,
+  declaratorId: string,
+) {
+  const classesBySlot: Record<string, string[]> = Object.fromEntries(
+    Object.entries(classnamesMapping).map(([slot, cssClassesMap]) => {
+      return [
+        slot,
+        Object.values(cssClassesMap).flatMap(cssClasses => (Array.isArray(cssClasses) ? cssClasses : [cssClasses])),
+      ];
+    }),
+  );
+
+  const cssRules: string[] = Object.values(cssRulesByBucket).flatMap(cssRulesByBucket => {
+    return cssRulesByBucket.map(bucketEntry => {
+      const [cssRule] = normalizeCSSBucketEntry(bucketEntry);
+      return cssRule;
+    });
+  });
+
+  state.cssEntries[declaratorId] = Object.fromEntries(
+    Object.entries(classesBySlot).map(([slot, cssClasses]) => {
+      return [
+        slot,
+        cssClasses.map(cssClass => {
+          return cssRules.find(rule => rule.includes(cssClass))!;
+        }),
+      ];
+    }),
+  );
+}
+
 /**
  * Checks if import statement import makeStyles().
  */
@@ -107,6 +200,7 @@ export const transformPlugin = declare<Partial<BabelPluginOptions>, PluginObj<Ba
 
   const pluginOptions: Required<BabelPluginOptions> = {
     babelOptions: {},
+    generateMetadata: false,
     modules: [
       { moduleSource: '@griffel/react', importName: 'makeStyles' },
       { moduleSource: '@fluentui/react-components', importName: 'makeStyles' },
@@ -132,6 +226,8 @@ export const transformPlugin = declare<Partial<BabelPluginOptions>, PluginObj<Ba
       this.importDeclarationPaths = [];
       this.definitionPaths = [];
       this.calleePaths = [];
+      this.cssEntries = {};
+      this.cssResetEntries = {};
     },
 
     visitor: {
@@ -196,6 +292,15 @@ export const transformPlugin = declare<Partial<BabelPluginOptions>, PluginObj<Ba
                 );
                 const uniqueCSSRules = dedupeCSSRules(cssRulesByBucket);
 
+                if (pluginOptions.generateMetadata) {
+                  buildCSSEntriesMetadata(
+                    state as Required<BabelPluginState>,
+                    classnamesMapping,
+                    uniqueCSSRules,
+                    definitionPath.declaratorId,
+                  );
+                }
+
                 (callExpressionPath.get('arguments.0') as NodePath).remove();
                 callExpressionPath.pushContainer('arguments', [
                   t.valueToNode(classnamesMapping),
@@ -218,6 +323,14 @@ export const transformPlugin = declare<Partial<BabelPluginOptions>, PluginObj<Ba
                   ),
                 );
 
+                if (pluginOptions.generateMetadata) {
+                  buildCSSResetEntriesMetadata(
+                    state as Required<BabelPluginState>,
+                    cssRules,
+                    definitionPath.declaratorId,
+                  );
+                }
+
                 (callExpressionPath.get('arguments.0') as NodePath).remove();
                 callExpressionPath.pushContainer('arguments', [
                   t.valueToNode(ltrClassName),
@@ -228,6 +341,13 @@ export const transformPlugin = declare<Partial<BabelPluginOptions>, PluginObj<Ba
 
               replaceAssetsWithImports(pluginOptions.projectRoot, state.filename!, programPath, callExpressionPath);
             });
+
+            if (pluginOptions.generateMetadata) {
+              Object.assign(this.file.metadata, {
+                cssResetEntries: state.cssResetEntries ?? {},
+                cssEntries: state.cssEntries ?? {},
+              } as BabelPluginMetadata);
+            }
           }
 
           state.importDeclarationPaths!.forEach(importDeclarationPath => {
@@ -296,9 +416,11 @@ export const transformPlugin = declare<Partial<BabelPluginOptions>, PluginObj<Ba
 
         if (calleePath.isIdentifier()) {
           const functionKind = getCalleeFunctionKind(calleePath, pluginOptions.modules);
+          const declaratorId = getParentDeclaratorId(path);
 
           if (functionKind) {
             state.definitionPaths!.push({
+              declaratorId,
               functionKind,
               path: getDefinitionPathFromCallExpression(functionKind, path),
             });
@@ -343,8 +465,10 @@ export const transformPlugin = declare<Partial<BabelPluginOptions>, PluginObj<Ba
           return;
         }
 
+        const declaratorId = getParentDeclaratorId(expressionPath);
         state.definitionPaths!.push({
           functionKind,
+          declaratorId,
           path: getDefinitionPathFromCallExpression(functionKind, parentPath),
         });
         state.calleePaths!.push(propertyPath as NodePath<t.Identifier>);
diff --git a/packages/babel-preset/src/types.ts b/packages/babel-preset/src/types.ts
index 5ac91d6..3107cb2 100644
--- a/packages/babel-preset/src/types.ts
+++ b/packages/babel-preset/src/types.ts
@@ -2,6 +2,11 @@ import type { TransformOptions } from '@babel/core';
 import type { EvalRule } from '@linaria/babel-preset';
 
 export type BabelPluginOptions = {
+  /**
+   * Returns the evaluated CSS rules in the file result metadata
+   * @default false
+   */
+  generateMetadata?: boolean;
   /** Defines set of modules and imports handled by a transformPlugin. */
   modules?: {
     moduleSource: string;
@@ -26,3 +31,8 @@ export type BabelPluginOptions = {
    */
   projectRoot?: string;
 };
+
+export type BabelPluginMetadata = {
+  cssEntries: Record<string, Record<string, string[]>>;
+  cssResetEntries: Record<string, string[]>;
+};
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/assets-flip/output.ts b/packages/webpack-extraction-plugin/__fixtures__/webpack/assets-flip/output.ts
index 6b28321..f387893 100644
--- a/packages/webpack-extraction-plugin/__fixtures__/webpack/assets-flip/output.ts
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/assets-flip/output.ts
@@ -6,4 +6,4 @@ export const useStyles = _css({
   },
 });
 
-import './code.griffel.css!=!../../../virtual-loader/index.js?style=%2F**%20%40griffel%3Acss-start%20%5Bd%5D%20**%2F%0A.ftcbn0w%7Bbackground-image%3Aurl(.%2Fleft.jpg)%2Curl(.%2Fright.jpg)%3B%7D.f37sp7w%7Bbackground-image%3Aurl(.%2Fright.jpg)%2Curl(.%2Fleft.jpg)%3B%7D%0A%2F**%20%40griffel%3Acss-end%20**%2F%0A!./code.ts';
+import './code.griffel.css!=!../../../virtual-loader/index.js!./code.ts';
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 3e9028b..b7e884f 100644
--- a/packages/webpack-extraction-plugin/__fixtures__/webpack/assets-multiple/output.ts
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/assets-multiple/output.ts
@@ -6,4 +6,4 @@ export const useStyles = _css({
   },
 });
 
-import './code.griffel.css!=!../../../virtual-loader/index.js?style=%2F**%20%40griffel%3Acss-start%20%5Bd%5D%20**%2F%0A.fp00rh9%7Bbackground-image%3Aurl(.%2Fblank.jpg)%2Curl(.%2Fempty.jpg)%3B%7D%0A%2F**%20%40griffel%3Acss-end%20**%2F%0A!./code.ts';
+import './code.griffel.css!=!../../../virtual-loader/index.js!./code.ts';
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/assets/output.ts b/packages/webpack-extraction-plugin/__fixtures__/webpack/assets/output.ts
index e996bbd..7ffe48a 100644
--- a/packages/webpack-extraction-plugin/__fixtures__/webpack/assets/output.ts
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/assets/output.ts
@@ -6,4 +6,4 @@ export const useStyles = _css({
   },
 });
 
-import './code.griffel.css!=!../../../virtual-loader/index.js?style=%2F**%20%40griffel%3Acss-start%20%5Bd%5D%20**%2F%0A.fnwsaxv%7Bbackground-image%3Aurl(.%2Fblank.jpg)%3B%7D%0A%2F**%20%40griffel%3Acss-end%20**%2F%0A!./code.ts';
+import './code.griffel.css!=!../../../virtual-loader/index.js!./code.ts';
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 0bf2287..02633e8 100644
--- a/packages/webpack-extraction-plugin/__fixtures__/webpack/basic-rules/output.ts
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/basic-rules/output.ts
@@ -11,4 +11,4 @@ export const styles = _css({
   },
 });
 
-import './code.griffel.css!=!../../../virtual-loader/index.js?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!./code.ts';
+import './code.griffel.css!=!../../../virtual-loader/index.js!./code.ts';
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/mixed/output.ts b/packages/webpack-extraction-plugin/__fixtures__/webpack/mixed/output.ts
index 745375c..652dcfa 100644
--- a/packages/webpack-extraction-plugin/__fixtures__/webpack/mixed/output.ts
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/mixed/output.ts
@@ -8,4 +8,4 @@ export const useClasses = _css({
 });
 export const useClassName = _resetCSS('rjefjbm', 'r7z97ji');
 
-import './code.griffel.css!=!../../../virtual-loader/index.js?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%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!./code.ts';
+import './code.griffel.css!=!../../../virtual-loader/index.js!./code.ts';
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/multiple/output.ts b/packages/webpack-extraction-plugin/__fixtures__/webpack/multiple/output.ts
index 9afa022..b56bf2e 100644
--- a/packages/webpack-extraction-plugin/__fixtures__/webpack/multiple/output.ts
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/multiple/output.ts
@@ -11,4 +11,4 @@ export const stylesB = _css({
   },
 });
 
-import './code.griffel.css!=!../../../virtual-loader/index.js?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!./code.ts';
+import './code.griffel.css!=!../../../virtual-loader/index.js!./code.ts';
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 e1d5b22..6bf8707 100644
--- a/packages/webpack-extraction-plugin/__fixtures__/webpack/reset-assets/output.ts
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/reset-assets/output.ts
@@ -2,4 +2,4 @@ import { __resetCSS as _resetCSS } from '@griffel/react';
 import { __resetStyles } from '@griffel/react';
 export const useClassName = _resetCSS('ra9m047', null);
 
-import './code.griffel.css!=!../../../virtual-loader/index.js?style=%2F**%20%40griffel%3Acss-start%20%5Br%5D%20**%2F%0A.ra9m047%7Bbackground-image%3Aurl(.%2Fblank.jpg)%3B%7D%0A%2F**%20%40griffel%3Acss-end%20**%2F%0A!./code.ts';
+import './code.griffel.css!=!../../../virtual-loader/index.js!./code.ts';
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/reset/output.ts b/packages/webpack-extraction-plugin/__fixtures__/webpack/reset/output.ts
index 9afe2cd..3959222 100644
--- a/packages/webpack-extraction-plugin/__fixtures__/webpack/reset/output.ts
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/reset/output.ts
@@ -2,4 +2,4 @@ import { __resetCSS as _resetCSS } from '@griffel/react';
 import { __resetStyles } from '@griffel/react';
 export const useClassName = _resetCSS('rjefjbm', 'r7z97ji');
 
-import './code.griffel.css!=!../../../virtual-loader/index.js?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!./code.ts';
+import './code.griffel.css!=!../../../virtual-loader/index.js!./code.ts';
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/unstable-attach-to-main/output.ts b/packages/webpack-extraction-plugin/__fixtures__/webpack/unstable-attach-to-main/output.ts
index 80a9935..741cd6e 100644
--- a/packages/webpack-extraction-plugin/__fixtures__/webpack/unstable-attach-to-main/output.ts
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/unstable-attach-to-main/output.ts
@@ -10,4 +10,4 @@ export const useClasses = __styles(
   },
 );
 
-require('./code.griffel.css!=!../../../virtual-loader/index.js?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!./code.ts');
+require('./code.griffel.css!=!../../../virtual-loader/index.js!./code.ts');
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/unstable-keep-original-code/output.ts b/packages/webpack-extraction-plugin/__fixtures__/webpack/unstable-keep-original-code/output.ts
index 3d53c0e..6044a32 100644
--- a/packages/webpack-extraction-plugin/__fixtures__/webpack/unstable-keep-original-code/output.ts
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/unstable-keep-original-code/output.ts
@@ -16,4 +16,4 @@ export const useClassName = __resetStyles('rjefjbm', 'r7z97ji', [
   '.r7z97ji{color:red;padding-right:4px;}',
 ]);
 
-import './code.griffel.css!=!../../../virtual-loader/index.js?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%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!./code.ts';
+import './code.griffel.css!=!../../../virtual-loader/index.js!./code.ts';
diff --git a/packages/webpack-extraction-plugin/src/GriffelCSSExtractionPlugin.ts b/packages/webpack-extraction-plugin/src/GriffelCSSExtractionPlugin.ts
index e644318..4fccaf2 100644
--- a/packages/webpack-extraction-plugin/src/GriffelCSSExtractionPlugin.ts
+++ b/packages/webpack-extraction-plugin/src/GriffelCSSExtractionPlugin.ts
@@ -1,9 +1,10 @@
 import { defaultCompareMediaQueries, GriffelRenderer } from '@griffel/core';
-import { Compilation } from 'webpack';
+import { Compilation, NormalModule } from 'webpack';
 import type { Chunk, Compiler, Module, sources } from 'webpack';
 
 import { parseCSSRules } from './parseCSSRules';
 import { sortCSSRules } from './sortCSSRules';
+import { PLUGIN_NAME, GriffelCssLoaderContextKey, type SupplementedLoaderContext } from './constants';
 
 // Webpack does not export these constants
 // https://github.com/webpack/webpack/blob/b67626c7b4ffed8737d195b27c8cea1e68d58134/lib/OptimizationStages.js#L8
@@ -15,8 +16,6 @@ export type GriffelCSSExtractionPluginOptions = {
   unstable_attachToMainEntryPoint?: boolean;
 };
 
-const PLUGIN_NAME = 'GriffelExtractPlugin';
-
 function attachGriffelChunkToMainEntryPoint(compilation: Compilation, griffelChunk: Chunk) {
   const entryPoints = Array.from(compilation.entrypoints.values());
 
@@ -177,6 +176,28 @@ export class GriffelCSSExtractionPlugin {
     }
 
     compiler.hooks.compilation.tap(PLUGIN_NAME, compilation => {
+      // WHAT?
+      //   Adds a callback to the loader context
+      // WHY?
+      //   Allows us to register the CSS extracted from Griffel calls to then process in a CSS module
+      const cssByModuleMap = new Map<string, string>();
+
+      NormalModule.getCompilationHooks(compilation).loader.tap(PLUGIN_NAME, (loaderContext, module) => {
+        const resourcePath = module.resource;
+
+        (loaderContext as SupplementedLoaderContext)[GriffelCssLoaderContextKey] = {
+          registerExtractedCss(css: string) {
+            cssByModuleMap.set(resourcePath, css);
+          },
+          getExtractedCss() {
+            const css = cssByModuleMap.get(resourcePath) ?? '';
+            cssByModuleMap.delete(resourcePath);
+
+            return css;
+          },
+        };
+      });
+
       compilation.hooks.optimizeChunks.tap({ name: PLUGIN_NAME, stage: OPTIMIZE_CHUNKS_STAGE_ADVANCED }, () => {
         // WHAT?
         //   Performs module movements between chunks if SplitChunksPlugin is not enabled.
diff --git a/packages/webpack-extraction-plugin/src/constants.ts b/packages/webpack-extraction-plugin/src/constants.ts
new file mode 100644
index 0000000..87a40e9
--- /dev/null
+++ b/packages/webpack-extraction-plugin/src/constants.ts
@@ -0,0 +1,13 @@
+import type { LoaderContext } from 'webpack';
+
+export const PLUGIN_NAME = 'GriffelExtractPlugin';
+export const GriffelCssLoaderContextKey = Symbol(`${PLUGIN_NAME}/GriffelCssLoaderContextKey`);
+
+export interface GriffelLoaderContextSupplement {
+  registerExtractedCss(css: string): void;
+  getExtractedCss(): string;
+}
+
+export type SupplementedLoaderContext<Options = unknown> = LoaderContext<Options> & {
+  [GriffelCssLoaderContextKey]?: GriffelLoaderContextSupplement;
+};
diff --git a/packages/webpack-extraction-plugin/src/webpackLoader.ts b/packages/webpack-extraction-plugin/src/webpackLoader.ts
index be70cfd..741094a 100644
--- a/packages/webpack-extraction-plugin/src/webpackLoader.ts
+++ b/packages/webpack-extraction-plugin/src/webpackLoader.ts
@@ -3,6 +3,7 @@ import * as path from 'path';
 import * as webpack from 'webpack';
 
 import { transformSync, TransformResult, TransformOptions } from './transformSync';
+import { GriffelCssLoaderContextKey, SupplementedLoaderContext } from './constants';
 
 export type WebpackLoaderOptions = {
   /**
@@ -15,10 +16,6 @@ type WebpackLoaderParams = Parameters<webpack.LoaderDefinitionFunction<WebpackLo
 
 const virtualLoaderPath = path.resolve(__dirname, '..', 'virtual-loader', 'index.js');
 
-function toURIComponent(rule: string): string {
-  return encodeURIComponent(rule).replace(/!/g, '%21');
-}
-
 /**
  * Webpack can also pass sourcemaps as a string, Babel accepts only objects.
  * See https://github.com/babel/babel-loader/pull/889.
@@ -36,7 +33,7 @@ function parseSourceMap(inputSourceMap: WebpackLoaderParams[1]): TransformOption
 }
 
 function webpackLoader(
-  this: webpack.LoaderContext<WebpackLoaderOptions>,
+  this: SupplementedLoaderContext<WebpackLoaderOptions>,
   sourceCode: WebpackLoaderParams[0],
   inputSourceMap: WebpackLoaderParams[1],
 ) {
@@ -59,7 +56,6 @@ function webpackLoader(
   try {
     result = transformSync(sourceCode, {
       filename: path.relative(process.cwd(), this.resourcePath),
-
       enableSourceMaps: this.sourceMap || false,
       inputSourceMap: parseSourceMap(inputSourceMap),
     });
@@ -70,9 +66,10 @@ function webpackLoader(
   if (result) {
     const resultCode = unstable_keepOriginalCode ? sourceCode : result.code;
     const resultSourceMap = unstable_keepOriginalCode ? inputSourceMap : result.sourceMap;
+    const { cssRulesByBucket } = result;
 
-    if (result.cssRulesByBucket) {
-      const entries = Object.entries(result.cssRulesByBucket);
+    if (cssRulesByBucket) {
+      const entries = Object.entries(cssRulesByBucket);
 
       if (entries.length === 0) {
         this.callback(null, resultCode, resultSourceMap);
@@ -109,7 +106,8 @@ function webpackLoader(
 
       const outputFileName = this.resourcePath.replace(/\.[^.]+$/, '.griffel.css');
 
-      const request = `${outputFileName}!=!${virtualLoaderPath}?style=${toURIComponent(css)}!${this.resourcePath}`;
+      this[GriffelCssLoaderContextKey]?.registerExtractedCss(css);
+      const request = `${outputFileName}!=!${virtualLoaderPath}!${this.resourcePath}`;
       const stringifiedRequest = JSON.stringify(this.utils.contextify(this.context || this.rootContext, request));
 
       this.callback(null, `${resultCode}\n\nimport ${stringifiedRequest};`, resultSourceMap);
diff --git a/packages/webpack-extraction-plugin/virtual-loader/index.js b/packages/webpack-extraction-plugin/virtual-loader/index.js
index 9b80d92..e3574ad 100644
--- a/packages/webpack-extraction-plugin/virtual-loader/index.js
+++ b/packages/webpack-extraction-plugin/virtual-loader/index.js
@@ -1,11 +1,11 @@
+const { GriffelCssLoaderContextKey } = require('../src/constants');
+
 /**
- * @this {import('webpack').LoaderContext<{ style?: string }>}
+ * @this {import("../src/constants").SupplementedLoaderContext}
  * @return {String}
  */
 function virtualLoader() {
-  const { style = '' } = this.getOptions();
-
-  return style;
+  return this[GriffelCssLoaderContextKey]?.getExtractedCss() ?? '';
 }
 
 module.exports = virtualLoader;


Test patch:
diff --git a/packages/babel-preset/src/transformPlugin.test.ts b/packages/babel-preset/src/transformPlugin.test.ts
index c038021..80e2625 100644
--- a/packages/babel-preset/src/transformPlugin.test.ts
+++ b/packages/babel-preset/src/transformPlugin.test.ts
@@ -1,3 +1,4 @@
+import * as Babel from '@babel/core';
 import pluginTester, { prettierFormatter } from 'babel-plugin-tester';
 import * as fs from 'fs';
 import * as path from 'path';
@@ -266,3 +267,80 @@ pluginTester({
   plugin: transformPlugin,
   pluginName: '@griffel/babel-plugin-transform',
 });
+
+// TODO use the plugin tester and all existing fixtures once there is support for that
+// https://github.com/babel-utils/babel-plugin-tester/issues/186
+describe('babel preset', () => {
+  it('should not generate metadata when not configured', () => {
+    const fixture = path.resolve(fixturesDir, 'object', 'code.ts');
+    const code = fs.readFileSync(fixture).toString();
+
+    const babelFileResult = Babel.transformSync(code, {
+      babelrc: false,
+      configFile: false,
+      plugins: [[transformPlugin]],
+      filename: fixture,
+      presets: ['@babel/typescript'],
+    });
+
+    expect(babelFileResult?.metadata).toMatchInlineSnapshot(`Object {}`);
+  });
+
+  it('should generate metadata for makeStyles when configured', () => {
+    const fixture = path.resolve(fixturesDir, 'object', 'code.ts');
+    const code = fs.readFileSync(fixture).toString();
+
+    const babelFileResult = Babel.transformSync(code, {
+      babelrc: false,
+      configFile: false,
+      plugins: [[transformPlugin, { generateMetadata: true }]],
+      filename: fixture,
+      presets: ['@babel/typescript'],
+    });
+
+    expect(babelFileResult?.metadata).toMatchInlineSnapshot(`
+      Object {
+        cssEntries: Object {
+          useStyles: Object {
+            icon: Array [
+              .fcnqdeg{background-color:green;},
+              .fjf1xye{margin-left:4px;},
+              .f8zmjen{margin-right:4px;},
+            ],
+            root: Array [
+              .fe3e8s9{color:red;},
+              .fycuoez{padding-left:4px;},
+              .f8wuabp{padding-right:4px;},
+            ],
+          },
+        },
+        cssResetEntries: Object {},
+      }
+    `);
+  });
+
+  it('should generate metadata for makeResetStyles when configured', () => {
+    const fixture = path.resolve(fixturesDir, 'reset-styles', 'code.ts');
+    const code = fs.readFileSync(fixture).toString();
+
+    const babelFileResult = Babel.transformSync(code, {
+      babelrc: false,
+      configFile: false,
+      plugins: [[transformPlugin, { generateMetadata: true }]],
+      filename: fixture,
+      presets: ['@babel/typescript'],
+    });
+
+    expect(babelFileResult?.metadata).toMatchInlineSnapshot(`
+      Object {
+        cssEntries: Object {},
+        cssResetEntries: Object {
+          useStyles: Array [
+            .rjefjbm{color:red;padding-left:4px;},
+            .r7z97ji{color:red;padding-right:4px;},
+          ],
+        },
+      }
+    `);
+  });
+});
diff --git a/packages/babel-preset/src/validateOptions.test.ts b/packages/babel-preset/src/validateOptions.test.ts
index e3769c0..fc7c594 100644
--- a/packages/babel-preset/src/validateOptions.test.ts
+++ b/packages/babel-preset/src/validateOptions.test.ts
@@ -35,7 +35,7 @@ describe('validateOptions', () => {
       expect(() => validateOptions(pluginOptions)).not.toThrow();
     });
 
-    it('throws on wrong options', () => {
+    it('throws when babelOptions is not valid', () => {
       const pluginOptions = { babelOptions: [] };
 
       // @ts-expect-error Invalid options are passed for testing purposes
@@ -43,6 +43,15 @@ describe('validateOptions', () => {
         `"Validation failed for passed config: data/babelOptions must be object"`,
       );
     });
+
+    it('throws when generateMetadata is not valid', () => {
+      const pluginOptions = { generateMetadata: 3 };
+
+      // @ts-expect-error Invalid options are passed for testing purposes
+      expect(() => validateOptions(pluginOptions)).toThrowErrorMatchingInlineSnapshot(
+        `"Validation failed for passed config: data/generateMetadata must be boolean"`,
+      );
+    });
   });
 
   describe('evaluationRules', () => {