Repository: microsoft/griffel (ts)

Issue:
Passing empty string to any property creates "void 0" style when build-time optimizations are enabled
When build-time optimizations are enabled (both babel and webpack plugins), passing an empty string as the value of a CSS property generates a `void 0` style that can cause errors when inserted into the stylesheet in certain environments.

For example, the code:

```ts
import { makeStyles } from '@griffel/react';

export const useStyles = makeStyles({
  test: {
    color: '',
  },
});
```

Produces output:

```js
import { __styles } from '@griffel/react';
export const useStyles = /*#__PURE__*/__styles({
  "test": {
    "sj55zd": "fsj55zd"
  }
}, {
  "d": [void 0]
});
//# sourceMappingURL=useTestStyles.js.map
```

This is the error being thrown for a similar style in an app:

<img width="597" alt="image" src="https://user-images.githubusercontent.com/7798177/207987221-7548b626-6f08-4ad9-b1a1-48e19e0e08ec.png">

A possible solution would be to ignore rules like this, similar to how we ignore `undefined` values being passed to a property.

Pull request description:
fix: do not emit "undefined" for CSS rules
Fixes #293.

Patch:
diff --git a/change/@griffel-core-a8eb3726-f851-4e4b-835a-a1b792aec1db.json b/change/@griffel-core-a8eb3726-f851-4e4b-835a-a1b792aec1db.json
new file mode 100644
index 0000000..b781f2b
--- /dev/null
+++ b/change/@griffel-core-a8eb3726-f851-4e4b-835a-a1b792aec1db.json
@@ -0,0 +1,7 @@
+{
+  "type": "minor",
+  "comment": "feat: expose \"styleElementAttributes\" on GriffelRenderer",
+  "packageName": "@griffel/core",
+  "email": "olfedias@microsoft.com",
+  "dependentChangeType": "patch"
+}
diff --git a/change/@griffel-core-ea61242a-f63f-4c81-ba59-b813d6ed5565.json b/change/@griffel-core-ea61242a-f63f-4c81-ba59-b813d6ed5565.json
new file mode 100644
index 0000000..8b35097
--- /dev/null
+++ b/change/@griffel-core-ea61242a-f63f-4c81-ba59-b813d6ed5565.json
@@ -0,0 +1,7 @@
+{
+  "type": "patch",
+  "comment": "fix: do not emit \"undefined\" for CSS rules",
+  "packageName": "@griffel/core",
+  "email": "olfedias@microsoft.com",
+  "dependentChangeType": "patch"
+}
diff --git a/packages/core/src/renderer/createDOMRenderer.ts b/packages/core/src/renderer/createDOMRenderer.ts
index 805edd1..59c0709 100644
--- a/packages/core/src/renderer/createDOMRenderer.ts
+++ b/packages/core/src/renderer/createDOMRenderer.ts
@@ -43,10 +43,11 @@ export function createDOMRenderer(
   target: Document | undefined = typeof document === 'undefined' ? undefined : document,
   options: CreateDOMRendererOptions = {},
 ): GriffelRenderer {
-  const { unstable_filterCSSRule, compareMediaQueries = defaultCompareMediaQueries } = options;
+  const { unstable_filterCSSRule, styleElementAttributes, compareMediaQueries = defaultCompareMediaQueries } = options;
   const renderer: GriffelRenderer = {
     insertionCache: {},
     stylesheets: {},
+    styleElementAttributes: Object.freeze(styleElementAttributes),
     compareMediaQueries,
 
     id: `d${lastIndex++}`,
@@ -59,13 +60,7 @@ export function createDOMRenderer(
         // 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++) {
           const [ruleCSS, metadata] = normalizeCSSBucketEntry(cssRulesForBucket[i]);
-          const sheet = getStyleSheetForBucket(
-            styleBucketName as StyleBucketName,
-            target,
-            renderer,
-            options.styleElementAttributes,
-            metadata,
-          );
+          const sheet = getStyleSheetForBucket(styleBucketName as StyleBucketName, target, renderer, metadata);
 
           if (renderer.insertionCache[ruleCSS]) {
             continue;
diff --git a/packages/core/src/renderer/getStyleSheetForBucket.ts b/packages/core/src/renderer/getStyleSheetForBucket.ts
index 446ddf3..2b92f46 100644
--- a/packages/core/src/renderer/getStyleSheetForBucket.ts
+++ b/packages/core/src/renderer/getStyleSheetForBucket.ts
@@ -47,7 +47,6 @@ export function getStyleSheetForBucket(
   bucketName: StyleBucketName,
   target: Document | undefined,
   renderer: GriffelRenderer,
-  elementAttributes: Record<string, string> = {},
   metadata: Record<string, unknown> = {},
 ): IsomorphicStyleSheet {
   const isMediaBucket = bucketName === 'm';
@@ -56,7 +55,7 @@ export function getStyleSheetForBucket(
   if (!renderer.stylesheets[stylesheetKey]) {
     const tag: HTMLStyleElement | undefined = target && target.createElement('style');
     const stylesheet = createIsomorphicStyleSheet(tag, bucketName, {
-      ...elementAttributes,
+      ...renderer.styleElementAttributes,
       ...(isMediaBucket && { media: metadata['m'] as string }),
     });
 
diff --git a/packages/core/src/runtime/compileCSS.ts b/packages/core/src/runtime/compileCSS.ts
index ef9c1f5..49a5e1d 100644
--- a/packages/core/src/runtime/compileCSS.ts
+++ b/packages/core/src/runtime/compileCSS.ts
@@ -74,7 +74,7 @@ function createCSSRule(classNameSelector: string, cssDeclaration: string, pseudo
   return `${classNameSelector}{${cssRule}}`;
 }
 
-export function compileCSS(options: CompileCSSOptions): [string /* ltr definition */, string? /* rtl definition */] {
+export function compileCSS(options: CompileCSSOptions): [string? /* ltr definition */, string? /* rtl definition */] {
   const { className, media, layer, selectors, support, property, rtlClassName, rtlProperty, rtlValue, value } = options;
 
   const classNameSelector = `.${className}`;
@@ -105,5 +105,5 @@ export function compileCSS(options: CompileCSSOptions): [string /* ltr definitio
     cssRule = `@supports ${support} { ${cssRule} }`;
   }
 
-  return compileCSSRules(cssRule) as [string, string?];
+  return compileCSSRules(cssRule) as [string?, string?];
 }
diff --git a/packages/core/src/runtime/resolveStyleRules.ts b/packages/core/src/runtime/resolveStyleRules.ts
index 4917927..8ad8a1c 100644
--- a/packages/core/src/runtime/resolveStyleRules.ts
+++ b/packages/core/src/runtime/resolveStyleRules.ts
@@ -44,7 +44,7 @@ function createBucketEntry(cssRule: string, metadata: Record<string, unknown> |
 function pushToCSSRules(
   cssRulesByBucket: CSSRulesByBucket,
   styleBucketName: StyleBucketName,
-  ltrCSS: string,
+  ltrCSS: string | undefined,
   rtlCSS: string | undefined,
   media: string | undefined,
 ) {
@@ -54,7 +54,10 @@ function pushToCSSRules(
   }
 
   cssRulesByBucket[styleBucketName] ??= [];
-  cssRulesByBucket[styleBucketName]!.push(createBucketEntry(ltrCSS, metadata));
+
+  if (ltrCSS) {
+    cssRulesByBucket[styleBucketName]!.push(createBucketEntry(ltrCSS, metadata));
+  }
 
   if (rtlCSS) {
     cssRulesByBucket[styleBucketName]!.push(createBucketEntry(rtlCSS, metadata));
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index 68b3bbd..a4513c7 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -266,6 +266,11 @@ export interface GriffelRenderer {
    */
   stylesheets: { [key in StyleBucketName]?: IsomorphicStyleSheet } & Record<string, IsomorphicStyleSheet>;
 
+  /**
+   * @private
+   */
+  styleElementAttributes?: Readonly<Record<string, string>>;
+
   /**
    * @private
    */


Test patch:
diff --git a/packages/core/src/renderer/getStyleSheetForBucket.test.ts b/packages/core/src/renderer/getStyleSheetForBucket.test.ts
index d71a14d..05888b8 100644
--- a/packages/core/src/renderer/getStyleSheetForBucket.test.ts
+++ b/packages/core/src/renderer/getStyleSheetForBucket.test.ts
@@ -47,23 +47,23 @@ describe('getStyleSheetForBucket', () => {
     getStyleSheetForBucket('d', target, renderer);
     getStyleSheetForBucket('v', target, renderer);
 
-    getStyleSheetForBucket('m', target, renderer, {}, { m: '(max-width: 3px)' });
+    getStyleSheetForBucket('m', target, renderer, { m: '(max-width: 3px)' });
 
     getStyleSheetForBucket('a', target, renderer);
     getStyleSheetForBucket('i', target, renderer);
 
-    getStyleSheetForBucket('m', target, renderer, {}, { m: '(max-width: 1px)' });
+    getStyleSheetForBucket('m', target, renderer, { m: '(max-width: 1px)' });
 
     getStyleSheetForBucket('h', target, renderer);
 
-    getStyleSheetForBucket('m', target, renderer, {}, { m: '(max-width: 4px)' });
+    getStyleSheetForBucket('m', target, renderer, { m: '(max-width: 4px)' });
 
     getStyleSheetForBucket('w', target, renderer);
     getStyleSheetForBucket('t', target, renderer);
     getStyleSheetForBucket('k', target, renderer);
     getStyleSheetForBucket('f', target, renderer);
 
-    getStyleSheetForBucket('m', target, renderer, {}, { m: '(max-width: 2px)' });
+    getStyleSheetForBucket('m', target, renderer, { m: '(max-width: 2px)' });
 
     const styleElements = target.head.querySelectorAll(`[${DATA_BUCKET_ATTR}]`);
     const styleElementOrder = Array.from(styleElements).map(styleElement =>
diff --git a/packages/core/src/runtime/resolveStyleRules.test.ts b/packages/core/src/runtime/resolveStyleRules.test.ts
index 4a2cbae..cb97bd0 100644
--- a/packages/core/src/runtime/resolveStyleRules.test.ts
+++ b/packages/core/src/runtime/resolveStyleRules.test.ts
@@ -85,6 +85,12 @@ describe('resolveStyleRules', () => {
 
       expect(classnamesSet.size).toBe(7);
     });
+
+    it('skips invalid rules', () => {
+      const [, cssRules] = resolveStyleRules({ color: '' });
+
+      expect(cssRules).toEqual({ d: [] });
+    });
   });
 
   describe('css', () => {