Repository: microsoft/griffel
Pull request #361 (2023-04-04 21:15:31)

Add a rule to enforce makeStyles calls only happen in .styles.ts files
We've seen issues internally where our webpack bundle times have regressed when integrating the griffel CSS extraction plugins and loaders. We found that having all makeStyles in a .styles.ts file lets us limit the scope of where these plugins and loaders run to just files ending in .styles.ts which dramatically improved performance of our bundle tasks.

This code was heavily adapted from existing code by Yuanbo Xue in the Microsoft Teams repository. 

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

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

<!-- Sticky Pull Request Comment -->

Patch:
diff --git a/apps/website/.eslintrc.json b/apps/website/.eslintrc.json
index 9d9c0db..37562ce 100644
--- a/apps/website/.eslintrc.json
+++ b/apps/website/.eslintrc.json
@@ -4,7 +4,9 @@
   "overrides": [
     {
       "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
-      "rules": {}
+      "rules": {
+        "import/no-extraneous-dependencies": "off"
+      }
     },
     {
       "files": ["*.ts", "*.tsx"],
diff --git a/apps/website/docs/react/api/make-styles.md b/apps/website/docs/react/api/make-styles.md
index ab5773f..3c4888d 100644
--- a/apps/website/docs/react/api/make-styles.md
+++ b/apps/website/docs/react/api/make-styles.md
@@ -146,7 +146,7 @@ html[data-whatintent='mouse'] .f1xz3i88 {
 
 ### At-rules
 
-`@media`, `@supports` & `@layer` queries are also supported:
+`@media`, `@container`, `@supports` & `@layer` queries are also supported:
 
 ```js
 import { makeStyles } from '@griffel/react';
@@ -154,6 +154,8 @@ import { makeStyles } from '@griffel/react';
 const useClasses = makeStyles({
   root: {
     '@media screen and (max-width: 992px)': { color: 'orange' },
+    '@container (max-width: 992px)': { color: 'orange' },
+    '@container foo (max-width: 992px)': { color: 'orange' },
     '@supports (display: grid)': { color: 'red' },
     '@layer utility': { marginBottom: '1em' },
   },
@@ -168,6 +170,19 @@ const useClasses = makeStyles({
     color: orange;
   }
 }
+
+@container (max-width: 992px) {
+  .xx {
+    color: orange;
+  }
+}
+
+@container foo (max-width: 992px) {
+  .xx {
+    color: orange;
+  }
+}
+
 @supports (display: grid) {
   .f1ofq0jl {
     color: red;
diff --git a/apps/website/src/components/Playground/code/templates/animation.js b/apps/website/src/components/Playground/code/templates/animation.js
new file mode 100644
index 0000000..645a067
--- /dev/null
+++ b/apps/website/src/components/Playground/code/templates/animation.js
@@ -0,0 +1,25 @@
+import { makeStyles } from '@griffel/core';
+
+export default makeStyles({
+  spinInACircle: {
+    animationPlayState: 'running',
+    animationDelay: '5s',
+    animationTimingFunction: 'cubic-bezier(0.1, 0.5, 0.1, 0.5)',
+    animationDirection: 'normal',
+    animationDuration: '5s',
+    animationIterationCount: 'infinite',
+    animationName: {
+      from: {
+        transform: 'rotate(0deg)',
+      },
+      to: {
+        transform: 'rotate(360deg)',
+      },
+    },
+  },
+});
+
+export const meta = {
+  name: 'Animations',
+  position: 12,
+};
diff --git a/apps/website/src/components/Playground/code/templates/container.js b/apps/website/src/components/Playground/code/templates/container.js
new file mode 100644
index 0000000..6d0a156
--- /dev/null
+++ b/apps/website/src/components/Playground/code/templates/container.js
@@ -0,0 +1,20 @@
+import { makeStyles, shorthands } from '@griffel/core';
+
+export default makeStyles({
+  root: {
+    '@container (max-width: 468px)': {
+      ...shorthands.gap('1.5rem'),
+      display: 'grid',
+    },
+
+    '@container foo (max-width: 468px)': {
+      ...shorthands.gap('1.5rem'),
+      display: 'grid',
+    },
+  },
+});
+
+export const meta = {
+  name: 'Container queries',
+  position: 11,
+};
diff --git a/change/@griffel-core-6c589809-d7d3-4888-a5fa-c7cc3df8e097.json b/change/@griffel-core-6c589809-d7d3-4888-a5fa-c7cc3df8e097.json
new file mode 100644
index 0000000..91bdf1e
--- /dev/null
+++ b/change/@griffel-core-6c589809-d7d3-4888-a5fa-c7cc3df8e097.json
@@ -0,0 +1,7 @@
+{
+  "type": "minor",
+  "comment": "feat: Support CSS container queries",
+  "packageName": "@griffel/core",
+  "email": "lingfangao@hotmail.com",
+  "dependentChangeType": "patch"
+}
diff --git a/change/@griffel-core-d4f54abc-a7c5-45ea-831f-679248cd7d3c.json b/change/@griffel-core-d4f54abc-a7c5-45ea-831f-679248cd7d3c.json
new file mode 100644
index 0000000..3a0b7a2
--- /dev/null
+++ b/change/@griffel-core-d4f54abc-a7c5-45ea-831f-679248cd7d3c.json
@@ -0,0 +1,7 @@
+{
+  "type": "patch",
+  "comment": "chore(core): hide warnings in Node (part 2)",
+  "packageName": "@griffel/core",
+  "email": "olfedias@microsoft.com",
+  "dependentChangeType": "patch"
+}
diff --git a/change/@griffel-eslint-plugin-a3b29662-7937-4c57-8d41-9ce578721b9f.json b/change/@griffel-eslint-plugin-a3b29662-7937-4c57-8d41-9ce578721b9f.json
new file mode 100644
index 0000000..6cf428d
--- /dev/null
+++ b/change/@griffel-eslint-plugin-a3b29662-7937-4c57-8d41-9ce578721b9f.json
@@ -0,0 +1,7 @@
+{
+  "type": "minor",
+  "comment": "Added a styles-file rule for enforcing makeStyles call be only made in files that end in .styles.ts",
+  "packageName": "@griffel/eslint-plugin",
+  "email": "1581488+christiango@users.noreply.github.com",
+  "dependentChangeType": "patch"
+}
diff --git a/packages/core/src/common/snapshotSerializers.ts b/packages/core/src/common/snapshotSerializers.ts
index 156e2e1..14856ee 100644
--- a/packages/core/src/common/snapshotSerializers.ts
+++ b/packages/core/src/common/snapshotSerializers.ts
@@ -19,7 +19,7 @@ export const griffelRendererSerializer: jest.SnapshotSerializerPlugin = {
      */
     const _value = value as GriffelRenderer;
 
-    const stylesheetKeys = Object.keys(_value.stylesheets) as (keyof typeof _value['stylesheets'])[];
+    const stylesheetKeys = Object.keys(_value.stylesheets) as (keyof (typeof _value)['stylesheets'])[];
 
     const rules = stylesheetKeys.reduce((acc, styleEl) => {
       const stylesheet = _value.stylesheets[styleEl];
@@ -60,3 +60,17 @@ export const griffelRulesSerializer: jest.SnapshotSerializerPlugin = {
     }, '');
   },
 };
+
+export const griffelResetRulesSerializer: jest.SnapshotSerializerPlugin = {
+  test(value) {
+    return Array.isArray(value);
+  },
+  print(value) {
+    /**
+     * test function makes sure that value is the guarded type
+     */
+    const _value = value as [string, string | null, string[]];
+
+    return prettier.format(_value[2].join(''), { parser: 'css' }).trim();
+  },
+};
diff --git a/packages/core/src/renderer/getStyleSheetForBucket.ts b/packages/core/src/renderer/getStyleSheetForBucket.ts
index 1b104eb..bfa18f4 100644
--- a/packages/core/src/renderer/getStyleSheetForBucket.ts
+++ b/packages/core/src/renderer/getStyleSheetForBucket.ts
@@ -32,6 +32,8 @@ export const styleBucketOrdering: StyleBucketName[] = [
   't',
   // @media rules
   'm',
+  // @container rules
+  'c',
 ];
 
 // avoid repeatedly calling `indexOf`to determine order during new insertions
diff --git a/packages/core/src/runtime/compileCSS.ts b/packages/core/src/runtime/compileCSS.ts
index 49a5e1d..ab33106 100644
--- a/packages/core/src/runtime/compileCSS.ts
+++ b/packages/core/src/runtime/compileCSS.ts
@@ -11,6 +11,7 @@ export interface CompileCSSOptions {
   media: string;
   layer: string;
   support: string;
+  container: string;
 
   property: string;
   value: number | string | Array<number | string>;
@@ -75,7 +76,19 @@ function createCSSRule(classNameSelector: string, cssDeclaration: string, pseudo
 }
 
 export function compileCSS(options: CompileCSSOptions): [string? /* ltr definition */, string? /* rtl definition */] {
-  const { className, media, layer, selectors, support, property, rtlClassName, rtlProperty, rtlValue, value } = options;
+  const {
+    className,
+    media,
+    layer,
+    selectors,
+    support,
+    property,
+    rtlClassName,
+    rtlProperty,
+    rtlValue,
+    value,
+    container,
+  } = options;
 
   const classNameSelector = `.${className}`;
   const cssDeclaration = Array.isArray(value)
@@ -105,5 +118,9 @@ export function compileCSS(options: CompileCSSOptions): [string? /* ltr definiti
     cssRule = `@supports ${support} { ${cssRule} }`;
   }
 
+  if (container) {
+    cssRule = `@container ${container} { ${cssRule} }`;
+  }
+
   return compileCSSRules(cssRule) as [string?, string?];
 }
diff --git a/packages/core/src/runtime/getStyleBucketName.ts b/packages/core/src/runtime/getStyleBucketName.ts
index 333de85..3f3eb51 100644
--- a/packages/core/src/runtime/getStyleBucketName.ts
+++ b/packages/core/src/runtime/getStyleBucketName.ts
@@ -47,6 +47,7 @@ export function getStyleBucketName(
   layer: string,
   media: string,
   support: string,
+  container: string,
 ): StyleBucketName {
   if (media) {
     return 'm';
@@ -57,6 +58,10 @@ export function getStyleBucketName(
     return 't';
   }
 
+  if (container) {
+    return 'c';
+  }
+
   if (selectors.length > 0) {
     const normalizedPseudo = selectors[0].trim();
 
diff --git a/packages/core/src/runtime/resolveResetStyleRules.ts b/packages/core/src/runtime/resolveResetStyleRules.ts
index 51dc913..aef486e 100644
--- a/packages/core/src/runtime/resolveResetStyleRules.ts
+++ b/packages/core/src/runtime/resolveResetStyleRules.ts
@@ -11,7 +11,8 @@ import { isObject } from './utils/isObject';
 import { hyphenateProperty } from './utils/hyphenateProperty';
 import { compileCSSRules, normalizePseudoSelector } from './compileCSS';
 import { compileKeyframeRule, compileKeyframesCSS } from './compileKeyframeCSS';
-import { warnAboutUnresolvedRule } from './warnAboutUnresolvedRule';
+import { isContainerQuerySelector } from './utils/isContainerQuerySelector';
+import { warnAboutUnresolvedRule } from './warnings/warnAboutUnresolvedRule';
 
 /**
  * @internal
@@ -109,7 +110,12 @@ function createStringFromStyles(styles: GriffelResetStyle) {
         continue;
       }
 
-      if (isMediaQuerySelector(property) || isLayerSelector(property) || isSupportQuerySelector(property)) {
+      if (
+        isMediaQuerySelector(property) ||
+        isLayerSelector(property) ||
+        isSupportQuerySelector(property) ||
+        isContainerQuerySelector(property)
+      ) {
         const [ltrNested, rtlNested] = createStringFromStyles(value);
 
         ltrCSS += `${property}{${ltrNested}}`;
diff --git a/packages/core/src/runtime/resolveStyleRules.ts b/packages/core/src/runtime/resolveStyleRules.ts
index 17d4612..6d191f8 100644
--- a/packages/core/src/runtime/resolveStyleRules.ts
+++ b/packages/core/src/runtime/resolveStyleRules.ts
@@ -17,12 +17,14 @@ import { isMediaQuerySelector } from './utils/isMediaQuerySelector';
 import { isLayerSelector } from './utils/isLayerSelector';
 import { isNestedSelector } from './utils/isNestedSelector';
 import { isSupportQuerySelector } from './utils/isSupportQuerySelector';
+import { isContainerQuerySelector } from './utils/isContainerQuerySelector';
 import { normalizeNestedProperty } from './utils/normalizeNestedProperty';
 import { isObject } from './utils/isObject';
 import { getStyleBucketName } from './getStyleBucketName';
 import { hashClassName } from './utils/hashClassName';
 import { hashPropertyKey } from './utils/hashPropertyKey';
-import { warnAboutUnresolvedRule } from './warnAboutUnresolvedRule';
+import { warnAboutUnresolvedRule } from './warnings/warnAboutUnresolvedRule';
+import { warnAboutUnsupportedProperties } from './warnings/warnAboutUnsupportedProperties';
 
 function pushToClassesMap(
   classesMap: CSSClassesMap,
@@ -75,6 +77,7 @@ export function resolveStyleRules(
   media = '',
   layer = '',
   support = '',
+  container = '',
   cssClassesMap: CSSClassesMap = {},
   cssRulesByBucket: CSSRulesByBucket = {},
   rtlValue?: string,
@@ -83,19 +86,7 @@ export function resolveStyleRules(
   for (const property in styles) {
     // eslint-disable-next-line no-prototype-builtins
     if (UNSUPPORTED_CSS_PROPERTIES.hasOwnProperty(property)) {
-      if (process.env.NODE_ENV !== 'production') {
-        console.error(
-          [
-            `@griffel/react: You are using unsupported shorthand CSS property "${property}". ` +
-              `Please check your "makeStyles" calls, there *should not* be following:`,
-            ' '.repeat(2) + `makeStyles({`,
-            ' '.repeat(4) + `[slot]: { ${property}: "${styles[property as keyof GriffelStyle]}" }`,
-            ' '.repeat(2) + `})`,
-            '',
-            'Learn why CSS shorthands are not supported: https://aka.ms/griffel-css-shorthands',
-          ].join('\n'),
-        );
-      }
+      warnAboutUnsupportedProperties(property, styles[property as keyof GriffelStyle]);
       continue;
     }
 
@@ -139,7 +130,7 @@ export function resolveStyleRules(
           }
         : undefined;
 
-      const styleBucketName = getStyleBucketName(selectors, layer, media, support);
+      const styleBucketName = getStyleBucketName(selectors, layer, media, support, container);
       const [ltrCSS, rtlCSS] = compileCSS({
         className,
         media,
@@ -147,6 +138,7 @@ export function resolveStyleRules(
         selectors,
         property,
         support,
+        container,
         value,
         ...rtlCompileOptions,
       });
@@ -198,6 +190,7 @@ export function resolveStyleRules(
         media,
         layer,
         support,
+        container,
         cssClassesMap,
         cssRulesByBucket,
         rtlAnimationNames.join(', '),
@@ -257,7 +250,7 @@ export function resolveStyleRules(
           }
         : undefined;
 
-      const styleBucketName = getStyleBucketName(selectors, layer, media, support);
+      const styleBucketName = getStyleBucketName(selectors, layer, media, support, container);
       const [ltrCSS, rtlCSS] = compileCSS({
         className,
         media,
@@ -265,6 +258,7 @@ export function resolveStyleRules(
         selectors,
         property,
         support,
+        container,
         value: value as Array<string | number>,
         ...rtlCompileOptions,
       });
@@ -279,6 +273,7 @@ export function resolveStyleRules(
           media,
           layer,
           support,
+          container,
           cssClassesMap,
           cssRulesByBucket,
         );
@@ -291,6 +286,7 @@ export function resolveStyleRules(
           combinedMediaQuery,
           layer,
           support,
+          container,
           cssClassesMap,
           cssRulesByBucket,
         );
@@ -303,6 +299,7 @@ export function resolveStyleRules(
           media,
           combinedLayerQuery,
           support,
+          container,
           cssClassesMap,
           cssRulesByBucket,
         );
@@ -315,6 +312,22 @@ export function resolveStyleRules(
           media,
           layer,
           combinedSupportQuery,
+          container,
+          cssClassesMap,
+          cssRulesByBucket,
+        );
+      } else if (isContainerQuerySelector(property)) {
+        // TODO implement nested container queries if needed
+        // The only way to target multiple containers is to nest container queries
+        // https://developer.mozilla.org/en-US/docs/Web/CSS/@container#nested_container_queries
+        const containerQuery = property.slice(10).trim();
+        resolveStyleRules(
+          value as GriffelStyle,
+          selectors,
+          media,
+          layer,
+          support,
+          containerQuery,
           cssClassesMap,
           cssRulesByBucket,
         );
diff --git a/packages/core/src/runtime/utils/isContainerQuerySelector.ts b/packages/core/src/runtime/utils/isContainerQuerySelector.ts
new file mode 100644
index 0000000..26ceecc
--- /dev/null
+++ b/packages/core/src/runtime/utils/isContainerQuerySelector.ts
@@ -0,0 +1,3 @@
+export function isContainerQuerySelector(property: string): boolean {
+  return property.substring(0, 10) === '@container';
+}
diff --git a/packages/core/src/runtime/warnings/logError.ts b/packages/core/src/runtime/warnings/logError.ts
new file mode 100644
index 0000000..fff72ec
--- /dev/null
+++ b/packages/core/src/runtime/warnings/logError.ts
@@ -0,0 +1,5 @@
+export function logError(...args: any[]): void {
+  if (process.env.NODE_ENV !== 'production' && typeof document !== 'undefined') {
+    console.error(...args);
+  }
+}
diff --git a/packages/core/src/runtime/warnAboutUnresolvedRule.ts b/packages/core/src/runtime/warnings/warnAboutUnresolvedRule.ts
similarity index 82%
rename from packages/core/src/runtime/warnAboutUnresolvedRule.ts
rename to packages/core/src/runtime/warnings/warnAboutUnresolvedRule.ts
index 45adc51..148e063 100644
--- a/packages/core/src/runtime/warnAboutUnresolvedRule.ts
+++ b/packages/core/src/runtime/warnings/warnAboutUnresolvedRule.ts
@@ -1,10 +1,7 @@
-import type { GriffelResetStyle, GriffelStyle } from '../types';
+import type { GriffelResetStyle, GriffelStyle } from '../../types';
+import { logError } from './logError';
 
 export function warnAboutUnresolvedRule(property: string, value: GriffelStyle | GriffelResetStyle) {
-  if (process.env.NODE_ENV === 'production' || typeof document === 'undefined') {
-    return;
-  }
-
   const ruleText = JSON.stringify(value, null, 2);
   const message: string[] = [
     '@griffel/react: A rule was not resolved to CSS properly. ' +
@@ -33,6 +30,5 @@ export function warnAboutUnresolvedRule(property: string, value: GriffelStyle |
     );
   }
 
-  // eslint-disable-next-line no-console
-  console.error(message.join('\n'));
+  logError(message.join('\n'));
 }
diff --git a/packages/core/src/runtime/warnings/warnAboutUnsupportedProperties.ts b/packages/core/src/runtime/warnings/warnAboutUnsupportedProperties.ts
new file mode 100644
index 0000000..1df0cf9
--- /dev/null
+++ b/packages/core/src/runtime/warnings/warnAboutUnsupportedProperties.ts
@@ -0,0 +1,16 @@
+import type { GriffelStyle } from '../../types';
+import { logError } from './logError';
+
+export function warnAboutUnsupportedProperties(property: string, value: GriffelStyle[keyof GriffelStyle]) {
+  logError(
+    [
+      `@griffel/react: You are using unsupported shorthand CSS property "${property}". ` +
+        `Please check your "makeStyles" calls, there *should not* be following:`,
+      ' '.repeat(2) + `makeStyles({`,
+      ' '.repeat(4) + `[slot]: { ${property}: "${value}" }`,
+      ' '.repeat(2) + `})`,
+      '',
+      'Learn why CSS shorthands are not supported: https://aka.ms/griffel-css-shorthands',
+    ].join('\n'),
+  );
+}
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index a4513c7..39fa928 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -319,6 +319,8 @@ export type CSSRulesByBucket = {
   t?: CSSBucketEntry[];
   // @media rules
   m?: CSSBucketEntry[];
+  // @container rules
+  c?: CSSBucketEntry[];
 };
 
 /** @internal */
diff --git a/packages/eslint-plugin/src/index.ts b/packages/eslint-plugin/src/index.ts
index e1f8761..3606248 100644
--- a/packages/eslint-plugin/src/index.ts
+++ b/packages/eslint-plugin/src/index.ts
@@ -1,6 +1,7 @@
 import { recommendedConfig } from './configs/recommended';
 import { hookNamingRule } from './rules/hook-naming';
 import { noShorthandsRule } from './rules/no-shorthands';
+import { stylesFileRule } from './rules/styles-file';
 
 export = {
   configs: {
@@ -9,5 +10,6 @@ export = {
   rules: {
     'hook-naming': hookNamingRule,
     'no-shorthands': noShorthandsRule,
+    'styles-file': stylesFileRule,
   },
 };
diff --git a/packages/eslint-plugin/src/rules/styles-file.md b/packages/eslint-plugin/src/rules/styles-file.md
new file mode 100644
index 0000000..1b11fa8
--- /dev/null
+++ b/packages/eslint-plugin/src/rules/styles-file.md
@@ -0,0 +1,33 @@
+# Enforce that `makeStyles()` calls are placed in a .styles.ts file (`@griffel/styles files`)
+
+Ensures that all `makeStyles()` and `makeResetStyles()` calls are placed in a `.styles.js` or `.styles.ts` file. The primary motivation for enabling rules is to reduce the overhead of Griffel CSS extraction build tools. With this rule enabled, your repo could be configured so the Griffel babel and webpack build tools only look at `.styles.ts` and `.styles.js` files rather than all .ts and .js files.
+
+## Rule Details
+
+`makeStyles()` calls should only exist inside `.styles.js` or `.styles.ts` files.
+
+Examples of **incorrect** code for this rule:
+
+```js
+// filename: component.tsx
+import { makeStyles } from '@griffel/react';
+
+export const useStyles = makeStyles({
+  root: {
+    backgroundColor: 'red',
+  },
+});
+```
+
+Examples of **correct** code for this rule:
+
+```js
+// filename: component.styles.ts
+import { makeStyles } from '@griffel/react';
+
+export const useStyles = makeStyles({
+  root: {
+    backgroundColor: 'red',
+  },
+});
+```
diff --git a/packages/eslint-plugin/src/rules/styles-file.ts b/packages/eslint-plugin/src/rules/styles-file.ts
new file mode 100644
index 0000000..38f2957
--- /dev/null
+++ b/packages/eslint-plugin/src/rules/styles-file.ts
@@ -0,0 +1,107 @@
+import { createRule } from '../utils/createRule';
+import { ESLintUtils, TSESTree } from '@typescript-eslint/utils';
+
+const MATCHING_PACKAGES = new Set(['@fluentui/react-components', '@griffel/core', '@griffel/react']);
+const STYLES_FILE_PATTERN = /^.*\.(styles)\.[j|t]s$/;
+
+function isMatchingPackage(packageName: string) {
+  return MATCHING_PACKAGES.has(packageName);
+}
+
+function isStylesFile(fileName: string) {
+  return STYLES_FILE_PATTERN.test(fileName);
+}
+
+/**
+ * @param node - import node from AST
+ * @returns true if makeStyles is imported, or if the entire library is imported. Otherwise returns false
+ */
+function isMakeStylesImport(node: TSESTree.ImportDeclaration) {
+  return (
+    isMatchingPackage(node.source.value) &&
+    node.specifiers.filter(specifier => {
+      // import * as ... from
+      if (specifier.type === 'ImportNamespaceSpecifier') {
+        return true;
+      }
+
+      if ('imported' in specifier) {
+        return (
+          specifier.imported.name === 'makeStyles' || // import { makeStyles } from
+          specifier.imported.name === 'makeResetStyles' // import { makeResetStyles } from
+        );
+      }
+
+      return false;
+    }).length > 0
+  );
+}
+
+/**
+ * @param callExpression CallExpression node in AST
+ * @returns if this is a makeStyles call or not
+ */
+function isMakeStyleCallExpression({ callee }: TSESTree.CallExpression) {
+  return (
+    ('name' in callee && callee.name === 'makeStyles') || // makeStyles({})
+    ('property' in callee && 'name' in callee.property && callee.property.name === 'makeStyles') // something.makeStyles({})
+  );
+}
+
+export const RULE_NAME = 'styles-file';
+
+export const stylesFileRule: ReturnType<ReturnType<typeof ESLintUtils.RuleCreator>> = createRule({
+  name: RULE_NAME,
+  meta: {
+    docs: {
+      description: 'Enforce that hooks returned from makeStyles() calls are placed in a .styles.ts file',
+      recommended: 'error',
+    },
+    messages: {
+      foundMakeStylesUsage:
+        'Styles created from `makeStyles` should be defined in a .styles.ts file so they can be extracted into static CSS',
+    },
+    type: 'problem',
+    schema: [],
+  },
+  defaultOptions: [],
+  create(context) {
+    const fileName = context.getFilename();
+
+    let isMakeStylesImported = false;
+    const makeStylesDeclarations = []; // contains constants from makeStyles calls: `const useStyles = makeStyles({})`
+
+    return {
+      ImportDeclaration(node) {
+        if (!isMakeStylesImported) {
+          isMakeStylesImported = isMakeStylesImport(node);
+        }
+      },
+      CallExpression(node) {
+        if (isMakeStylesImported && isMakeStyleCallExpression(node)) {
+          if (!isStylesFile(fileName)) {
+            context.report({
+              messageId: 'foundMakeStylesUsage',
+              node,
+            });
+          }
+        }
+      },
+      VariableDeclaration(node) {
+        if (isStylesFile(fileName)) {
+          node.declarations?.forEach(declaration => {
+            if (
+              isMakeStylesImported &&
+              declaration.init?.type === 'CallExpression' &&
+              isMakeStyleCallExpression(declaration.init)
+            ) {
+              if (declaration.id.type === 'Identifier') {
+                makeStylesDeclarations.push(declaration.id.name);
+              }
+            }
+          });
+        }
+      },
+    };
+  },
+});
diff --git a/packages/react/README.md b/packages/react/README.md
index a6772da..9ca0b0f 100644
--- a/packages/react/README.md
+++ b/packages/react/README.md
@@ -85,6 +85,8 @@ const useClasses = makeStyles({
     ':nth-child(2n)': { backgroundColor: '#fafafa' },
 
     '@media screen and (max-width: 992px)': { color: 'orange' },
+    '@container (max-width: 992px)': { color: 'orange' },
+    '@container foo (max-width: 992px)': { color: 'orange' },
     '@supports (display: grid)': { color: 'red' },
     '@layer utility': { marginBottom: '1em' },
   },


Test patch:
diff --git a/packages/core/src/renderer/getStyleSheetForBucket.test.ts b/packages/core/src/renderer/getStyleSheetForBucket.test.ts
index abf4186..9afcc26 100644
--- a/packages/core/src/renderer/getStyleSheetForBucket.test.ts
+++ b/packages/core/src/renderer/getStyleSheetForBucket.test.ts
@@ -46,6 +46,7 @@ describe('getStyleSheetForBucket', () => {
     getStyleSheetForBucket('d', target, null, renderer);
     getStyleSheetForBucket('v', target, null, renderer);
     getStyleSheetForBucket('a', target, null, renderer);
+    getStyleSheetForBucket('c', target, null, renderer);
     getStyleSheetForBucket('i', target, null, renderer);
     getStyleSheetForBucket('h', target, null, renderer);
     getStyleSheetForBucket('m', target, null, renderer);
diff --git a/packages/core/src/runtime/compileCSS.test.ts b/packages/core/src/runtime/compileCSS.test.ts
index dffa52a..50b6011 100644
--- a/packages/core/src/runtime/compileCSS.test.ts
+++ b/packages/core/src/runtime/compileCSS.test.ts
@@ -2,7 +2,7 @@ import { compileCSS, CompileCSSOptions, normalizePseudoSelector } from './compil
 
 const defaultOptions: Pick<
   CompileCSSOptions,
-  'rtlClassName' | 'className' | 'media' | 'selectors' | 'support' | 'layer'
+  'rtlClassName' | 'className' | 'media' | 'selectors' | 'support' | 'layer' | 'container'
 > = {
   className: 'foo',
   rtlClassName: 'rtl-foo',
@@ -10,6 +10,7 @@ const defaultOptions: Pick<
   selectors: [],
   support: '',
   layer: '',
+  container: '',
 };
 
 describe('compileCSS', () => {
diff --git a/packages/core/src/runtime/getStyleBucketName.test.ts b/packages/core/src/runtime/getStyleBucketName.test.ts
index becade1..f9bd051 100644
--- a/packages/core/src/runtime/getStyleBucketName.test.ts
+++ b/packages/core/src/runtime/getStyleBucketName.test.ts
@@ -2,22 +2,23 @@ import { getStyleBucketName } from './getStyleBucketName';
 
 describe('getStyleBucketName', () => {
   it('returns bucketName based on mapping', () => {
-    expect(getStyleBucketName([], '', '', '')).toBe('d');
-    expect(getStyleBucketName(['.foo'], '', '', '')).toBe('d');
+    expect(getStyleBucketName([], '', '', '', '')).toBe('d');
+    expect(getStyleBucketName(['.foo'], '', '', '', '')).toBe('d');
 
-    expect(getStyleBucketName([' :link'], '', '', '')).toBe('l');
-    expect(getStyleBucketName([' :hover '], '', '', '')).toBe('h');
+    expect(getStyleBucketName([' :link'], '', '', '', '')).toBe('l');
+    expect(getStyleBucketName([' :hover '], '', '', '', '')).toBe('h');
 
-    expect(getStyleBucketName([':link'], '', '', '')).toBe('l');
-    expect(getStyleBucketName([':visited'], '', '', '')).toBe('v');
-    expect(getStyleBucketName([':focus-within'], '', '', '')).toBe('w');
-    expect(getStyleBucketName([':focus'], '', '', '')).toBe('f');
-    expect(getStyleBucketName([':focus-visible'], '', '', '')).toBe('i');
-    expect(getStyleBucketName([':hover'], '', '', '')).toBe('h');
-    expect(getStyleBucketName([':active'], '', '', '')).toBe('a');
+    expect(getStyleBucketName([':link'], '', '', '', '')).toBe('l');
+    expect(getStyleBucketName([':visited'], '', '', '', '')).toBe('v');
+    expect(getStyleBucketName([':focus-within'], '', '', '', '')).toBe('w');
+    expect(getStyleBucketName([':focus'], '', '', '', '')).toBe('f');
+    expect(getStyleBucketName([':focus-visible'], '', '', '', '')).toBe('i');
+    expect(getStyleBucketName([':hover'], '', '', '', '')).toBe('h');
+    expect(getStyleBucketName([':active'], '', '', '', '')).toBe('a');
 
-    expect(getStyleBucketName([':active'], 'theme', '', '')).toBe('t');
-    expect(getStyleBucketName([':active'], '', '(max-width: 100px)', '')).toBe('m');
-    expect(getStyleBucketName([':active'], '', '', '(display: table-cell)')).toBe('t');
+    expect(getStyleBucketName([':active'], 'theme', '', '', '')).toBe('t');
+    expect(getStyleBucketName([':active'], '', '(max-width: 100px)', '', '')).toBe('m');
+    expect(getStyleBucketName([':active'], '', '', '(display: table-cell)', '')).toBe('t');
+    expect(getStyleBucketName([':active'], '', '', '', 'foo (max-width: 1px)')).toBe('c');
   });
 });
diff --git a/packages/core/src/runtime/resolveResetStyleRules.test.ts b/packages/core/src/runtime/resolveResetStyleRules.test.ts
index e86b2b4..b2899de 100644
--- a/packages/core/src/runtime/resolveResetStyleRules.test.ts
+++ b/packages/core/src/runtime/resolveResetStyleRules.test.ts
@@ -1,19 +1,7 @@
-import * as prettier from 'prettier';
 import { resolveResetStyleRules } from './resolveResetStyleRules';
+import { griffelResetRulesSerializer } from '../common/snapshotSerializers';
 
-expect.addSnapshotSerializer({
-  test(value) {
-    return Array.isArray(value);
-  },
-  print(value) {
-    /**
-     * test function makes sure that value is the guarded type
-     */
-    const _value = value as ReturnType<typeof resolveResetStyleRules>;
-
-    return prettier.format(_value[2].join(''), { parser: 'css' }).trim();
-  },
-});
+expect.addSnapshotSerializer(griffelResetRulesSerializer);
 
 describe('resolveResetStyleRules', () => {
   it('handles base rules', () => {
@@ -100,6 +88,38 @@ describe('resolveResetStyleRules', () => {
     `);
   });
 
+  it('handles named container queries', () => {
+    const result = resolveResetStyleRules({
+      '@container foo (max-width: 1px)': {
+        color: 'orange',
+      },
+    });
+
+    expect(result).toMatchInlineSnapshot(`
+      @container foo (max-width: 1px) {
+        .rmph5rz {
+          color: orange;
+        }
+      }
+    `);
+  });
+
+  it('handles unnamed container queries', () => {
+    const result = resolveResetStyleRules({
+      '@container (max-width: 1px)': {
+        color: 'orange',
+      },
+    });
+
+    expect(result).toMatchInlineSnapshot(`
+      @container (max-width: 1px) {
+        .r1ph1abo {
+          color: orange;
+        }
+      }
+    `);
+  });
+
   it('handles media queries', () => {
     const result = resolveResetStyleRules({
       color: 'red',
diff --git a/packages/core/src/runtime/resolveStyleRules.test.ts b/packages/core/src/runtime/resolveStyleRules.test.ts
index cb97bd0..c40bac7 100644
--- a/packages/core/src/runtime/resolveStyleRules.test.ts
+++ b/packages/core/src/runtime/resolveStyleRules.test.ts
@@ -435,6 +435,32 @@ describe('resolveStyleRules', () => {
       `);
     });
 
+    it('handles named container queries', () => {
+      const result = resolveStyleRules({ '@container foo (max-width: 1px)': { color: 'red' } });
+      expect(result).toMatchInlineSnapshot(`
+        @container foo (max-width: 1px) {
+          .fe3e8s9 {
+            color: red;
+          }
+        }
+      `);
+
+      expect(Object.keys(result[1])).toEqual(['c']);
+    });
+
+    it('handles unnamed container queries', () => {
+      const result = resolveStyleRules({ '@container (max-width: 1px)': { color: 'red' } });
+      expect(result).toMatchInlineSnapshot(`
+        @container (max-width: 1px) {
+          .fe3e8s9 {
+            color: red;
+          }
+        }
+      `);
+
+      expect(Object.keys(result[1])).toEqual(['c']);
+    });
+
     it('handles media queries', () => {
       expect(
         resolveStyleRules({
diff --git a/packages/core/src/runtime/warnAboutUnresolvedRule-node.test.ts b/packages/core/src/runtime/warnAboutUnresolvedRule-node.test.ts
deleted file mode 100644
index 8ec9e13..0000000
--- a/packages/core/src/runtime/warnAboutUnresolvedRule-node.test.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
- * @jest-environment node
- */
-
-import { warnAboutUnresolvedRule } from './warnAboutUnresolvedRule';
-
-describe('warnAboutUnresolvedRule', () => {
-  it('does not warn in Node', () => {
-    const spy = jest.spyOn(console, 'error').mockImplementation();
-
-    warnAboutUnresolvedRule('div', {
-      color: 'red',
-      ':hover': { color: 'blue' },
-    });
-
-    expect(spy).not.toBeCalled();
-  });
-});
diff --git a/packages/core/src/runtime/warnings/logError-node.test.ts b/packages/core/src/runtime/warnings/logError-node.test.ts
new file mode 100644
index 0000000..ceb73ea
--- /dev/null
+++ b/packages/core/src/runtime/warnings/logError-node.test.ts
@@ -0,0 +1,14 @@
+/*
+ * @jest-environment node
+ */
+
+import { logError } from './logError';
+
+describe('logError', () => {
+  it('does not log in Node', () => {
+    const spy = jest.spyOn(console, 'error').mockImplementation();
+
+    logError('An error occurred');
+    expect(spy).not.toBeCalled();
+  });
+});
diff --git a/packages/core/src/runtime/warnings/logError.test.ts b/packages/core/src/runtime/warnings/logError.test.ts
new file mode 100644
index 0000000..6ee4790
--- /dev/null
+++ b/packages/core/src/runtime/warnings/logError.test.ts
@@ -0,0 +1,11 @@
+import { logError } from './logError';
+
+describe('logError', () => {
+  it('logs in browser env', () => {
+    const spy = jest.spyOn(console, 'error').mockImplementation();
+
+    logError('An error occurred');
+    expect(spy).toHaveBeenCalledTimes(1);
+    expect(spy).toHaveBeenCalledWith('An error occurred');
+  });
+});
diff --git a/packages/core/src/runtime/warnAboutUnresolvedRule.test.ts b/packages/core/src/runtime/warnings/warnAboutUnresolvedRule.test.ts
similarity index 83%
rename from packages/core/src/runtime/warnAboutUnresolvedRule.test.ts
rename to packages/core/src/runtime/warnings/warnAboutUnresolvedRule.test.ts
index 86926e4..991d04f 100644
--- a/packages/core/src/runtime/warnAboutUnresolvedRule.test.ts
+++ b/packages/core/src/runtime/warnings/warnAboutUnresolvedRule.test.ts
@@ -1,15 +1,16 @@
 import { warnAboutUnresolvedRule } from './warnAboutUnresolvedRule';
+import { logError } from './logError';
+
+jest.mock('./logError', () => ({ logError: jest.fn() }));
 
 describe('warnAboutUnresolvedRule', () => {
   it('warns on a missing "&"', () => {
-    const spy = jest.spyOn(console, 'error').mockImplementation();
-
     warnAboutUnresolvedRule('div', {
       color: 'red',
       ':hover': { color: 'blue' },
     });
 
-    expect(spy.mock.calls[0][0]).toMatchInlineSnapshot(`
+    expect((logError as jest.Mock).mock.calls[0][0]).toMatchInlineSnapshot(`
       "@griffel/react: A rule was not resolved to CSS properly. Please check your \`makeStyles\` or \`makeResetStyles\` calls for following:
         makeStyles({
           [slot]: {
diff --git a/packages/core/src/runtime/warnings/warnAboutUnsupportedProperties.test.ts b/packages/core/src/runtime/warnings/warnAboutUnsupportedProperties.test.ts
new file mode 100644
index 0000000..c73541c
--- /dev/null
+++ b/packages/core/src/runtime/warnings/warnAboutUnsupportedProperties.test.ts
@@ -0,0 +1,17 @@
+import { warnAboutUnsupportedProperties } from './warnAboutUnsupportedProperties';
+import { logError } from './logError';
+
+jest.mock('./logError', () => ({ logError: jest.fn() }));
+
+it('warnAboutUnresolvedRule', () => {
+  warnAboutUnsupportedProperties('flex', 0);
+
+  expect((logError as jest.Mock).mock.calls[0][0]).toMatchInlineSnapshot(`
+      "@griffel/react: You are using unsupported shorthand CSS property \\"flex\\". Please check your \\"makeStyles\\" calls, there *should not* be following:
+        makeStyles({
+          [slot]: { flex: \\"0\\" }
+        })
+
+      Learn why CSS shorthands are not supported: https://aka.ms/griffel-css-shorthands"
+    `);
+});
diff --git a/packages/eslint-plugin/src/rules/styles-file.test.ts b/packages/eslint-plugin/src/rules/styles-file.test.ts
new file mode 100644
index 0000000..17f9955
--- /dev/null
+++ b/packages/eslint-plugin/src/rules/styles-file.test.ts
@@ -0,0 +1,78 @@
+import { TSESLint } from '@typescript-eslint/utils';
+import { stylesFileRule, RULE_NAME } from './styles-file';
+import * as path from 'path';
+
+const componentFileName = 'packages/components/components-foo/foo.tsx';
+const stylesFileName = 'packages/components/components-foo/foo.styles.ts';
+
+const ruleTester = new TSESLint.RuleTester({
+  parser: path.resolve('./node_modules/@typescript-eslint/parser'),
+  parserOptions: {
+    ecmaVersion: 2018,
+    sourceType: 'module',
+  },
+});
+
+const codeWithFluentNamespaceImport = `
+import * as fluent from '@fluentui/react-components';
+
+export const useStyles = fluent.makeStyles({
+  root: { color: 'blue' },
+});
+`;
+const codeWithMakeStylesImport = `
+import { makeStyles } from '@fluentui/react-components';
+
+export const useStyles = makeStyles({
+  root: { color: 'blue' },
+});
+`;
+
+ruleTester.run(RULE_NAME, stylesFileRule, {
+  valid: [
+    {
+      code: `
+        import { Button } from "@fluentui/react-components";
+        const tag = <Button>test content</Button>;
+      `,
+      filename: componentFileName,
+    },
+    // import and use makeStyles in COMPONENT_NAME.styles.ts is okay
+    {
+      code: codeWithFluentNamespaceImport,
+      filename: stylesFileName,
+    },
+    {
+      code: codeWithMakeStylesImport,
+      filename: stylesFileName,
+    },
+    {
+      code: `
+        import { makeStyles } from '@fluentui/react-components';
+
+        const useStyles = makeStyles({
+          root: { color: 'blue' },
+        });
+        const useStyles1 = makeStyles({
+          root: { color: 'red' },
+        });
+
+        export { useStyles as foo, useStyles1 as bar };
+      `,
+      filename: stylesFileName,
+    },
+  ],
+  invalid: [
+    // Error when import and use makeStyles in component
+    {
+      code: codeWithFluentNamespaceImport,
+      errors: [{ messageId: 'foundMakeStylesUsage' }],
+      filename: componentFileName,
+    },
+    {
+      code: codeWithMakeStylesImport,
+      errors: [{ messageId: 'foundMakeStylesUsage' }],
+      filename: componentFileName,
+    },
+  ],
+});