Repository: microsoft/griffel (ts)

Issue:
extraction-plugin: support for appDir in NextJS 13
Hi,
I was trying to use  NextJS v13 appDir mode with `@griffel` and I get the following errors:

```bash
Attempted import error: '__css' is not exported from '@griffel/react' (imported as '__css').
```

and

```bash
TypeError: core.createDOMRenderer is not a function
    at 4415 (/home/projects/nextjs-ltekvi/.next/server/chunks/792.js:3446:88)
    at __webpack_require__ (/home/projects/nextjs-ltekvi/.next/server/webpack-runtime.js:25:43)
    at 5933 (/home/projects/nextjs-ltekvi/.next/server/chunks/792.js:3829:23)
    at __webpack_require__ (/home/projects/nextjs-ltekvi/.next/server/webpack-runtime.js:25:43)
    at 9692 (/home/projects/nextjs-ltekvi/.next/server/chunks/792.js:3710:18)
    at __webpack_require__ (/home/projects/nextjs-ltekvi/.next/server/webpack-runtime.js:25:43)
    at 4312 (/home/projects/nextjs-ltekvi/.next/server/app/page.js:99:72)
    at __webpack_require__ (/home/projects/nextjs-ltekvi/.next/server/webpack-runtime.js:25:43)
    at eval (/home/projects/nextjs-ltekvi/node_modules/next/dist/compiled/react-server-dom-webpack/client.js:918:337)
```

There are errors only when using the '@griffel/next-extraction-plugin' package. 

To reproduce these errors g to the [Sandbox](https://stackblitz.com/edit/nextjs-ltekvi) and run the command:
```bash 
npm install && npx next build
```


Pull request description:
fix: avoid empty CSS imports
Fixes #266. More details in https://github.com/microsoft/griffel/issues/266#issuecomment-1340962907.

Our Babel plugin that strips runtime also returns a set of CSS rules in its metadata. Before this PR it always returned at least an empty object. Because of this the check below didn't work:

https://github.com/microsoft/griffel/blob/1289343d950319f48888cfe70c1c84af066f08db/packages/webpack-extraction-plugin/src/webpackLoader.ts#L68

As the check was passing always we were creating invalid imports for files that contain `__styles` as a string. [Files like that](https://github.com/microsoft/fluentui/blob/f3e6e65b7f6435dc6b09bfd626e99921a10983d8/packages/merge-styles/src/Stylesheet.ts#L90) were processed => got empty set of CSS rules => created an invalid import => crashed build.

Patch:
diff --git a/apps/website/docs/react/guides/limitations.md b/apps/website/docs/react/guides/limitations.md
index 9860ea2..46ebea7 100644
--- a/apps/website/docs/react/guides/limitations.md
+++ b/apps/website/docs/react/guides/limitations.md
@@ -10,10 +10,12 @@ Styles can't be created at runtime which includes dynamic selectors as well.
 
 ```jsx
 function App(props) {
-  // ❌ This will not work and throw an expection
+  // ❌ This will not work and throw an exception
   const useClasses = makeStyles({
-    color: props.color,
-    [`.${props.area}`]: { display: 'block' },
+    root: {
+      color: props.color,
+      [`.${props.area}`]: { display: 'block' },
+    }
   });
 }
 ```
@@ -26,9 +28,9 @@ As Griffel performs ahead-of-time [compilation](/react/ahead-of-time-compilation
 
   ```jsx
   const useClasses = makeStyles({
-    twoColumns: {},
-    threeColumns: {},
-    fourColumns: {},
+    twoColumns: { /* styles */ },
+    threeColumns: { /* styles */ },
+    fourColumns: { /* styles */ },
   });
 
   function App(props) {
@@ -47,12 +49,12 @@ As Griffel performs ahead-of-time [compilation](/react/ahead-of-time-compilation
 
   ```jsx
   const useClasses = makeStyles({
-    /* your styles */
+    root: { /* your other styles styles */ },
   });
 
   function App(props) {
     const classes = useClasses();
-    return <div style={{ color: props.color }} />;
+    return <div className={classes.root} style={{ color: props.color }} />;
   }
   ```
 
diff --git a/change/@griffel-core-cb8022d0-7662-4724-a582-c6536e4413fc.json b/change/@griffel-core-cb8022d0-7662-4724-a582-c6536e4413fc.json
new file mode 100644
index 0000000..8b789f0
--- /dev/null
+++ b/change/@griffel-core-cb8022d0-7662-4724-a582-c6536e4413fc.json
@@ -0,0 +1,7 @@
+{
+  "type": "patch",
+  "comment": "fix: use global constants",
+  "packageName": "@griffel/core",
+  "email": "olfedias@microsoft.com",
+  "dependentChangeType": "patch"
+}
diff --git a/change/@griffel-webpack-extraction-plugin-46602e0c-3b1b-4553-a919-32df4dda9fd9.json b/change/@griffel-webpack-extraction-plugin-46602e0c-3b1b-4553-a919-32df4dda9fd9.json
new file mode 100644
index 0000000..9756a23
--- /dev/null
+++ b/change/@griffel-webpack-extraction-plugin-46602e0c-3b1b-4553-a919-32df4dda9fd9.json
@@ -0,0 +1,7 @@
+{
+  "type": "patch",
+  "comment": "fix: avoid empty CSS imports",
+  "packageName": "@griffel/webpack-extraction-plugin",
+  "email": "olfedias@microsoft.com",
+  "dependentChangeType": "patch"
+}
diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts
index 87f6e4a..c598930 100644
--- a/packages/core/src/constants.ts
+++ b/packages/core/src/constants.ts
@@ -1,5 +1,31 @@
 import { GriffelStylesUnsupportedCSSProperties, LookupItem, SequenceHash } from './types';
 
+// ----
+
+// Heads up!
+// These constants are global and will be shared between Griffel instances.
+// Any change in them should happen only in a MAJOR version. If it happens,
+// please change "__NAMESPACE_PREFIX__" to include a version.
+
+const __GLOBAL__: any = typeof window === 'undefined' ? global : window;
+const __NAMESPACE_PREFIX__ = '@griffel/';
+
+function getGlobalVar<T>(name: string, defaultValue: T): T {
+  if (!__GLOBAL__[Symbol.for(__NAMESPACE_PREFIX__ + name)]) {
+    __GLOBAL__[Symbol.for(__NAMESPACE_PREFIX__ + name)] = defaultValue;
+  }
+
+  return __GLOBAL__[Symbol.for(__NAMESPACE_PREFIX__ + name)];
+}
+
+/** @internal */
+export const DEBUG_RESET_CLASSES = getGlobalVar<Record<string, 1>>('DEBUG_RESET_CLASSES', {});
+
+/** @internal */
+export const DEFINITION_LOOKUP_TABLE = getGlobalVar<Record<SequenceHash, LookupItem>>('DEFINITION_LOOKUP_TABLE', {});
+
+// ----
+
 /** @internal */
 export const DATA_BUCKET_ATTR = 'data-make-styles-bucket';
 
@@ -24,12 +50,6 @@ export const SEQUENCE_SIZE =
     ? SEQUENCE_PREFIX.length + SEQUENCE_HASH_LENGTH
     : SEQUENCE_PREFIX.length + SEQUENCE_HASH_LENGTH + DEBUG_SEQUENCE_SEPARATOR.length + SEQUENCE_HASH_LENGTH;
 
-/** @internal */
-export const DEBUG_RESET_CLASSES: Record<string, 1> = {};
-
-/** @internal */
-export const DEFINITION_LOOKUP_TABLE: Record<SequenceHash, LookupItem> = {};
-
 // indexes for values in LookupItem tuple
 
 /** @internal */
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/missing-calls/code.ts b/packages/webpack-extraction-plugin/__fixtures__/webpack/missing-calls/code.ts
new file mode 100644
index 0000000..bb5adbd
--- /dev/null
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/missing-calls/code.ts
@@ -0,0 +1,1 @@
+export const foo = '__styles';
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/missing-calls/output.ts b/packages/webpack-extraction-plugin/__fixtures__/webpack/missing-calls/output.ts
new file mode 100644
index 0000000..bb5adbd
--- /dev/null
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/missing-calls/output.ts
@@ -0,0 +1,1 @@
+export const foo = '__styles';
diff --git a/packages/webpack-extraction-plugin/src/babelPluginStripGriffelRuntime.ts b/packages/webpack-extraction-plugin/src/babelPluginStripGriffelRuntime.ts
index a2a8240..f25014e 100644
--- a/packages/webpack-extraction-plugin/src/babelPluginStripGriffelRuntime.ts
+++ b/packages/webpack-extraction-plugin/src/babelPluginStripGriffelRuntime.ts
@@ -1,6 +1,6 @@
 import { NodePath, PluginObj, PluginPass, types as t } from '@babel/core';
 import { declare } from '@babel/helper-plugin-utils';
-import { CSSRulesByBucket } from '@griffel/core';
+import type { CSSRulesByBucket } from '@griffel/core';
 import * as path from 'path';
 
 type StripRuntimeBabelPluginOptions = {
@@ -15,7 +15,7 @@ type StripRuntimeBabelPluginState = PluginPass & {
 };
 
 export type StripRuntimeBabelPluginMetadata = {
-  cssRulesByBucket: CSSRulesByBucket;
+  cssRulesByBucket?: CSSRulesByBucket;
 };
 
 export function transformUrl(filename: string, resourceDirectory: string, assetPath: string) {
@@ -30,6 +30,16 @@ export function transformUrl(filename: string, resourceDirectory: string, assetP
   return relativeAssetPath.split(path.sep).join(path.posix.sep);
 }
 
+function concatCSSRulesByBucket(bucketA: CSSRulesByBucket = {}, bucketB: CSSRulesByBucket) {
+  Object.entries(bucketB).forEach(([cssBucketName, cssBucketEntries]) => {
+    bucketA[cssBucketName as keyof CSSRulesByBucket] = cssBucketEntries.concat(
+      bucketA[cssBucketName as keyof CSSRulesByBucket] || [],
+    );
+  });
+
+  return bucketA;
+}
+
 export const babelPluginStripGriffelRuntime = declare<
   Partial<StripRuntimeBabelPluginOptions>,
   PluginObj<StripRuntimeBabelPluginState>
@@ -38,11 +48,8 @@ export const babelPluginStripGriffelRuntime = declare<
 
   return {
     name: '@griffel/webpack-extraction-plugin/babel',
-    pre() {
-      this.cssRulesByBucket = {};
-    },
     post() {
-      (this.file.metadata as unknown as StripRuntimeBabelPluginMetadata).cssRulesByBucket = this.cssRulesByBucket!;
+      (this.file.metadata as unknown as StripRuntimeBabelPluginMetadata).cssRulesByBucket = this.cssRulesByBucket;
     },
 
     visitor: {
@@ -215,15 +222,11 @@ export const babelPluginStripGriffelRuntime = declare<
               if (functionKind === '__styles') {
                 const cssRulesByBucket = evaluationResult.value as CSSRulesByBucket;
 
-                Object.entries(cssRulesByBucket).forEach(([cssBucketName, cssBucketEntries]) => {
-                  state.cssRulesByBucket![cssBucketName as keyof CSSRulesByBucket] = cssBucketEntries.concat(
-                    state.cssRulesByBucket![cssBucketName as keyof CSSRulesByBucket] || [],
-                  );
-                });
+                state.cssRulesByBucket = concatCSSRulesByBucket(state.cssRulesByBucket, cssRulesByBucket);
               } else if (functionKind === '__resetStyles') {
                 const cssRules = evaluationResult.value as NonNullable<CSSRulesByBucket['r']>;
 
-                state.cssRulesByBucket!.r = cssRules.concat(state.cssRulesByBucket!.r || []);
+                state.cssRulesByBucket = concatCSSRulesByBucket(state.cssRulesByBucket, { r: cssRules });
               }
 
               argumentPath.remove();


Test patch:
diff --git a/packages/core/src/constants.test.ts b/packages/core/src/constants.test.ts
new file mode 100644
index 0000000..8f16899
--- /dev/null
+++ b/packages/core/src/constants.test.ts
@@ -0,0 +1,14 @@
+it('hosts global constants', async () => {
+  const importA = await import('./constants');
+
+  // We are reloading a module, so instances that are created inside a module will be different
+  // https://jestjs.io/docs/jest-object#jestresetmodules
+  jest.resetModules();
+  const importB = await import('./constants');
+
+  expect(importA.DEFINITION_LOOKUP_TABLE).toBe(importA.DEFINITION_LOOKUP_TABLE);
+  expect(importA.DEBUG_RESET_CLASSES).toBe(importA.DEBUG_RESET_CLASSES);
+
+  // This constant is not global, expected to be different
+  expect(importA.UNSUPPORTED_CSS_PROPERTIES).not.toBe(importB.UNSUPPORTED_CSS_PROPERTIES);
+});
diff --git a/packages/webpack-extraction-plugin/src/webpackLoader.test.ts b/packages/webpack-extraction-plugin/src/webpackLoader.test.ts
index 8744ce7..8201d1a 100644
--- a/packages/webpack-extraction-plugin/src/webpackLoader.test.ts
+++ b/packages/webpack-extraction-plugin/src/webpackLoader.test.ts
@@ -190,4 +190,7 @@ describe('webpackLoader', () => {
   testFixture('assets');
   testFixture('assets-multiple');
   testFixture('reset-assets');
+
+  // Ensures that a file without __styles calls remains unprocessed
+  testFixture('missing-calls');
 });